/* vi: set expandtab sw=4 sts=4: */ /* opkg_archive.c - the opkg package management system Copyright (C) 2014 Paul Barker SPDX-License-Identifier: GPL-2.0-or-later This program 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, or (at your option) any later version. This program 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. */ #include "config.h" #include #include #include #include #include #include #include "opkg_conf.h" #include "opkg_message.h" #include "opkg_archive.h" #include "file_util.h" #include "sprintf_alloc.h" #include "xfuncs.h" /******************************************************************************* * internal functions */ struct inner_data { /* Pointer to the original archive file we're extracting from. */ struct archive *outer; /* Buffer for extracted data. */ void *buffer; }; static ssize_t inner_read(struct archive *a, void *client_data, const void **buff) { (void)a; struct inner_data *data = (struct inner_data *)client_data; *buff = data->buffer; return archive_read_data(data->outer, data->buffer, EXTRACT_BUFFER_LEN); } static int inner_close(struct archive *inner, void *client_data) { (void)inner; struct inner_data *data = (struct inner_data *)client_data; archive_read_free(data->outer); free(data->buffer); free(data); return ARCHIVE_OK; } /* Read data from an archive into a buffer. Returns 0 on error as the return * value is an unsigned size_t. If an eof pointer is provided, it will be set to * 1 on EOF or 0 otherwise. */ static size_t read_data(struct archive *a, void *buffer, size_t len, int *eof) { ssize_t r; int retries = 0; if (eof) *eof = 0; retry: /* Read data into buffer. */ r = archive_read_data(a, buffer, len); if (r > 0) { /* We got good data. */ return (size_t) r; } switch (r) { case 0: /* We reached EOF. */ if (eof) *eof = 1; return 0; case ARCHIVE_WARN: /* We don't know the size read so we have to treat this * as an error. */ opkg_msg(ERROR, "Warning when reading data from archive: %s (errno=%d)\n", archive_error_string(a), archive_errno(a)); return 0; case ARCHIVE_RETRY: opkg_msg(ERROR, "Failed to read data from archive: %s (errno=%d)\n", archive_error_string(a), archive_errno(a)); if (retries++ < 3) { opkg_msg(NOTICE, "Retrying..."); goto retry; } else return 0; default: opkg_msg(ERROR, "Failed to read data from archive: %s (errno=%d)\n", archive_error_string(a), archive_errno(a)); return 0; } } static int copy_to_stream(struct archive *a, FILE * stream) { void *buffer; size_t sz_out, sz_in; int eof; size_t len = EXTRACT_BUFFER_LEN; if (archive_format(a) == ARCHIVE_FORMAT_EMPTY) return 0; buffer = xmalloc(len); while (1) { sz_in = read_data(a, buffer, len, &eof); if (eof) { free(buffer); return 0; } if (sz_in == 0) goto err_cleanup; /* Now write data to the output stream. */ sz_out = fwrite(buffer, 1, sz_in, stream); if (sz_out < sz_in) { opkg_msg(ERROR, "Failed to write data to stream: %s\n", strerror(errno)); goto err_cleanup; } } err_cleanup: free(buffer); return -1; } /* Join left and right path components without intervening '/' as left may end * with a prefix to be applied to the names of extracted files. * * The right path component is stripped of leading '/' or './' components. If * the right path component is simply '.' after stripping, it should not be * created. * * Returns the joined path, or NULL if the resulting path should not be created. */ static char *join_paths(const char *left, const char *right) { char *path; /* Skip leading '/' or './' in right-hand path if present. */ while (right[0] == '.' && right[1] == '/') right += 2; while (right[0] == '/') right++; /* Don't create '.' directory. */ if (right[0] == '.' && right[1] == '\0') return NULL; /* Don't extract empty paths. */ if (right[0] == '\0') return NULL; if (!left) return xstrdup(right); sprintf_alloc(&path, "%s%s", left, right); return path; } /* Transform the destination path of the given entry. * * Returns 0 on success, 1 where the file does not need to be extracted and <0 * on error. */ static int transform_dest_path(struct archive_entry *entry, const char *dest) { char *path; const char *filename; filename = archive_entry_pathname(entry); path = join_paths(dest, filename); if (!path) return 1; archive_entry_set_pathname(entry, path); free(path); return 0; } /* Transform all path components of the given entry. This includes hardlink and * symlink paths as well as the destination path. * * Returns 0 on success, 1 where the file does not need to be extracted and <0 * on error. */ static int transform_all_paths(struct archive_entry *entry, const char *dest) { char *path; const char *filename; int r; r = transform_dest_path(entry, dest); if (r) return r; /* Next transform hardlink and symlink destinations. */ filename = archive_entry_hardlink(entry); if (filename) { /* Apply the same transform to the hardlink path as was applied * to the destination path. */ path = join_paths(dest, filename); if (!path) { opkg_msg(ERROR, "Not extracting '%s': Hardlink to nowhere.\n", archive_entry_pathname(entry)); return 1; } archive_entry_set_hardlink(entry, path); free(path); } /* Currently no transform to perform for symlinks. */ return 0; } /* Read a header from the given archive object and handle all possible outcomes. * The returned pointer is managed by libarchive and should not be freed by the * caller. * * If the caller needs to know whether EOF was hit without an error, they can * pass an eof pointer which will be set to 1 in this case or 0 otherwise. */ static struct archive_entry *read_header(struct archive *ar, int *eof) { struct archive_entry *entry; int r; int retries = 0; if (eof) *eof = 0; retry: r = archive_read_next_header(ar, &entry); switch (r) { case ARCHIVE_OK: break; case ARCHIVE_WARN: opkg_msg(NOTICE, "Warning when reading ar archive header: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); break; case ARCHIVE_EOF: if (eof) *eof = 1; return NULL; case ARCHIVE_RETRY: opkg_msg(ERROR, "Failed to read archive header: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); if (retries++ < 3) goto retry; else return NULL; default: opkg_msg(ERROR, "Failed to read archive header: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); return NULL; } return entry; } /* Extact a single file from an open archive, writing data to an open stream. * Returns 0 on success or <0 on error. */ static int extract_file_to_stream(struct archive *a, const char *name, FILE * stream) { struct archive_entry *entry; const char *path; while (1) { entry = read_header(a, NULL); if (!entry) return -1; /* Cleanup the path of the entry incase it starts with './' or * other prefixes. * * We ignore the return value of transform_dest_path as bad * paths simply won't match the requested path name. */ transform_dest_path(entry, NULL); path = archive_entry_pathname(entry); if (strcmp(path, name) == 0) /* We found the requested file. */ return copy_to_stream(a, stream); } } /* Extact the paths of files contained in an open archive, writing data to an * open stream. Returns 0 on success or <0 on error. */ static int extract_paths_to_stream(struct archive *a, FILE * stream) { struct archive_entry *entry; int r; const char *path; const struct stat *entry_stat; int eof; while (1) { entry = read_header(a, &eof); if (eof) return 0; if (!entry) return -1; path = archive_entry_pathname(entry); entry_stat = archive_entry_stat(entry); if (S_ISLNK(entry_stat->st_mode)) { r = fprintf(stream, "%s\t%#03o\t%s\n", path, (unsigned int)entry_stat->st_mode, archive_entry_symlink(entry)); } else { r = fprintf(stream, "%s\t%#03o\n", path, (unsigned int)entry_stat->st_mode); } if (r <= 0) { opkg_msg(ERROR, "Failed to path to stream: %s\n", strerror(errno)); return -1; } } } static struct archive *open_disk(int flags) { struct archive *disk; int r; disk = archive_write_disk_new(); if (!disk) { opkg_msg(ERROR, "Failed to create disk archive object.\n"); return NULL; } r = archive_write_disk_set_options(disk, flags); if (r == ARCHIVE_WARN) opkg_msg(NOTICE, "Warning when setting disk options: %s (errno=%d)\n", archive_error_string(disk), archive_errno(disk)); else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to set disk options: %s (errno=%d)\n", archive_error_string(disk), archive_errno(disk)); goto err_cleanup; } r = archive_write_disk_set_standard_lookup(disk); if (r == ARCHIVE_WARN) opkg_msg(NOTICE, "Warning when setting user/group lookup functions: %s (errno=%d)\n", archive_error_string(disk), archive_errno(disk)); else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to set user/group lookup functions: %s (errno=%d)\n", archive_error_string(disk), archive_errno(disk)); goto err_cleanup; } return disk; err_cleanup: archive_write_free(disk); return NULL; } /* Print destination paths at DEBUG level. */ static void print_paths(struct archive_entry *entry) { const char *path = archive_entry_pathname(entry); const char *hardlink = archive_entry_hardlink(entry); const char *symlink = archive_entry_symlink(entry); if (hardlink) opkg_msg(DEBUG, "Extracting '%s', hardlink to '%s'.\n", path, hardlink); else if (symlink) opkg_msg(DEBUG, "Extracting '%s', symlink to '%s'.\n", path, symlink); else opkg_msg(DEBUG, "Extracting '%s'.\n", path); } /* Extract an entry to disk, returns 0 on success or <0 on error. */ static int extract_entry(struct archive *a, struct archive_entry *entry, struct archive *disk) { int r; int retries = 0; retry: r = archive_read_extract2(a, entry, disk); switch (r) { case ARCHIVE_OK: break; case ARCHIVE_WARN: opkg_msg(NOTICE, "Warning when extracting archive entry '%s': %s (errno=%d)\n", archive_entry_pathname(entry), archive_error_string(a), archive_errno(a)); break; case ARCHIVE_RETRY: opkg_msg(ERROR, "Failed to extract archive entry '%s': %s (errno=%d)\n", archive_entry_pathname(entry), archive_error_string(a), archive_errno(a)); if (retries++ < 3) { opkg_msg(NOTICE, "Retrying...\n"); goto retry; } else return -1; default: opkg_msg(ERROR, "Failed to extract archive entry '%s': %s (errno=%d)\n", archive_entry_pathname(entry), archive_error_string(a), archive_errno(a)); return -1; } return 0; } /* Extract all files in an archive to the filesystem under the path given by * dest. Returns 0 on success or <0 on error. */ static int extract_all(struct archive *a, const char *dest, int flags, long unsigned int *size) { struct archive *disk; struct archive_entry *entry; int r; int eof; disk = open_disk(flags); if (!disk) return -1; while (1) { entry = read_header(a, &eof); if (eof) break; if (!entry) { r = -1; goto err_cleanup; } r = transform_all_paths(entry, dest); if (r == 1) continue; if (r < 0) { opkg_msg(ERROR, "Failed to transform path.\n"); goto err_cleanup; } print_paths(entry); r = extract_entry(a, entry, disk); if (r < 0) goto err_cleanup; else if (size) *size += archive_entry_size(entry); } r = ARCHIVE_OK; err_cleanup: archive_write_free(disk); return (r == ARCHIVE_OK) ? 0 : -1; } /* Open an outer archive with the given filename. */ static struct archive *open_outer(const char *filename) { struct archive *outer; int r; outer = archive_read_new(); if (!outer) { opkg_msg(ERROR, "Failed to create outer archive object.\n"); return NULL; } /* Outer package is in 'ar' format, uncompressed. */ r = archive_read_support_format_ar(outer); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Ar format not supported: %s (errno=%d)\n", archive_error_string(outer), archive_errno(outer)); goto err_cleanup; } r = archive_read_support_filter_gzip(outer); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Gzip support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Gzip format not supported.\n"); goto err_cleanup; } r = archive_read_support_format_tar(outer); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Tar format not supported: %s (errno=%d)\n", archive_error_string(outer), archive_errno(outer)); goto err_cleanup; } r = archive_read_open_filename(outer, filename, EXTRACT_BUFFER_LEN); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to open package '%s': %s (errno=%d)\n", filename, archive_error_string(outer), archive_errno(outer)); goto err_cleanup; } return outer; err_cleanup: archive_read_free(outer); return NULL; } /* Open an inner archive at the current position within the given outer archive. */ static struct archive *open_inner(struct archive *outer) { struct archive *inner; struct inner_data *data; int r; inner = archive_read_new(); if (!inner) { opkg_msg(ERROR, "Failed to create inner archive object.\n"); return NULL; } data = (struct inner_data *)xmalloc(sizeof(struct inner_data)); data->buffer = xmalloc(EXTRACT_BUFFER_LEN); data->outer = outer; /* Inner package is in 'tar' format, gzip compressed. */ r = archive_read_support_filter_gzip(inner); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Gzip support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Gzip format not supported.\n"); goto err_cleanup; } #if HAVE_XZ r = archive_read_support_filter_xz(inner); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Xz support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Xz format not supported.\n"); goto err_cleanup; } #endif #if HAVE_BZIP2 r = archive_read_support_filter_bzip2(inner); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Bzip2 support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Bzip2 format not supported.\n"); goto err_cleanup; } #endif #if HAVE_LZ4 r = archive_read_support_filter_lz4(inner); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Lz4 support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Lz4 format not supported.\n"); goto err_cleanup; } #endif #if HAVE_ZSTD r = archive_read_support_filter_zstd(inner); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Zstandard support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Zstandard format not supported.\n"); goto err_cleanup; } #endif r = archive_read_support_format_tar(inner); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Tar format not supported: %s (errno=%d)\n", archive_error_string(outer), archive_errno(outer)); goto err_cleanup; } r = archive_read_support_format_empty(inner); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Empty format not supported: %s (errno=%d)\n", archive_error_string(inner), archive_errno(inner)); goto err_cleanup; } r = archive_read_open(inner, data, NULL, inner_read, inner_close); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to open inner archive: %s (errno=%d)\n", archive_error_string(inner), archive_errno(inner)); return NULL; } return inner; err_cleanup: archive_read_free(inner); free(data->buffer); free(data); return NULL; } /* Locate an inner archive with the given name in the given outer archive. * Returns 0 if the item was found, <0 otherwise. */ static int find_inner(struct archive *outer, const char *arname) { const char *path; struct archive_entry *entry; while (1) { entry = read_header(outer, NULL); if (!entry) return -1; /* Cleanup the path of the entry incase it starts with './' or * other prefixes. * * We ignore the return value of transform_dest_path as bad * paths simply won't match the requested path name. */ transform_dest_path(entry, NULL); path = archive_entry_pathname(entry); if (strcmp(path, arname) == 0) { /* We found the requested file. */ return 0; } } } /* Prepare to extract 'control.tar.gz' or 'data.tar.gz' from the outer package * archive, returning a `struct archive *` for the enclosed file. On error, * return NULL. */ static struct archive *extract_outer(const char *filename, const char *arname) { int r; struct archive *inner; struct archive *outer; outer = open_outer(filename); if (!outer) return NULL; r = find_inner(outer, arname); if (r < 0) goto err_cleanup; inner = open_inner(outer); if (!inner) return NULL; return inner; err_cleanup: archive_read_free(outer); return NULL; } static struct archive *open_compressed_file(const char *filename) { struct archive *ar; int r; ar = archive_read_new(); if (!ar) { opkg_msg(ERROR, "Failed to create archive object for compressed file.\n"); return NULL; } /* Support raw data in gzip compression format. */ r = archive_read_support_filter_gzip(ar); if (r == ARCHIVE_WARN) { /* libarchive returns ARCHIVE_WARN if the filter is provided by * an external program. */ opkg_msg(INFO, "Gzip support provided by external program.\n"); } else if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Gzip format not supported: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); goto err_cleanup; } r = archive_read_support_format_raw(ar); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Raw format not supported: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); goto err_cleanup; } r = archive_read_support_format_empty(ar); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Empty format not supported: %s (errno=%d)\n", archive_error_string(ar), archive_errno(ar)); goto err_cleanup; } /* Open input file and prepare for reading. */ r = archive_read_open_filename(ar, filename, EXTRACT_BUFFER_LEN); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to open compressed file '%s': %s (errno=%d)\n", filename, archive_error_string(ar), archive_errno(ar)); goto err_cleanup; } return ar; err_cleanup: archive_read_free(ar); return NULL; } int gz_write_archive(const char *filename, const char *gz_filename) { struct archive *a; struct archive_entry *entry; char buff[8192]; int len; FILE *fd; int r = 0; a = archive_write_new(); archive_write_add_filter_gzip(a); archive_write_set_format_raw(a); archive_write_open_filename(a, gz_filename); struct archive *disk = archive_read_disk_new(); r = archive_read_disk_open(disk, filename); entry = archive_entry_new(); r = archive_read_next_header2(disk, entry); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to read file: '%s' : %s (errno=%d)", filename, archive_error_string(disk), archive_errno(disk)); goto cleanup; } /* Remove path hierarchy, as we are only compressing a single file */ char* tmp = xstrdup(filename); archive_entry_set_pathname(entry, basename(tmp)); free(tmp); r = archive_write_header(a, entry); if (r != ARCHIVE_OK) { opkg_msg(ERROR, "Failed to create compressed file: '%s' : %s (errno=%d)", gz_filename, archive_error_string(a), archive_errno(a)); goto cleanup; } fd = fopen(filename, "r"); len = fread(buff, 1, sizeof(buff), fd); while (len > 0) { archive_write_data(a, buff, len); len = fread(buff, 1, sizeof(buff), fd); } fclose(fd); archive_entry_free(entry); cleanup: archive_read_close(disk); archive_read_free(disk); archive_write_close(a); archive_write_free(a); return r; } /******************************************************************************* * Glue layer. */ struct opkg_ar *ar_open_pkg_control_archive(const char *filename) { struct opkg_ar *ar; ar = (struct opkg_ar *)xmalloc(sizeof(struct opkg_ar)); ar->ar = extract_outer(filename, "control.tar.gz"); #if HAVE_XZ if (!ar->ar) { ar->ar = extract_outer(filename, "control.tar.xz"); } #endif #if HAVE_BZIP2 if (!ar->ar) { ar->ar = extract_outer(filename, "control.tar.bz2"); } #endif #if HAVE_LZ4 if (!ar->ar) { ar->ar = extract_outer(filename, "control.tar.lz4"); } #endif #if HAVE_ZSTD if (!ar->ar) { ar->ar = extract_outer(filename, "control.tar.zst"); } #endif if (!ar->ar) { free(ar); return NULL; } /* We don't want to set ownership and permissions of control files * incase they're incorrect in the archive. They should instead be owned * by the user who ran opkg (usually root) and given permissions * according to the current umask. */ ar->extract_flags = 0; return ar; } struct opkg_ar *ar_open_pkg_data_archive(const char *filename) { struct opkg_ar *ar; ar = (struct opkg_ar *)xmalloc(sizeof(struct opkg_ar)); ar->ar = extract_outer(filename, "data.tar.gz"); #if HAVE_XZ if (!ar->ar) { ar->ar = extract_outer(filename, "data.tar.xz"); } #endif #if HAVE_BZIP2 if (!ar->ar) { ar->ar = extract_outer(filename, "data.tar.bz2"); } #endif #if HAVE_LZ4 if (!ar->ar) { ar->ar = extract_outer(filename, "data.tar.lz4"); } #endif #if HAVE_ZSTD if (!ar->ar) { ar->ar = extract_outer(filename, "data.tar.zst"); } #endif if (!ar->ar) { free(ar); return NULL; } /** Flags: * * TODO: Do we want to support ACLs, extended flags and extended * attributes? (ARCHIVE_EXTRACT_ACL, ARCHIVE_EXTRACT_FFLAGS, * ARCHIVE_EXTRACT_XATTR). */ ar->extract_flags = ARCHIVE_EXTRACT_OWNER | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_UNLINK | ARCHIVE_EXTRACT_NO_OVERWRITE; #ifdef ENABLE_ACL ar->extract_flags |= ARCHIVE_EXTRACT_ACL; #endif #ifdef ENABLE_XATTR ar->extract_flags |= ARCHIVE_EXTRACT_FFLAGS | ARCHIVE_EXTRACT_XATTR; #endif if (opkg_config->ignore_uid) ar->extract_flags &= ~ARCHIVE_EXTRACT_OWNER; return ar; } struct opkg_ar *ar_open_compressed_file(const char *filename) { struct opkg_ar *ar; struct archive_entry *entry; int eof; ar = (struct opkg_ar *)xmalloc(sizeof(struct opkg_ar)); ar->ar = open_compressed_file(filename); if (!ar->ar) goto err_cleanup; /* Flags aren't used when handling compressed files. */ ar->extract_flags = 0; /* As ar represents a single compressed file it should just have one * header. We skip over this header here so that the caller doesn't need * to know about it. */ entry = read_header(ar->ar, &eof); if (!entry && !eof) goto err_cleanup; return ar; err_cleanup: if (ar->ar) archive_read_free(ar->ar); free(ar); return NULL; } int ar_copy_to_stream(struct opkg_ar *ar, FILE * stream) { return copy_to_stream(ar->ar, stream); } int ar_extract_file_to_stream(struct opkg_ar *ar, const char *filename, FILE * stream) { return extract_file_to_stream(ar->ar, filename, stream); } int ar_extract_paths_to_stream(struct opkg_ar *ar, FILE * stream) { return extract_paths_to_stream(ar->ar, stream); } int ar_extract_all(struct opkg_ar *ar, const char *prefix, long unsigned int *size) { return extract_all(ar->ar, prefix, ar->extract_flags, size); } void ar_close(struct opkg_ar *ar) { archive_read_free(ar->ar); free(ar); }