blob: 3ea193681df7250cf939bf6cf5021812f3869f74 [file] [log] [blame]
/*
* libdpkg - Debian packaging suite library routines
* tarfn.c - tar archive extraction functions
*
* Copyright © 1995 Bruce Perens
* Copyright © 2007-2010 Guillem Jover <guillem@debian.org>
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <compat.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
#include <dpkg/macros.h>
#include <dpkg/dpkg.h>
#include <dpkg/tarfn.h>
#define TAR_MAGIC_USTAR "ustar\0" "00"
#define TAR_MAGIC_GNU "ustar " " \0"
struct tar_header {
char name[100];
char mode[8];
char uid[8];
char gid[8];
char size[12];
char mtime[12];
char checksum[8];
char linkflag;
char linkname[100];
char magic[8];
char user[32];
char group[32];
char devmajor[8];
char devminor[8];
/* Only valid on ustar. */
char prefix[155];
};
/**
* Convert an ASCII octal string to an intmax_t.
*/
static intmax_t
OtoM(const char *s, int size)
{
intmax_t n = 0;
while (*s == ' ') {
s++;
size--;
}
while (--size >= 0 && *s >= '0' && *s <= '7')
n = (n * 010) + (*s++ - '0');
return n;
}
/**
* Convert a string block to C NUL-terminated string.
*/
static char *
StoC(const char *s, int size)
{
int len;
char *str;
len = strnlen(s, size);
str = m_malloc(len + 1);
memcpy(str, s, len);
str[len] = '\0';
return str;
}
static char *
get_prefix_name(struct tar_header *h)
{
char *path;
m_asprintf(&path, "%.*s/%.*s", (int)sizeof(h->prefix), h->prefix,
(int)sizeof(h->name), h->name);
return path;
}
static mode_t
get_unix_mode(struct tar_header *h)
{
mode_t mode;
enum tar_filetype type;
type = (enum tar_filetype)h->linkflag;
switch (type) {
case tar_filetype_file0:
case tar_filetype_file:
mode = S_IFREG;
break;
case tar_filetype_symlink:
mode = S_IFLNK;
break;
case tar_filetype_dir:
mode = S_IFDIR;
break;
case tar_filetype_chardev:
mode = S_IFCHR;
break;
case tar_filetype_blockdev:
mode = S_IFBLK;
break;
case tar_filetype_fifo:
mode = S_IFIFO;
break;
case tar_filetype_hardlink:
default:
mode = 0;
break;
}
mode |= OtoM(h->mode, sizeof(h->mode));
return mode;
}
static bool
tar_header_checksum(struct tar_header *h)
{
unsigned char *s = (unsigned char *)h;
unsigned int i;
const size_t checksum_offset = offsetof(struct tar_header, checksum);
long checksum;
long sum;
checksum = OtoM(h->checksum, sizeof(h->checksum));
/* Treat checksum field as all blank. */
sum = ' ' * sizeof(h->checksum);
for (i = checksum_offset; i > 0; i--)
sum += *s++;
/* Skip the real checksum field. */
s += sizeof(h->checksum);
for (i = TARBLKSZ - checksum_offset - sizeof(h->checksum); i > 0; i--)
sum += *s++;
return (sum == checksum);
}
static int
tar_header_decode(struct tar_header *h, struct tar_entry *d)
{
struct passwd *passwd = NULL;
struct group *group = NULL;
if (memcmp(h->magic, TAR_MAGIC_GNU, 6) == 0)
d->format = tar_format_gnu;
else if (memcmp(h->magic, TAR_MAGIC_USTAR, 6) == 0)
d->format = tar_format_ustar;
else
d->format = tar_format_old;
d->type = (enum tar_filetype)h->linkflag;
if (d->type == tar_filetype_file0)
d->type = tar_filetype_file;
/* Concatenate prefix and name to support ustar style long names. */
if (d->format == tar_format_ustar && h->prefix[0] != '\0')
d->name = get_prefix_name(h);
else
d->name = StoC(h->name, sizeof(h->name));
d->linkname = StoC(h->linkname, sizeof(h->linkname));
d->stat.mode = get_unix_mode(h);
d->size = (off_t)OtoM(h->size, sizeof(h->size));
d->mtime = (time_t)OtoM(h->mtime, sizeof(h->mtime));
d->dev = ((OtoM(h->devmajor, sizeof(h->devmajor)) & 0xff) << 8) |
(OtoM(h->devminor, sizeof(h->devminor)) & 0xff);
if (*h->user)
passwd = getpwnam(h->user);
if (passwd)
d->stat.uid = passwd->pw_uid;
else
d->stat.uid = (uid_t)OtoM(h->uid, sizeof(h->uid));
if (*h->group)
group = getgrnam(h->group);
if (group)
d->stat.gid = group->gr_gid;
else
d->stat.gid = (gid_t)OtoM(h->gid, sizeof(h->gid));
return tar_header_checksum(h);
}
/**
* Decode a GNU longlink or longname from the tar archive.
*
* The way the GNU long{link,name} stuff works is like this:
*
* - The first header is a “dummy” header that contains the size of the
* filename.
* - The next N headers contain the filename.
* - After the headers with the filename comes the “real” header with a
* bogus name or link.
*/
static int
tar_gnu_long(void *ctx, const struct tar_operations *ops, struct tar_entry *te,
char **longp)
{
char buf[TARBLKSZ];
char *bp;
int status = 0;
int long_read;
free(*longp);
*longp = bp = m_malloc(te->size);
for (long_read = te->size; long_read > 0; long_read -= TARBLKSZ) {
int copysize;
status = ops->read(ctx, buf, TARBLKSZ);
if (status == TARBLKSZ)
status = 0;
else {
/* Read partial header record? */
if (status > 0) {
errno = 0;
status = -1;
}
/* If we didn't get TARBLKSZ bytes read, punt. */
break;
}
copysize = min(long_read, TARBLKSZ);
memcpy(bp, buf, copysize);
bp += copysize;
};
return status;
}
struct symlinkList {
struct symlinkList *next;
struct tar_entry h;
};
int
tar_extractor(void *ctx, const struct tar_operations *ops)
{
int status;
char buffer[TARBLKSZ];
struct tar_entry h;
char *next_long_name, *next_long_link;
struct symlinkList *symlink_head, *symlink_tail, *symlink_node;
next_long_name = NULL;
next_long_link = NULL;
symlink_tail = symlink_head = NULL;
h.name = NULL;
h.linkname = NULL;
while ((status = ops->read(ctx, buffer, TARBLKSZ)) == TARBLKSZ) {
int name_len;
if (!tar_header_decode((struct tar_header *)buffer, &h)) {
if (h.name[0] == '\0') {
/* End of tape. */
status = 0;
} else {
/* Indicates broken tarfile:
* “Header checksum error”. */
errno = 0;
status = -1;
}
break;
}
if (h.type != tar_filetype_gnu_longlink &&
h.type != tar_filetype_gnu_longname) {
if (next_long_name)
h.name = next_long_name;
if (next_long_link)
h.linkname = next_long_link;
next_long_link = NULL;
next_long_name = NULL;
}
if (h.name[0] == '\0') {
/* Indicates broken tarfile: “Bad header data”. */
errno = 0;
status = -1;
break;
}
name_len = strlen(h.name);
switch (h.type) {
case tar_filetype_file:
/* Compatibility with pre-ANSI ustar. */
if (h.name[name_len - 1] != '/') {
status = ops->extract_file(ctx, &h);
break;
}
/* Else, fall through. */
case tar_filetype_dir:
if (h.name[name_len - 1] == '/') {
h.name[name_len - 1] = '\0';
}
status = ops->mkdir(ctx, &h);
break;
case tar_filetype_hardlink:
status = ops->link(ctx, &h);
break;
case tar_filetype_symlink:
symlink_node = m_malloc(sizeof(*symlink_node));
memcpy(&symlink_node->h, &h, sizeof(struct tar_entry));
symlink_node->h.name = m_strdup(h.name);
symlink_node->h.linkname = m_strdup(h.linkname);
symlink_node->next = NULL;
if (symlink_head)
symlink_tail->next = symlink_node;
else
symlink_head = symlink_node;
symlink_tail = symlink_node;
status = 0;
break;
case tar_filetype_chardev:
case tar_filetype_blockdev:
case tar_filetype_fifo:
status = ops->mknod(ctx, &h);
break;
case tar_filetype_gnu_longlink:
status = tar_gnu_long(ctx, ops, &h, &next_long_link);
break;
case tar_filetype_gnu_longname:
status = tar_gnu_long(ctx, ops, &h, &next_long_name);
break;
default:
/* Indicates broken tarfile: “Bad header field”. */
errno = 0;
status = -1;
}
if (status != 0)
/* Pass on status from coroutine. */
break;
}
while (symlink_head) {
symlink_node = symlink_head->next;
if (status == 0)
status = ops->symlink(ctx, &symlink_head->h);
free(symlink_head->h.name);
free(symlink_head->h.linkname);
free(symlink_head);
symlink_head = symlink_node;
}
free(h.name);
free(h.linkname);
if (status > 0) {
/* Indicates broken tarfile: “Read partial header record”. */
errno = 0;
return -1;
} else {
/* Return whatever I/O function returned. */
return status;
}
}