summaryrefslogtreecommitdiffstats
path: root/lib/dpkg/tarfn.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dpkg/tarfn.c')
-rw-r--r--lib/dpkg/tarfn.c601
1 files changed, 601 insertions, 0 deletions
diff --git a/lib/dpkg/tarfn.c b/lib/dpkg/tarfn.c
new file mode 100644
index 0000000..142931d
--- /dev/null
+++ b/lib/dpkg/tarfn.c
@@ -0,0 +1,601 @@
+/*
+ * libdpkg - Debian packaging suite library routines
+ * tarfn.c - tar archive extraction functions
+ *
+ * Copyright © 1995 Bruce Perens
+ * Copyright © 2007-2011, 2013-2017 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <compat.h>
+
+#if HAVE_SYS_SYSMACROS_H
+#include <sys/sysmacros.h>
+#endif
+#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/i18n.h>
+#include <dpkg/error.h>
+#include <dpkg/tarfn.h>
+
+#define TAR_MAGIC_USTAR "ustar\0" "00"
+#define TAR_MAGIC_GNU "ustar " " \0"
+
+#define TAR_TYPE_SIGNED(t) (!((t)0 < (t)-1))
+
+#define TAR_TYPE_MIN(t) \
+ (TAR_TYPE_SIGNED(t) ? \
+ ~(t)TAR_TYPE_MAX(t) : \
+ (t)0)
+#define TAR_TYPE_MAX(t) \
+ (TAR_TYPE_SIGNED(t) ? \
+ ((((t)1 << (sizeof(t) * 8 - 2)) - 1) * 2 + 1) : \
+ ~(t)0)
+
+#define TAR_ATOUL(str, type) \
+ (type)tar_atoul(str, sizeof(str), TAR_TYPE_MAX(type))
+#define TAR_ATOSL(str, type) \
+ (type)tar_atosl(str, sizeof(str), TAR_TYPE_MIN(type), TAR_TYPE_MAX(type))
+
+struct tar_header {
+ char name[100] DPKG_ATTR_NONSTRING;
+ char mode[8] DPKG_ATTR_NONSTRING;
+ char uid[8] DPKG_ATTR_NONSTRING;
+ char gid[8] DPKG_ATTR_NONSTRING;
+ char size[12] DPKG_ATTR_NONSTRING;
+ char mtime[12] DPKG_ATTR_NONSTRING;
+ char checksum[8] DPKG_ATTR_NONSTRING;
+ char linkflag;
+ char linkname[100] DPKG_ATTR_NONSTRING;
+
+ /* Only valid on ustar and gnu. */
+ char magic[8] DPKG_ATTR_NONSTRING;
+ char user[32] DPKG_ATTR_NONSTRING;
+ char group[32] DPKG_ATTR_NONSTRING;
+ char devmajor[8] DPKG_ATTR_NONSTRING;
+ char devminor[8] DPKG_ATTR_NONSTRING;
+
+ /* Only valid on ustar. */
+ char prefix[155] DPKG_ATTR_NONSTRING;
+};
+
+static inline uintmax_t
+tar_ret_errno(int err, uintmax_t ret)
+{
+ errno = err;
+ return ret;
+}
+
+/**
+ * Convert an ASCII octal string to an intmax_t.
+ */
+static uintmax_t
+tar_atol8(const char *s, size_t size)
+{
+ const char *end = s + size;
+ uintmax_t n = 0;
+
+ /* Old implementations might precede the value with spaces. */
+ while (s < end && *s == ' ')
+ s++;
+
+ if (s == end)
+ return tar_ret_errno(EINVAL, 0);
+
+ while (s < end) {
+ if (*s == '\0' || *s == ' ')
+ break;
+ if (*s < '0' || *s > '7')
+ return tar_ret_errno(ERANGE, 0);
+ n = (n * 010) + (*s++ - '0');
+ }
+
+ while (s < end) {
+ if (*s != '\0' && *s != ' ')
+ return tar_ret_errno(EINVAL, 0);
+ s++;
+ }
+
+ if (s < end)
+ return tar_ret_errno(EINVAL, 0);
+
+ return tar_ret_errno(0, n);
+}
+
+/**
+ * Convert a base-256 two-complement number to an intmax_t.
+ */
+static uintmax_t
+tar_atol256(const char *s, size_t size, intmax_t min, uintmax_t max)
+{
+ uintmax_t n = 0;
+ unsigned char c;
+ int sign;
+
+ /* The encoding always sets the first bit to one, so that it can be
+ * distinguished from the ASCII encoding. For positive numbers we
+ * need to reset it. For negative numbers we initialize n to -1. */
+ c = *s++;
+ if (c == 0x80)
+ c = 0;
+ else
+ n = ~(uintmax_t)0;
+ sign = c;
+
+ /* Check for overflows. */
+ while (size > sizeof(uintmax_t)) {
+ if (c != sign)
+ return tar_ret_errno(ERANGE, sign ? (uintmax_t)min : max);
+ c = *s++;
+ size--;
+ }
+
+ if ((c & 0x80) != (sign & 0x80))
+ return tar_ret_errno(ERANGE, sign ? (uintmax_t)min : max);
+
+ for (;;) {
+ n = (n << 8) | c;
+ if (--size == 0)
+ break;
+ c = *s++;
+ }
+
+ return tar_ret_errno(0, n);
+}
+
+static uintmax_t
+tar_atol(const char *s, size_t size, intmax_t min, uintmax_t max)
+{
+ const unsigned char *a = (const unsigned char *)s;
+
+ /* Check if it is a long two-complement base-256 number, positive or
+ * negative. */
+ if (*a == 0xff || *a == 0x80)
+ return tar_atol256(s, size, min, max);
+ else
+ return tar_atol8(s, size);
+}
+
+uintmax_t
+tar_atoul(const char *s, size_t size, uintmax_t max)
+{
+ uintmax_t n = tar_atol(s, size, 0, UINTMAX_MAX);
+
+ if (n > max)
+ return tar_ret_errno(ERANGE, UINTMAX_MAX);
+
+ return n;
+}
+
+intmax_t
+tar_atosl(const char *s, size_t size, intmax_t min, intmax_t max)
+{
+ intmax_t n = tar_atol(s, size, INTMAX_MIN, INTMAX_MAX);
+
+ if (n < min)
+ return tar_ret_errno(ERANGE, INTMAX_MIN);
+ if (n > max)
+ return tar_ret_errno(ERANGE, INTMAX_MAX);
+
+ return n;
+}
+
+static char *
+tar_header_get_prefix_name(struct tar_header *h)
+{
+ return str_fmt("%.*s/%.*s", (int)sizeof(h->prefix), h->prefix,
+ (int)sizeof(h->name), h->name);
+}
+
+static mode_t
+tar_header_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:
+ case TAR_FILETYPE_HARDLINK:
+ 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;
+ default:
+ mode = 0;
+ break;
+ }
+
+ mode |= TAR_ATOUL(h->mode, mode_t);
+
+ return mode;
+}
+
+static long
+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 sum;
+
+ /* 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;
+}
+
+static int
+tar_header_decode(struct tar_header *h, struct tar_entry *d, struct dpkg_error *err)
+{
+ long checksum;
+
+ errno = 0;
+
+ 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 = tar_header_get_prefix_name(h);
+ else
+ d->name = m_strndup(h->name, sizeof(h->name));
+ d->linkname = m_strndup(h->linkname, sizeof(h->linkname));
+ d->stat.mode = tar_header_get_unix_mode(h);
+ /* Even though off_t is signed, we use an unsigned parser here because
+ * negative offsets are not allowed. */
+ d->size = TAR_ATOUL(h->size, off_t);
+ if (errno)
+ return dpkg_put_errno(err, _("invalid tar header size field"));
+ d->mtime = TAR_ATOSL(h->mtime, time_t);
+ if (errno)
+ return dpkg_put_errno(err, _("invalid tar header mtime field"));
+
+ if (d->type == TAR_FILETYPE_CHARDEV || d->type == TAR_FILETYPE_BLOCKDEV)
+ d->dev = makedev(TAR_ATOUL(h->devmajor, dev_t),
+ TAR_ATOUL(h->devminor, dev_t));
+ else
+ d->dev = makedev(0, 0);
+
+ if (*h->user)
+ d->stat.uname = m_strndup(h->user, sizeof(h->user));
+ else
+ d->stat.uname = NULL;
+ d->stat.uid = TAR_ATOUL(h->uid, uid_t);
+ if (errno)
+ return dpkg_put_errno(err, _("invalid tar header uid field"));
+
+ if (*h->group)
+ d->stat.gname = m_strndup(h->group, sizeof(h->group));
+ else
+ d->stat.gname = NULL;
+ d->stat.gid = TAR_ATOUL(h->gid, gid_t);
+ if (errno)
+ return dpkg_put_errno(err, _("invalid tar header gid field"));
+
+ checksum = tar_atol8(h->checksum, sizeof(h->checksum));
+ if (errno)
+ return dpkg_put_errno(err, _("invalid tar header checksum field"));
+
+ if (tar_header_checksum(h) != checksum)
+ return dpkg_put_error(err, _("invalid tar header checksum"));
+
+ return 0;
+}
+
+/**
+ * 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(struct tar_archive *tar, 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 = tar->ops->read(tar, buf, TARBLKSZ);
+ if (status == TARBLKSZ)
+ status = 0;
+ else {
+ /* Read partial header record? */
+ if (status > 0) {
+ errno = 0;
+ status = dpkg_put_error(&tar->err,
+ _("partially read tar header"));
+ }
+
+ /* If we didn't get TARBLKSZ bytes read, punt. */
+ break;
+ }
+
+ copysize = min(long_read, TARBLKSZ);
+ memcpy(bp, buf, copysize);
+ bp += copysize;
+ }
+
+ return status;
+}
+
+static void
+tar_entry_copy(struct tar_entry *dst, struct tar_entry *src)
+{
+ memcpy(dst, src, sizeof(struct tar_entry));
+
+ dst->name = m_strdup(src->name);
+ dst->linkname = m_strdup(src->linkname);
+
+ if (src->stat.uname)
+ dst->stat.uname = m_strdup(src->stat.uname);
+ if (src->stat.gname)
+ dst->stat.gname = m_strdup(src->stat.gname);
+}
+
+static void
+tar_entry_destroy(struct tar_entry *te)
+{
+ free(te->name);
+ free(te->linkname);
+ free(te->stat.uname);
+ free(te->stat.gname);
+
+ memset(te, 0, sizeof(*te));
+}
+
+struct tar_symlink_entry {
+ struct tar_symlink_entry *next;
+ struct tar_entry h;
+};
+
+/**
+ * Update the tar entry from system information.
+ *
+ * Normalize UID and GID relative to the current system.
+ */
+void
+tar_entry_update_from_system(struct tar_entry *te)
+{
+ struct passwd *passwd;
+ struct group *group;
+
+ if (te->stat.uname) {
+ passwd = getpwnam(te->stat.uname);
+ if (passwd)
+ te->stat.uid = passwd->pw_uid;
+ }
+ if (te->stat.gname) {
+ group = getgrnam(te->stat.gname);
+ if (group)
+ te->stat.gid = group->gr_gid;
+ }
+}
+
+int
+tar_extractor(struct tar_archive *tar)
+{
+ int status;
+ char buffer[TARBLKSZ];
+ struct tar_entry h;
+
+ char *next_long_name, *next_long_link;
+ struct tar_symlink_entry *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;
+ h.stat.uname = NULL;
+ h.stat.gname = NULL;
+
+ while ((status = tar->ops->read(tar, buffer, TARBLKSZ)) == TARBLKSZ) {
+ int name_len;
+
+ if (tar_header_decode((struct tar_header *)buffer, &h, &tar->err) < 0) {
+ if (h.name[0] == '\0') {
+ /* The checksum failed on the terminating
+ * End Of Tape block entry of zeros. */
+ dpkg_error_destroy(&tar->err);
+
+ /* End Of Tape. */
+ status = 0;
+ } else {
+ status = -1;
+ }
+ tar_entry_destroy(&h);
+ break;
+ }
+ if (h.type != TAR_FILETYPE_GNU_LONGLINK &&
+ h.type != TAR_FILETYPE_GNU_LONGNAME) {
+ if (next_long_name) {
+ free(h.name);
+ h.name = next_long_name;
+ }
+
+ if (next_long_link) {
+ free(h.linkname);
+ h.linkname = next_long_link;
+ }
+
+ next_long_link = NULL;
+ next_long_name = NULL;
+ }
+
+ if (h.name[0] == '\0') {
+ status = dpkg_put_error(&tar->err,
+ _("invalid tar header with empty name field"));
+ errno = 0;
+ tar_entry_destroy(&h);
+ 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 = tar->ops->extract_file(tar, &h);
+ break;
+ }
+ /* Else, fall through. */
+ case TAR_FILETYPE_DIR:
+ if (h.name[name_len - 1] == '/') {
+ h.name[name_len - 1] = '\0';
+ }
+ status = tar->ops->mkdir(tar, &h);
+ break;
+ case TAR_FILETYPE_HARDLINK:
+ status = tar->ops->link(tar, &h);
+ break;
+ case TAR_FILETYPE_SYMLINK:
+ symlink_node = m_malloc(sizeof(*symlink_node));
+ symlink_node->next = NULL;
+ tar_entry_copy(&symlink_node->h, &h);
+
+ 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 = tar->ops->mknod(tar, &h);
+ break;
+ case TAR_FILETYPE_GNU_LONGLINK:
+ status = tar_gnu_long(tar, &h, &next_long_link);
+ break;
+ case TAR_FILETYPE_GNU_LONGNAME:
+ status = tar_gnu_long(tar, &h, &next_long_name);
+ break;
+ case TAR_FILETYPE_GNU_VOLUME:
+ case TAR_FILETYPE_GNU_MULTIVOL:
+ case TAR_FILETYPE_GNU_SPARSE:
+ case TAR_FILETYPE_GNU_DUMPDIR:
+ status = dpkg_put_error(&tar->err,
+ _("unsupported GNU tar header type '%c'"),
+ h.type);
+ errno = 0;
+ break;
+ case TAR_FILETYPE_SOLARIS_EXTENDED:
+ case TAR_FILETYPE_SOLARIS_ACL:
+ status = dpkg_put_error(&tar->err,
+ _("unsupported Solaris tar header type '%c'"),
+ h.type);
+ errno = 0;
+ break;
+ case TAR_FILETYPE_PAX_GLOBAL:
+ case TAR_FILETYPE_PAX_EXTENDED:
+ status = dpkg_put_error(&tar->err,
+ _("unsupported PAX tar header type '%c'"),
+ h.type);
+ errno = 0;
+ break;
+ default:
+ status = dpkg_put_error(&tar->err,
+ _("unknown tar header type '%c'"),
+ h.type);
+ errno = 0;
+ }
+ tar_entry_destroy(&h);
+ if (status != 0)
+ /* Pass on status from coroutine. */
+ break;
+ }
+
+ while (symlink_head) {
+ symlink_node = symlink_head->next;
+ if (status == 0)
+ status = tar->ops->symlink(tar, &symlink_head->h);
+ tar_entry_destroy(&symlink_head->h);
+ free(symlink_head);
+ symlink_head = symlink_node;
+ }
+ /* Make sure we free the long names, in case of a bogus or truncated
+ * tar archive with long entries not followed by a normal entry. */
+ free(next_long_name);
+ free(next_long_link);
+
+ if (status > 0) {
+ status = dpkg_put_error(&tar->err,
+ _("partially read tar header"));
+ errno = 0;
+ }
+
+ /* Return whatever I/O function returned. */
+ return status;
+}