Files
LisherSong--ps5-web-file-ma…/src/zip_extract.c
T
Songlx516 021c9cb339 feat(7z): extraction facade + format dispatch + UTF-8 host shim
The 7z chain decoder (commit 2748b38) handles bytes; what the /api/extract
task needed was a third sibling of zip_extract.c and rar_extract.c that
runs that chain over a real archive, mirrors their staging / publish /
rollback / name-validation machinery, and fills in zipx_result_t the same
way so the dispatcher and the web UI can treat every format alike.

* src/sevenz_extract.[ch] -- one pass over the archive (scan: validate every
  name, sum bytes, drop archives the chain cannot drive), then folder by
  folder through the chain.  The publish phase recurses with OVERWRITE and
  MERGE alike for matching directories, and only differs at the file
  leaves -- a strict non-recursive OVERWRITE would have made the policy
  useless for any archive that overlaps a directory already on disk.
* src/zipx_common.c -- the limits profiles and zipx_status_string() that
  the three engines share.  Pulled out of zip_extract.c so every consumer
  gets the same error text and the same MAX_* numbers.
* src/zip_extract.[ch] -- ZIPX_ERR_PASSWORD joined the contract (7zAES).
* tests/run-sevenz-tests.sh -- the engine matrix now also runs the facade
  over every fixture, plus 22 error and policy checks against
  test_sevenz_extract.  KNOWN_GAPS holds only "aeshe" (encrypted header);
  the plain 7zAES fixture is in the matrix.
* tests/test_sevenz_extract.c -- end-to-end driver for the facade.

The POSIX shim the host test runner injects needed three more pieces so
the tests pass on a CP936 host:

* lstat/stat -> wfm_stat (the MinGW ANSI entry points can't see a UTF-8
  filename in CP936; without the redirect a non-ASCII entry causes the
  publish phase to lstat() a mangled path and fail with ENOENT).
* opendir/readdir/closedir -> the wide variants, so readdir() hands us
  real UTF-8 names instead of CP936 bytes that nothing can round-trip.
* fopen -> _wfopen, for the test helpers that read a non-ASCII file
  back to verify it.

ZIP 108 checks / RAR 27 checks / 7z 26 checks, all green; the AES
fixture extracts with the password "Secret123" and rejects wrong / absent
passwords with "password required or wrong" / "wrong password, or the
archive is damaged".

tests/fixtures/ gains the volume-set fixtures (broken.zip.001, gap.zip.*,
disks.*, parts.part*.zip, plain.zip.*, split_single.zip) that were
generated by tests/make_split_fixtures.py in earlier sessions but never
made it into the index.

Next commit wires sevenz_extract() into the dispatch in src/extract.c and
recognises 7z archives in assets/main.js.
2026-09-13 09:52:44 +08:00

1396 lines
38 KiB
C

/* Safe ZIP extraction engine used by the /api/extract task.
POSIX only; see tests/ for the host harness that exercises it. */
#include "zip_extract.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "mz.h"
#include "mz_zip.h"
#include "mz_strm.h"
#include "mz_strm_os.h"
#include "zipx_volume.h"
#include "zipx_volstream.h"
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
#define ZIPX_IO_BUFFER (128 * 1024)
#define ZIPX_TMP_ATTEMPTS 64
#define ZIPX_STAGING_PREFIX ".wfm-extract-"
#define ZIPX_PART_PREFIX ".wfm-part-"
#define ZIPX_PUBLISH_MAX_DEPTH 128
#define ZIPX_SPACE_SLACK_PER_ENTRY 512
/* The limit profiles and zipx_status_string() are format independent and live
in src/zipx_common.c, which every engine links. */
/**************************************************************************
* small helpers
**************************************************************************/
typedef struct {
zipx_conflict_t conflict;
zipx_limits_t limits;
zipx_cancel_fn cancel;
zipx_progress_fn progress;
void *userdata;
zipx_result_t *result;
uint64_t entries_total;
uint64_t entries_done;
uint64_t bytes_total;
uint64_t bytes_done;
uint64_t files_created;
uint64_t dirs_created;
uint64_t progress_floor;
struct timespec last_report;
char staging[ZIPX_PATH_MAX];
char **created; /* published destination paths, oldest first */
size_t created_count;
size_t created_cap;
unsigned char *created_is_dir;
int staging_created;
unsigned int part_counter;
} zipx_ctx_t;
static void
ctx_set_detail(zipx_ctx_t *c, const char *path) {
if(path) {
snprintf(c->result->detail, sizeof(c->result->detail), "%s", path);
}
}
static int
ctx_fail(zipx_ctx_t *c, zipx_status_t status, const char *detail,
const char *fmt, ...) {
va_list ap;
c->result->status = status;
c->result->sys_errno = errno;
ctx_set_detail(c, detail);
va_start(ap, fmt);
if(fmt) {
vsnprintf(c->result->message, sizeof(c->result->message), fmt, ap);
}
va_end(ap);
if(!c->result->message[0]) {
snprintf(c->result->message, sizeof(c->result->message), "%s",
zipx_status_string(status));
}
/* Every error code is non-zero, so callers can test the return value. */
return (int)status;
}
static long long
mono_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void
report(zipx_ctx_t *c, int phase, const char *current, int force) {
zipx_progress_t p;
long long now;
if(!c->progress) {
return;
}
now = mono_ms();
if(!force && c->last_report.tv_sec &&
now - ((long long)c->last_report.tv_sec * 1000 +
c->last_report.tv_nsec / 1000000) < 200 &&
c->bytes_done - c->progress_floor < (1024 * 1024)) {
return;
}
clock_gettime(CLOCK_MONOTONIC, &c->last_report);
c->progress_floor = c->bytes_done;
p.phase = phase;
p.entries_total = c->entries_total;
p.entries_done = c->entries_done;
p.bytes_total = c->bytes_total;
p.bytes_done = c->bytes_done;
p.current = current;
c->progress(c->userdata, &p);
}
static int
canceled(zipx_ctx_t *c) {
return c->cancel && c->cancel(c->userdata);
}
static int
remember_published(zipx_ctx_t *c, const char *path, int is_dir) {
if(c->created_count == c->created_cap) {
size_t cap = c->created_cap ? c->created_cap * 2 : 64;
char **paths = realloc(c->created, cap * sizeof(*paths));
unsigned char *flags = realloc(c->created_is_dir, cap * sizeof(*flags));
if(!paths || !flags) {
free(paths);
free(flags);
return -1;
}
c->created = paths;
c->created_is_dir = flags;
c->created_cap = cap;
}
if(!(c->created[c->created_count] = strdup(path))) {
return -1;
}
c->created_is_dir[c->created_count] = is_dir ? 1 : 0;
c->created_count++;
return 0;
}
static void
free_published(zipx_ctx_t *c) {
size_t i;
for(i = 0; i < c->created_count; i++) {
free(c->created[i]);
}
free(c->created);
free(c->created_is_dir);
c->created = NULL;
c->created_is_dir = NULL;
c->created_count = c->created_cap = 0;
}
/**************************************************************************
* duplicate / file-vs-directory clash detection
**************************************************************************/
typedef struct {
uint64_t *hash;
unsigned char *is_dir;
size_t cap; /* power of two */
size_t count;
} zipx_nameset_t;
static uint64_t
fnv1a(const char *s, size_t len) {
uint64_t h = 1469598103934665603ULL;
size_t i;
for(i = 0; i < len; i++) {
h ^= (unsigned char)s[i];
h *= 1099511628211ULL;
}
return h ? h : 1; /* 0 is the empty slot marker */
}
static int
nameset_init(zipx_nameset_t *set, size_t hint) {
size_t cap = 1024;
while(cap < hint * 2) {
cap *= 2;
}
set->hash = calloc(cap, sizeof(*set->hash));
set->is_dir = calloc(cap, sizeof(*set->is_dir));
if(!set->hash || !set->is_dir) {
free(set->hash);
free(set->is_dir);
set->hash = NULL;
set->is_dir = NULL;
return -1;
}
set->cap = cap;
set->count = 0;
return 0;
}
static void
nameset_free(zipx_nameset_t *set) {
free(set->hash);
free(set->is_dir);
set->hash = NULL;
set->is_dir = NULL;
set->cap = set->count = 0;
}
static size_t
nameset_slot(zipx_nameset_t *set, uint64_t h) {
size_t mask = set->cap - 1;
size_t i = (size_t)(h & mask);
while(set->hash[i] && set->hash[i] != h) {
i = (i + 1) & mask;
}
return i;
}
static int
nameset_grow(zipx_nameset_t *set) {
zipx_nameset_t bigger;
size_t i;
if(nameset_init(&bigger, set->cap) ) {
return -1;
}
for(i = 0; i < set->cap; i++) {
if(!set->hash[i]) {
continue;
}
bigger.hash[nameset_slot(&bigger, set->hash[i])] = set->hash[i];
bigger.is_dir[nameset_slot(&bigger, set->hash[i])] = set->is_dir[i];
bigger.count++;
}
nameset_free(set);
*set = bigger;
return 0;
}
/* 0 = not present, 1 = present as directory, 2 = present as file */
static int
nameset_get(zipx_nameset_t *set, uint64_t h) {
size_t i = nameset_slot(set, h);
if(!set->hash[i]) {
return 0;
}
return set->is_dir[i] ? 1 : 2;
}
static int
nameset_put(zipx_nameset_t *set, uint64_t h, int is_dir) {
size_t i;
if(set->count * 4 >= set->cap * 3 && nameset_grow(set)) {
return -1;
}
i = nameset_slot(set, h);
if(!set->hash[i]) {
set->hash[i] = h;
set->count++;
}
set->is_dir[i] = is_dir ? 1 : 0;
return 0;
}
/**************************************************************************
* entry name validation
**************************************************************************/
/* Normalizes a ZIP entry name into out using '/' separators.
Returns 0 on success and sets *is_dir / *depth. */
static int
normalize_name(const char *in, char *out, size_t out_size, int *is_dir,
uint32_t *depth, zipx_ctx_t *c) {
size_t in_len = strlen(in);
size_t out_len = 0;
size_t i = 0;
size_t seg_len = 0;
uint32_t levels = 0;
*is_dir = 0;
*depth = 0;
if(!in_len) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "empty entry name");
}
if(in_len > c->limits.max_path_len) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, in, "entry name is too long");
}
/* Absolute paths, drive letters and UNC prefixes are never accepted. */
if(in[0] == '/' || in[0] == '\\') {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "absolute entry name");
}
if(in_len >= 2 && ((in[0] >= 'A' && in[0] <= 'Z') ||
(in[0] >= 'a' && in[0] <= 'z')) && in[1] == ':') {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "drive letter in entry name");
}
while(i < in_len) {
char ch = in[i];
if((unsigned char)ch < 0x20 || (unsigned char)ch == 0x7f) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in,
"control character in entry name");
}
if(ch == '/' || ch == '\\') {
if(!seg_len) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "empty path segment");
}
if(seg_len == 1 && out[out_len - 1] == '.') {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "'.' path segment");
}
if(seg_len == 2 && !memcmp(out + out_len - 2, "..", 2)) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "'..' path segment");
}
if(seg_len > c->limits.max_name_len) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, in, "path component is too long");
}
out[out_len++] = '/';
levels++;
seg_len = 0;
i++;
continue;
}
if(out_len + 2 >= out_size) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, in, "entry name is too long");
}
out[out_len++] = ch;
seg_len++;
i++;
}
if(seg_len) {
if(seg_len == 1 && out[out_len - 1] == '.') {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "'.' path segment");
}
if(seg_len == 2 && !memcmp(out + out_len - 2, "..", 2)) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "'..' path segment");
}
if(seg_len > c->limits.max_name_len) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, in, "path component is too long");
}
levels++;
} else {
*is_dir = 1; /* trailing separator */
}
out[out_len] = 0;
if(!levels) {
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "empty entry name");
}
if(levels > c->limits.max_depth) {
return ctx_fail(c, ZIPX_ERR_LIMIT_DEPTH, in, "entry path is too deep");
}
*depth = levels;
return 0;
}
/* Registers every ancestor of name as a directory and rejects clashes. */
static int
nameset_add_path(zipx_nameset_t *set, const char *name, int is_dir,
zipx_ctx_t *c) {
char buf[ZIPX_PATH_MAX];
size_t len = strlen(name);
size_t i;
if(len >= sizeof(buf)) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, name, "entry name is too long");
}
memcpy(buf, name, len + 1);
for(i = 0; i < len; i++) {
if(buf[i] != '/') {
continue;
}
buf[i] = 0;
if(nameset_get(set, fnv1a(buf, i)) == 2) {
return ctx_fail(c, ZIPX_ERR_DUPLICATE, name,
"entry uses a file as a directory");
}
if(nameset_put(set, fnv1a(buf, i), 1)) {
return ctx_fail(c, ZIPX_ERR_INTERNAL, name, "out of memory");
}
buf[i] = '/';
}
if(nameset_put(set, fnv1a(name, len), is_dir)) {
return ctx_fail(c, ZIPX_ERR_INTERNAL, name, "out of memory");
}
return 0;
}
/* Duplicate detection: a repeated file is rejected, a repeated directory is
harmless. A name used both as a file and as a directory is rejected. */
static int
nameset_check_duplicate(zipx_nameset_t *set, const char *name, int is_dir,
zipx_ctx_t *c) {
int existing = nameset_get(set, fnv1a(name, strlen(name)));
if(existing == 2 || (existing == 1 && !is_dir)) {
return ctx_fail(c, ZIPX_ERR_DUPLICATE, name, "duplicate entry name");
}
return 0;
}
/**************************************************************************
* staging helpers
**************************************************************************/
static int
path_parent(const char *path, char *out, size_t out_size) {
size_t len = strlen(path);
size_t i;
if(!len) {
return -1;
}
i = len;
while(i && path[i - 1] == '/') {
i--;
}
while(i && path[i - 1] != '/') {
i--;
}
while(i > 1 && path[i - 1] == '/') {
i--;
}
if(!i) {
return -1; /* relative path without a directory part */
}
if(i >= out_size) {
return -1;
}
memcpy(out, path, i);
out[i] = 0;
return 0;
}
static void
chmod_0777_fd(int fd) {
int err = errno;
/* FAT/exFAT style filesystems reject fchmod; that is not a failure. */
if(fchmod(fd, 0777)) {
errno = err;
}
}
static int
make_staging(zipx_ctx_t *c, const char *parent) {
unsigned int attempt;
for(attempt = 0; attempt < ZIPX_TMP_ATTEMPTS; attempt++) {
int n = snprintf(c->staging, sizeof(c->staging), "%s/%s%ld-%lld-%u.tmp",
parent, ZIPX_STAGING_PREFIX, (long)getpid(),
(long long)time(NULL), attempt);
if(n < 0 || (size_t)n >= sizeof(c->staging)) {
return ctx_fail(c, ZIPX_ERR_INTERNAL, NULL, "staging path is too long");
}
if(!mkdir(c->staging, 0777)) {
c->staging_created = 1;
return 0;
}
if(errno != EEXIST) {
return ctx_fail(c, ZIPX_ERR_IO, c->staging, "cannot create staging: %s",
strerror(errno));
}
}
return ctx_fail(c, ZIPX_ERR_IO, parent, "cannot create staging directory");
}
static void
remove_tree(const char *path) {
DIR *dir = opendir(path);
struct dirent *ent;
if(!dir) {
rmdir(path);
return;
}
while((ent = readdir(dir))) {
char child[ZIPX_PATH_MAX];
struct stat st;
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
continue;
}
if(snprintf(child, sizeof(child), "%s/%s", path, ent->d_name) >=
(int)sizeof(child)) {
continue;
}
if(!lstat(child, &st) && S_ISDIR(st.st_mode)) {
remove_tree(child);
} else {
unlink(child);
}
}
closedir(dir);
rmdir(path);
}
static void
cleanup_staging(zipx_ctx_t *c) {
if(c->staging_created) {
remove_tree(c->staging);
c->staging_created = 0;
}
}
/* Undo the objects this task already published, newest first. */
static void
rollback_published(zipx_ctx_t *c) {
size_t i = c->created_count;
while(i) {
i--;
if(c->created_is_dir[i]) {
rmdir(c->created[i]);
} else {
unlink(c->created[i]);
}
}
}
/**************************************************************************
* scan phase
**************************************************************************/
static int
check_space(zipx_ctx_t *c, const char *target) {
struct statvfs vfs;
unsigned long long available;
unsigned long long block;
unsigned long long required = c->bytes_total +
c->entries_total * ZIPX_SPACE_SLACK_PER_ENTRY;
if(statvfs(target, &vfs)) {
return ctx_fail(c, ZIPX_ERR_SPACE, target, "cannot read free space: %s",
strerror(errno));
}
block = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
available = (unsigned long long)vfs.f_bavail * block;
if(available < required) {
return ctx_fail(c, ZIPX_ERR_SPACE, target,
"not enough space, required %llu bytes, available %llu bytes",
required, available);
}
return 0;
}
static int
scan_archive(void *zip, zipx_ctx_t *c) {
zipx_nameset_t set;
int32_t err;
int ret = 0;
if(nameset_init(&set, 4096)) {
return ctx_fail(c, ZIPX_ERR_INTERNAL, NULL, "out of memory");
}
err = mz_zip_goto_first_entry(zip);
while(err == MZ_OK) {
mz_zip_file *info = NULL;
char name[ZIPX_PATH_MAX];
int is_dir = 0;
uint32_t depth = 0;
uint64_t uncompressed;
if(mz_zip_entry_get_info(zip, &info) || !info || !info->filename) {
ret = ctx_fail(c, ZIPX_ERR_FORMAT, NULL, "cannot read entry header");
break;
}
if(strlen(info->filename) != info->filename_size) {
ret = ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, NULL,
"entry name contains a NUL byte");
break;
}
ret = normalize_name(info->filename, name, sizeof(name), &is_dir, &depth,
c);
(void)depth;
if(ret) {
break;
}
if((info->flag & MZ_ZIP_FLAG_ENCRYPTED) || info->aes_version) {
ret = ctx_fail(c, ZIPX_ERR_UNSUPPORTED, name, "encrypted entry");
break;
}
if(info->compression_method != MZ_COMPRESS_METHOD_STORE &&
info->compression_method != MZ_COMPRESS_METHOD_DEFLATE) {
ret = ctx_fail(c, ZIPX_ERR_UNSUPPORTED, name,
"unsupported compression method %d",
(int)info->compression_method);
break;
}
if(mz_zip_attrib_is_symlink(info->external_fa,
info->version_madeby) == MZ_OK) {
ret = ctx_fail(c, ZIPX_ERR_SPECIAL, name, "symbolic link entry");
break;
}
is_dir = is_dir || (mz_zip_attrib_is_dir(info->external_fa,
info->version_madeby) == MZ_OK);
if(!is_dir && MZ_HOST_SYSTEM(info->version_madeby) == MZ_HOST_SYSTEM_UNIX) {
mode_t mode = (mode_t)(info->external_fa >> 16);
if(mode && (mode & S_IFMT) != S_IFREG) {
ret = ctx_fail(c, ZIPX_ERR_SPECIAL, name, "special file entry");
break;
}
}
if(nameset_check_duplicate(&set, name, is_dir, c) ||
nameset_add_path(&set, name, is_dir, c)) {
ret = -1;
break;
}
uncompressed = info->uncompressed_size > 0 ?
(uint64_t)info->uncompressed_size : 0;
if(!is_dir) {
if(uncompressed > c->limits.max_file_bytes) {
ret = ctx_fail(c, ZIPX_ERR_LIMIT_FILE, name,
"entry is larger than %llu bytes",
(unsigned long long)c->limits.max_file_bytes);
break;
}
if(c->limits.max_ratio && info->compressed_size > 0 &&
uncompressed >= c->limits.ratio_min_bytes &&
uncompressed > (uint64_t)info->compressed_size *
c->limits.max_ratio) {
ret = ctx_fail(c, ZIPX_ERR_LIMIT_RATIO, name,
"compression ratio is above %u (%llu -> %llu bytes)",
c->limits.max_ratio,
(unsigned long long)info->compressed_size,
(unsigned long long)uncompressed);
break;
}
if(uncompressed >= c->limits.max_total_bytes ||
c->bytes_total > c->limits.max_total_bytes - uncompressed) {
ret = ctx_fail(c, ZIPX_ERR_LIMIT_TOTAL, name,
"archive contents are larger than %llu bytes",
(unsigned long long)c->limits.max_total_bytes);
break;
}
c->bytes_total += uncompressed;
}
c->entries_total++;
if(c->entries_total > c->limits.max_entries) {
ret = ctx_fail(c, ZIPX_ERR_LIMIT_ENTRIES, name,
"archive has more than %llu entries",
(unsigned long long)c->limits.max_entries);
break;
}
if(c->entries_total % 4096 == 0) {
if(canceled(c)) {
ret = ctx_fail(c, ZIPX_ERR_CANCELED, name, NULL);
break;
}
report(c, ZIPX_PHASE_SCAN, name, 0);
}
err = mz_zip_goto_next_entry(zip);
}
if(!ret && err != MZ_END_OF_LIST && err != MZ_OK) {
ret = ctx_fail(c, ZIPX_ERR_FORMAT, NULL, "corrupt central directory");
}
nameset_free(&set);
return ret;
}
/**************************************************************************
* extract phase
**************************************************************************/
/* Builds "<staging>/<rel>[/<leaf>]" for the plain-call fallbacks below.
rel may be empty (staging root); leaf may be NULL. */
static void
staging_path(const zipx_ctx_t *c, const char *rel, const char *leaf,
char *out, size_t cap) {
if(rel[0] && leaf) {
snprintf(out, cap, "%s/%s/%s", c->staging, rel, leaf);
} else if(rel[0]) {
snprintf(out, cap, "%s/%s", c->staging, rel);
} else if(leaf) {
snprintf(out, cap, "%s/%s", c->staging, leaf);
} else {
snprintf(out, cap, "%s", c->staging);
}
}
/* The *at() family can be present in the target libc yet fail at runtime
without setting errno (observed on PS5 hardware: mkdirat() returns -1
with errno 0, while plain path-based calls work). Every *at() call in
the extract phase therefore falls back to a full-path call built from
the staging root before reporting an I/O error. */
/* Opens (creating when needed) every directory of rel below root_fd.
Returns an open descriptor for the deepest directory. */
static int
open_parent_dirs(int root_fd, const char *rel, zipx_ctx_t *c) {
char buf[ZIPX_PATH_MAX];
char cur[ZIPX_PATH_MAX] = "";
int fd = root_fd;
char *seg;
char *save = NULL;
if(strlen(rel) >= sizeof(buf)) {
ctx_fail(c, ZIPX_ERR_LIMIT_NAME, rel, "path is too long");
return -1;
}
strcpy(buf, rel);
for(seg = strtok_r(buf, "/", &save); seg; seg = strtok_r(NULL, "/", &save)) {
int next;
int made = 0;
if(cur[0]) {
strncat(cur, "/", sizeof(cur) - strlen(cur) - 1);
}
strncat(cur, seg, sizeof(cur) - strlen(cur) - 1);
if(!mkdirat(fd, seg, 0777)) {
made = 1;
} else if(errno != EEXIST) {
char full[ZIPX_PATH_MAX];
staging_path(c, cur, NULL, full, sizeof(full));
if(mkdir(full, 0777) && errno != EEXIST) {
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot create directory '%s': %s "
"(errno=%d)", seg, strerror(errno), errno);
if(fd != root_fd) {
close(fd);
}
return -1;
}
made = 1;
}
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if(next < 0) {
char full[ZIPX_PATH_MAX];
staging_path(c, cur, NULL, full, sizeof(full));
next = open(full, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if(next < 0) {
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot open directory '%s': %s "
"(errno=%d)", seg, strerror(errno), errno);
if(fd != root_fd) {
close(fd);
}
return -1;
}
}
if(made) {
chmod_0777_fd(next);
c->dirs_created++;
}
if(fd != root_fd) {
close(fd);
}
fd = next;
}
return fd;
}
static int
write_entry(void *zip, zipx_ctx_t *c, int root_fd, const char *name,
uint64_t declared) {
char dir_part[ZIPX_PATH_MAX];
char base[ZIPX_PATH_MAX];
char tmp[ZIPX_PATH_MAX];
uint64_t remaining = declared ? declared : c->limits.max_file_bytes;
uint64_t written = 0;
char *buf = malloc(ZIPX_IO_BUFFER);
char *slash;
int dir_fd;
int fd = -1;
int ret = 0;
if(!buf) {
return ctx_fail(c, ZIPX_ERR_INTERNAL, name, "out of memory");
}
snprintf(dir_part, sizeof(dir_part), "%s", name);
slash = strrchr(dir_part, '/');
if(slash) {
snprintf(base, sizeof(base), "%s", slash + 1);
*slash = 0;
} else {
snprintf(base, sizeof(base), "%s", name);
dir_part[0] = 0;
}
if(!base[0]) {
free(buf);
return ctx_fail(c, ZIPX_ERR_UNSAFE_NAME, name, "empty file name");
}
dir_fd = open_parent_dirs(root_fd, dir_part, c);
if(dir_fd < 0) {
free(buf);
return -1;
}
snprintf(tmp, sizeof(tmp), "%s%u", ZIPX_PART_PREFIX, ++c->part_counter);
fd = openat(dir_fd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
if(fd < 0) {
/* *at() fallback (see open_parent_dirs). */
char full[ZIPX_PATH_MAX];
staging_path(c, dir_part, tmp, full, sizeof(full));
fd = open(full, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
}
if(fd < 0) {
ctx_fail(c, ZIPX_ERR_IO, name, "cannot create file '%s': %s (errno=%d)",
tmp, strerror(errno), errno);
if(dir_fd != root_fd) {
close(dir_fd);
}
free(buf);
return -1;
}
if(mz_zip_entry_read_open(zip, 0, NULL) != MZ_OK) {
ctx_fail(c, ZIPX_ERR_FORMAT, name, "cannot read entry data");
ret = -1;
goto done;
}
while(ret == 0) {
int32_t n = mz_zip_entry_read(zip, buf, (int32_t)ZIPX_IO_BUFFER);
if(n == 0) {
break;
}
if(n < 0) {
ctx_fail(c, n == MZ_CRC_ERROR ? ZIPX_ERR_CRC : ZIPX_ERR_FORMAT, name,
"%s", n == MZ_CRC_ERROR ? "crc mismatch" : "corrupt entry data");
ret = -1;
break;
}
if((uint64_t)n > remaining || written + (uint64_t)n > remaining) {
ctx_fail(c, ZIPX_ERR_LIMIT_FILE, name,
"entry is larger than the declared size");
ret = -1;
break;
}
if(written + (uint64_t)n > c->limits.max_total_bytes - c->bytes_done) {
ctx_fail(c, ZIPX_ERR_LIMIT_TOTAL, name,
"archive contents are larger than %llu bytes",
(unsigned long long)c->limits.max_total_bytes);
ret = -1;
break;
}
while(n > 0) {
ssize_t w = write(fd, buf, (size_t)n);
if(w <= 0) {
ctx_fail(c, ZIPX_ERR_IO, name, "write failed: %s",
strerror(errno ? errno : EIO));
ret = -1;
break;
}
n -= (int32_t)w;
written += (uint64_t)w;
c->bytes_done += (uint64_t)w;
}
if(ret) {
break;
}
if(canceled(c)) {
ctx_fail(c, ZIPX_ERR_CANCELED, name, NULL);
ret = -1;
break;
}
report(c, ZIPX_PHASE_EXTRACT, name, 0);
}
{
uint32_t crc = 0;
int64_t comp = 0;
int64_t uncomp = 0;
int32_t close_err = mz_zip_entry_read_close(zip, &crc, &comp, &uncomp);
if(!ret && close_err != MZ_OK) {
ctx_fail(c, close_err == MZ_CRC_ERROR ? ZIPX_ERR_CRC : ZIPX_ERR_FORMAT,
name, "%s", close_err == MZ_CRC_ERROR ?
"crc mismatch" : "corrupt entry data");
ret = -1;
}
}
done:
if(!ret) {
if(fsync(fd) || close(fd)) {
ctx_fail(c, ZIPX_ERR_IO, name, "cannot flush file: %s", strerror(errno));
ret = -1;
}
fd = -1;
}
if(!ret && renameat(dir_fd, tmp, dir_fd, base)) {
/* *at() fallback (see open_parent_dirs). */
char src_full[ZIPX_PATH_MAX];
char dst_full[ZIPX_PATH_MAX];
staging_path(c, dir_part, tmp, src_full, sizeof(src_full));
staging_path(c, dir_part, base, dst_full, sizeof(dst_full));
if(rename(src_full, dst_full)) {
ctx_fail(c, ZIPX_ERR_IO, name, "cannot move file into place: %s "
"(errno=%d)", strerror(errno), errno);
ret = -1;
}
}
if(fd >= 0) {
close(fd);
}
if(ret) {
if(unlinkat(dir_fd, tmp, 0)) {
/* *at() fallback (see open_parent_dirs). */
char full[ZIPX_PATH_MAX];
staging_path(c, dir_part, tmp, full, sizeof(full));
unlink(full);
}
} else {
c->files_created++;
}
if(dir_fd != root_fd) {
close(dir_fd);
}
free(buf);
return ret;
}
static int
extract_archive(void *zip, zipx_ctx_t *c, int root_fd) {
int32_t err = mz_zip_goto_first_entry(zip);
int ret = 0;
uint64_t index = 0;
while(err == MZ_OK && !ret) {
mz_zip_file *info = NULL;
char name[ZIPX_PATH_MAX];
int is_dir = 0;
uint32_t depth = 0;
if(mz_zip_entry_get_info(zip, &info) || !info || !info->filename) {
return ctx_fail(c, ZIPX_ERR_FORMAT, NULL, "cannot read entry header");
}
if(strlen(info->filename) != info->filename_size ||
normalize_name(info->filename, name, sizeof(name), &is_dir, &depth, c)) {
return (int)c->result->status;
}
(void)depth;
is_dir = is_dir || (mz_zip_attrib_is_dir(info->external_fa,
info->version_madeby) == MZ_OK);
if(canceled(c)) {
ret = ctx_fail(c, ZIPX_ERR_CANCELED, name, NULL);
break;
}
if(is_dir) {
char copy[ZIPX_PATH_MAX];
int dir_fd;
snprintf(copy, sizeof(copy), "%s", name);
dir_fd = open_parent_dirs(root_fd, copy, c);
if(dir_fd < 0) {
ret = -1;
break;
}
if(dir_fd != root_fd) {
close(dir_fd);
}
} else {
uint64_t declared = info->uncompressed_size > 0 ?
(uint64_t)info->uncompressed_size : 0;
if(write_entry(zip, c, root_fd, name, declared)) {
ret = -1;
break;
}
}
c->entries_done = ++index;
report(c, ZIPX_PHASE_EXTRACT, name, 0);
err = mz_zip_goto_next_entry(zip);
}
if(!ret && err != MZ_END_OF_LIST) {
ret = ctx_fail(c, ZIPX_ERR_FORMAT, NULL, "corrupt central directory");
}
return ret;
}
/**************************************************************************
* publish phase
**************************************************************************/
static int publish_dir(const char *src, const char *dst, int depth,
zipx_ctx_t *c);
static int
publish_entry(const char *src, const char *dst, const char *name, int depth,
zipx_ctx_t *c) {
char src_child[ZIPX_PATH_MAX];
char dst_child[ZIPX_PATH_MAX];
struct stat st;
struct stat src_st;
int src_is_dir;
if(snprintf(src_child, sizeof(src_child), "%s/%s", src, name) >=
(int)sizeof(src_child) ||
snprintf(dst_child, sizeof(dst_child), "%s/%s", dst, name) >=
(int)sizeof(dst_child)) {
return ctx_fail(c, ZIPX_ERR_LIMIT_NAME, name, "path is too long");
}
if(lstat(src_child, &src_st)) {
return ctx_fail(c, ZIPX_ERR_IO, src_child, "cannot read staging: %s",
strerror(errno));
}
src_is_dir = S_ISDIR(src_st.st_mode) ? 1 : 0;
if(lstat(dst_child, &st)) {
if(errno != ENOENT) {
return ctx_fail(c, ZIPX_ERR_IO, dst_child, "cannot check target: %s",
strerror(errno));
}
if(rename(src_child, dst_child)) {
return ctx_fail(c, ZIPX_ERR_IO, dst_child, "cannot publish: %s",
strerror(errno));
}
return remember_published(c, dst_child, src_is_dir);
}
if(S_ISDIR(st.st_mode)) {
if(!src_is_dir) {
/* A file in the archive collides with a directory on disk. */
return ctx_fail(c, ZIPX_ERR_CONFLICT, dst_child,
"file collides with an existing directory");
}
if(c->conflict != ZIPX_CONFLICT_MERGE) {
return ctx_fail(c, ZIPX_ERR_CONFLICT, dst_child,
"directory already exists");
}
if(depth >= ZIPX_PUBLISH_MAX_DEPTH) {
return ctx_fail(c, ZIPX_ERR_LIMIT_DEPTH, dst_child, "path is too deep");
}
if(publish_dir(src_child, dst_child, depth + 1, c)) {
return -1;
}
rmdir(src_child);
return 0;
}
if(S_ISREG(st.st_mode)) {
if(!src_is_dir) {
if(c->conflict == ZIPX_CONFLICT_OVERWRITE) {
if(rename(src_child, dst_child)) {
return ctx_fail(c, ZIPX_ERR_IO, dst_child, "cannot replace: %s",
strerror(errno));
}
return remember_published(c, dst_child, 0);
}
if(c->conflict == ZIPX_CONFLICT_MERGE) {
/* Preserve the existing file; drop the staged copy. */
if(unlink(src_child)) {
return ctx_fail(c, ZIPX_ERR_IO, dst_child,
"cannot drop staged file: %s", strerror(errno));
}
return 0;
}
}
return ctx_fail(c, ZIPX_ERR_CONFLICT, dst_child, "target already exists");
}
return ctx_fail(c, ZIPX_ERR_CONFLICT, dst_child, "target already exists");
}
static int
publish_dir(const char *src, const char *dst, int depth, zipx_ctx_t *c) {
DIR *dir = opendir(src);
struct dirent *ent;
char **names = NULL;
size_t count = 0;
size_t cap = 0;
size_t i;
int ret = 0;
if(!dir) {
return ctx_fail(c, ZIPX_ERR_IO, src, "cannot read staging: %s",
strerror(errno));
}
while((ent = readdir(dir))) {
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
continue;
}
if(count == cap) {
size_t next = cap ? cap * 2 : 64;
char **grown = realloc(names, next * sizeof(*grown));
if(!grown) {
ret = ctx_fail(c, ZIPX_ERR_INTERNAL, src, "out of memory");
break;
}
names = grown;
cap = next;
}
if(!(names[count] = strdup(ent->d_name))) {
ret = ctx_fail(c, ZIPX_ERR_INTERNAL, src, "out of memory");
break;
}
count++;
}
closedir(dir);
for(i = 0; i < count && !ret; i++) {
report(c, ZIPX_PHASE_PUBLISH, names[i], 0);
ret = publish_entry(src, dst, names[i], depth, c);
}
for(i = 0; i < count; i++) {
free(names[i]);
}
free(names);
return ret;
}
static int
publish_staging(zipx_ctx_t *c, const char *dst_dir, int dst_existed) {
int ret;
report(c, ZIPX_PHASE_PUBLISH, dst_dir, 1);
if(!dst_existed) {
if(rename(c->staging, dst_dir)) {
return ctx_fail(c, ZIPX_ERR_IO, dst_dir, "cannot publish: %s",
strerror(errno));
}
c->staging_created = 0; /* the directory now belongs to the user */
return 0;
}
ret = publish_dir(c->staging, dst_dir, 0, c);
if(ret) {
rollback_published(c);
}
return ret;
}
/**************************************************************************
* public entry point
**************************************************************************/
/* Opens the archive for scanning. A split set is served by the volume stream;
because tools disagree on whether a split keeps absolute offsets or offsets
relative to each volume, both layouts are attempted before giving up. */
static int
open_archive(const char *zip_path, zipx_volume_t *vol, int vol_set,
zipx_ctx_t *c, void **zip_out, void **stream_out) {
int attempts = vol_set ? 2 : 1;
int attempt;
for(attempt = 0; attempt < attempts; attempt++) {
void *stream = NULL;
void *zip = NULL;
int rc;
if(vol_set) {
int mode = vol->mode;
if(attempt == 1) {
mode = (vol->mode == ZIPX_VOL_MODE_DISK) ? ZIPX_VOL_MODE_CONCAT
: ZIPX_VOL_MODE_DISK;
}
stream = zipx_volstream_create(mode);
if(stream && zipx_volstream_set_parts(
stream, (const char *const *)vol->paths,
vol->count) != MZ_OK) {
zipx_volstream_delete(&stream);
stream = NULL;
}
} else {
stream = mz_stream_os_create();
}
zip = mz_zip_create();
if(!stream || !zip) {
if(stream) {
mz_stream_delete(&stream);
}
if(zip) {
mz_zip_delete(&zip);
}
ctx_fail(c, ZIPX_ERR_INTERNAL, zip_path, "out of memory");
return (int)c->result->status;
}
rc = mz_stream_open(stream, vol_set ? vol->paths[0] : zip_path,
MZ_OPEN_MODE_READ);
if(rc == MZ_OK) {
rc = mz_zip_open(zip, stream, MZ_OPEN_MODE_READ);
}
if(rc == MZ_OK) {
*zip_out = zip;
*stream_out = stream;
return ZIPX_OK;
}
/* Wrong layout for this set (or a corrupt archive): drop it and retry. */
mz_zip_close(zip);
mz_zip_delete(&zip);
mz_stream_close(stream);
mz_stream_delete(&stream);
}
if(vol_set) {
ctx_fail(c, ZIPX_ERR_OPEN, zip_path,
"cannot read the split archive starting at '%s' (%d volumes): %s",
vol->paths[0], vol->count,
errno ? strerror(errno) : "no known volume layout matched");
} else {
ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s", strerror(errno ? errno : EIO));
}
return (int)c->result->status;
}
zipx_status_t
zipx_extract(const char *zip_path, const char *dst_dir,
zipx_conflict_t conflict, const zipx_limits_t *limits,
zipx_cancel_fn cancel, zipx_progress_fn progress,
void *userdata, zipx_result_t *result) {
zipx_ctx_t ctx;
zipx_ctx_t *c = &ctx;
char parent[ZIPX_PATH_MAX];
char dst_copy[ZIPX_PATH_MAX];
struct stat st;
void *zip = NULL;
void *stream = NULL;
int root_fd = -1;
int dst_existed = 0;
int status = ZIPX_OK;
zipx_volume_t vol;
int vol_set = 0;
if(!result || !zip_path || !dst_dir || !dst_dir[0]) {
if(result) {
memset(result, 0, sizeof(*result));
result->status = ZIPX_ERR_INTERNAL;
snprintf(result->message, sizeof(result->message), "invalid argument");
}
return ZIPX_ERR_INTERNAL;
}
memset(&ctx, 0, sizeof(ctx));
memset(&vol, 0, sizeof(vol));
vol.index = -1;
memset(result, 0, sizeof(*result));
c->result = result;
c->conflict = conflict;
c->limits = limits ? *limits : *zipx_default_limits();
c->cancel = cancel;
c->progress = progress;
c->userdata = userdata;
snprintf(dst_copy, sizeof(dst_copy), "%s", dst_dir);
{
size_t len = strlen(dst_copy);
while(len > 1 && dst_copy[len - 1] == '/') {
dst_copy[--len] = 0;
}
}
if(path_parent(dst_copy, parent, sizeof(parent))) {
status = ctx_fail(c, ZIPX_ERR_INTERNAL, dst_dir, "invalid destination");
goto done;
}
if(stat(parent, &st) || !S_ISDIR(st.st_mode)) {
status = ctx_fail(c, ZIPX_ERR_IO, parent, "destination parent is missing");
goto done;
}
dst_existed = !lstat(dst_copy, &st);
if(dst_existed && !S_ISDIR(st.st_mode)) {
status = ctx_fail(c, ZIPX_ERR_CONFLICT, dst_copy,
"destination is not a directory");
goto done;
}
{
char *vol_err = NULL;
int vrc = zipx_volume_detect(zip_path, &vol, &vol_err);
if(vrc < 0) {
/* A volume of a set that is incomplete gets a precise message here
instead of a generic "cannot open" further down. */
status = ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s",
vol_err ? vol_err : "cannot read the volume set");
free(vol_err);
goto done;
}
free(vol_err);
if(vrc > 0) {
vol_set = 1;
}
}
status = open_archive(zip_path, &vol, vol_set, c, &zip, &stream);
if(status != ZIPX_OK) {
goto done;
}
if(scan_archive(zip, c)) {
status = (int)c->result->status;
goto done;
}
if(canceled(c)) {
status = ctx_fail(c, ZIPX_ERR_CANCELED, NULL, NULL);
goto done;
}
if(check_space(c, dst_existed ? dst_copy : parent)) {
status = (int)c->result->status;
goto done;
}
if(make_staging(c, parent)) {
status = (int)c->result->status;
goto done;
}
root_fd = open(c->staging, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if(root_fd < 0) {
status = ctx_fail(c, ZIPX_ERR_IO, c->staging, "cannot open staging: %s",
strerror(errno));
goto done;
}
if(extract_archive(zip, c, root_fd)) {
status = (int)c->result->status;
goto done;
}
if(root_fd >= 0) {
close(root_fd);
root_fd = -1;
}
if(canceled(c)) {
status = ctx_fail(c, ZIPX_ERR_CANCELED, NULL, NULL);
goto done;
}
if(publish_staging(c, dst_copy, dst_existed)) {
status = (int)c->result->status;
goto done;
}
result->status = ZIPX_OK;
status = ZIPX_OK;
done:
if(root_fd >= 0) {
close(root_fd);
}
cleanup_staging(c);
if(zip) {
mz_zip_close(zip);
mz_zip_delete(&zip);
}
if(stream) {
mz_stream_close(stream);
mz_stream_delete(&stream);
}
zipx_volume_free(&vol);
result->entries_total = c->entries_total;
result->entries_done = c->entries_done;
result->bytes_total = c->bytes_total;
result->files_created = c->files_created;
result->dirs_created = c->dirs_created;
if(status != ZIPX_OK && !result->message[0]) {
snprintf(result->message, sizeof(result->message), "%s",
zipx_status_string(result->status));
}
if(status != ZIPX_OK) {
result->status = (zipx_status_t)status;
}
report(c, ZIPX_PHASE_CLEANUP, NULL, 1);
free_published(c);
return result->status;
}