mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 14:00:24 +02:00
A split ZIP is several files that only form an archive together. Until now a `.zip.001` was rejected as an unsupported format and a `.z01`/`.partN.zip` member could not be resolved, so these downloads were unusable. Three naming conventions are recognised, from any member of the set: name.zip.001, name.zip.002, ... byte split (7-Zip) name.part1.zip, name.part2.zip byte split (WinRAR) name.z01, ..., name.zip zip split disks (Info-ZIP / PKZIP) The parts are served through a new stream that makes them look like one archive, so nothing is copied to disk first (a 160 GiB set would otherwise need a second 160 GiB scratch copy): - src/zipx_volstream.c implements an mz_stream over the ordered part list. Byte splits keep absolute offsets; zip split disks carry offsets relative to the disk an entry starts on, so that mode honours MZ_STREAM_PROP_DISK_NUMBER the way mz_zip_entry_seek_local_header drives it and starts on the last volume, where the central directory lives. - src/zipx_volume.c groups the set, verifies the numbering is complete and reports exactly which volume is missing. - zip_extract.c tries the layout implied by the naming first and the other one as a fallback, so tools that disagree about offset bases still work. - extract.c dispatches volume members to the engines and, when the caller asked for the source to be deleted, removes every volume instead of leaving orphaned parts behind. Error messages are specific on purpose: an incomplete set reports the missing file name (e.g. "gap.zip.002 is missing"), a lone first volume says the remaining volumes are missing, `.rar.001` sets explain the rename that makes them work, and 7z volumes say 7z is not supported yet. Tests: tests/make_split_fixtures.py builds all three layouts (the disk set is written with per-disk offsets exactly as APPNOTE 4.4.11 describes) plus two broken sets; test_zip_extract.c extracts each layout from a different member and byte-compares the nested binary entries. 108 ZIP + 27 RAR checks, 0 failures.
1481 lines
41 KiB
C
1481 lines
41 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
|
|
|
|
/* Default limits.
|
|
*
|
|
* Tuned to cover real-world PS5 workloads without prompting:
|
|
* - PS5 system backup ZIPs (~200-300 GiB total, individual chunks <64 GiB)
|
|
* - 3A-game archives with a single ~300 GiB uncompressed file
|
|
*
|
|
* Safety against zip bombs is delegated to:
|
|
* 1. `check_space()` (statvfs-based real disk space check) before extract
|
|
* 2. `max_ratio` below (declared compression ratio cap)
|
|
* The size caps here are an early-fail UX guard, not a security boundary.
|
|
*/
|
|
static const zipx_limits_t k_default_limits = {
|
|
.max_entries = 200000,
|
|
.max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024,
|
|
.max_file_bytes = 512ULL * 1024 * 1024 * 1024,
|
|
.max_ratio = 500,
|
|
/* Only entries that would individually materialise >=1 GiB are screened
|
|
by ratio; anything smaller is harmless (bounded by declared size + the
|
|
real free-space check) and is commonly highly compressible in
|
|
legitimate archives. */
|
|
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
|
.max_depth = 32,
|
|
.max_name_len = 255,
|
|
.max_path_len = 1024
|
|
};
|
|
|
|
/* Large profile for archives that exceed the default cap.
|
|
*
|
|
* - max_file_bytes = 1 TiB (single uncompressed file)
|
|
* - max_total_bytes = 4 TiB (whole archive)
|
|
* - max_ratio = 1000 (relaxed ratio cap; check_space still applies)
|
|
*
|
|
* Requires the user to opt in via the web UI (large=1) before these take
|
|
* effect. Default limits must always be strictly smaller than large so the
|
|
* large profile is unambiguously a relaxation.
|
|
*/
|
|
static const zipx_limits_t k_large_limits = {
|
|
.max_entries = 500000,
|
|
.max_total_bytes = 4ULL * 1024 * 1024 * 1024 * 1024,
|
|
.max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024,
|
|
.max_ratio = 1000,
|
|
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
|
.max_depth = 32,
|
|
.max_name_len = 255,
|
|
.max_path_len = 1024
|
|
};
|
|
|
|
const zipx_limits_t *
|
|
zipx_default_limits(void) {
|
|
return &k_default_limits;
|
|
}
|
|
|
|
const zipx_limits_t *
|
|
zipx_limits_profile(int profile) {
|
|
switch(profile) {
|
|
case ZIPX_LIMITS_LARGE:
|
|
return &k_large_limits;
|
|
case ZIPX_LIMITS_DEFAULT:
|
|
default:
|
|
return &k_default_limits;
|
|
}
|
|
}
|
|
|
|
const char *
|
|
zipx_status_string(zipx_status_t status) {
|
|
switch(status) {
|
|
case ZIPX_OK: return "ok";
|
|
case ZIPX_ERR_CANCELED: return "canceled";
|
|
case ZIPX_ERR_OPEN: return "cannot open archive";
|
|
case ZIPX_ERR_FORMAT: return "corrupt archive";
|
|
case ZIPX_ERR_UNSUPPORTED: return "unsupported archive";
|
|
case ZIPX_ERR_UNSAFE_NAME: return "unsafe entry name";
|
|
case ZIPX_ERR_SPECIAL: return "unsupported entry type";
|
|
case ZIPX_ERR_DUPLICATE: return "duplicate entry name";
|
|
case ZIPX_ERR_LIMIT_ENTRIES: return "too many entries";
|
|
case ZIPX_ERR_LIMIT_FILE: return "entry too large";
|
|
case ZIPX_ERR_LIMIT_TOTAL: return "archive contents too large";
|
|
case ZIPX_ERR_LIMIT_RATIO: return "compression ratio too high";
|
|
case ZIPX_ERR_LIMIT_DEPTH: return "path too deep";
|
|
case ZIPX_ERR_LIMIT_NAME: return "path too long";
|
|
case ZIPX_ERR_CONFLICT: return "target already exists";
|
|
case ZIPX_ERR_SPACE: return "not enough space";
|
|
case ZIPX_ERR_IO: return "read or write failed";
|
|
case ZIPX_ERR_CRC: return "crc mismatch";
|
|
default: return "internal error";
|
|
}
|
|
}
|
|
|
|
/**************************************************************************
|
|
* 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 : k_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;
|
|
}
|