mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 09:00:26 +02:00
A 3A-game archive with a single ~300 GiB uncompressed file was silently
rejected by the v1.8.1 default profile (scan_archive returns
ZIPX_ERR_LIMIT_FILE_SIZE before the request ever reaches the frontend
confirmation prompt, so the user just sees "卡壳"). The 256 GiB default
cap was tuned for PS5 system backups (many smaller entries) and was wrong
for the 3A-game single-file case.
Limits changes:
- src/zip_extract.c k_default_limits: 1 TiB / 256 GiB / 500 -> 2 TiB / 512 GiB / 500
- src/zip_extract.c k_large_limits: 2 TiB / 1 TiB / 1000 -> 4 TiB / 1 TiB / 1000 (must stay > default)
- assets/main.js LARGE_FILE_THRESHOLD_BYTES: 240 GiB -> 480 GiB
- assets/lang-{en,zh}.js extractLargeAsk copy reflects new numbers
- tests/test_zip_extract.c: large.max_total_bytes advertised-number assertion updated to 4 TiB
- README.md / docs/HANDOVER.md: limit tables, FAQ, threshold snippets updated
- CHANGELOG.md: v1.8.2 entry added above v1.8.1
PS5-only build fixes (caught by WSL cross-compile, host tests pass without):
- Makefile CFLAGS: add -Ithird_party/unrar so src/rar_extract.c can find
the dmc_unrar_api.h facade header (THIRD_PARTY_CFLAGS already had it
but the main CFLAGS for project sources did not).
- src/extract.c: move extract_progress() definition before
extract_dispatch() so the implicit function declaration is not flagged
by -Werror=implicit-function-declaration (clang 18 in the PS5 SDK is
stricter than the host gcc used by tests/run-tests.sh).
Release artifact: web-file-mgr.elf 509 704 bytes,
sha256 1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65,
e_machine 0x003e (x86_64-sie-ps5).
Safety argument is unchanged from v1.8.1: zip-bomb defence is
check_space() (statvfs-based real disk-space check before staging) +
max_ratio (declared compression ratio cap). The size caps are a UX guard,
not a security boundary.
RAR extraction inherits the new defaults automatically (rar_extract.c
threads c->limits from the engine).
84 host-side checks (70 ZIP + 14 RAR), 0 failures. PS5 cross-compile
succeeds; ELF size grew from 427 656 (v1.8) to 509 704 (v1.8.2) due to
the dmc_unrar vendor TU being linked in.
1322 lines
36 KiB
C
1322 lines
36 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"
|
|
|
|
#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,
|
|
.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,
|
|
.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 > (uint64_t)info->compressed_size *
|
|
c->limits.max_ratio) {
|
|
ret = ctx_fail(c, ZIPX_ERR_LIMIT_RATIO, name,
|
|
"compression ratio is above %u", c->limits.max_ratio);
|
|
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
|
|
**************************************************************************/
|
|
|
|
/* 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];
|
|
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;
|
|
|
|
if(!mkdirat(fd, seg, 0777)) {
|
|
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
if(next >= 0) {
|
|
chmod_0777_fd(next);
|
|
c->dirs_created++;
|
|
}
|
|
} else if(errno == EEXIST) {
|
|
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
} else {
|
|
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot create directory '%s': %s", seg,
|
|
strerror(errno));
|
|
if(fd != root_fd) {
|
|
close(fd);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
if(next < 0) {
|
|
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot open directory '%s': %s", seg,
|
|
strerror(errno));
|
|
if(fd != root_fd) {
|
|
close(fd);
|
|
}
|
|
return -1;
|
|
}
|
|
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) {
|
|
ctx_fail(c, ZIPX_ERR_IO, name, "cannot create file: %s", strerror(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)) {
|
|
ctx_fail(c, ZIPX_ERR_IO, name, "cannot move file into place: %s",
|
|
strerror(errno));
|
|
ret = -1;
|
|
}
|
|
if(fd >= 0) {
|
|
close(fd);
|
|
}
|
|
if(ret) {
|
|
unlinkat(dir_fd, tmp, 0);
|
|
} 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
|
|
**************************************************************************/
|
|
|
|
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;
|
|
|
|
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(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;
|
|
}
|
|
|
|
stream = mz_stream_os_create();
|
|
zip = mz_zip_create();
|
|
if(!stream || !zip) {
|
|
status = ctx_fail(c, ZIPX_ERR_INTERNAL, zip_path, "out of memory");
|
|
goto done;
|
|
}
|
|
if(mz_stream_open(stream, zip_path, MZ_OPEN_MODE_READ) ||
|
|
mz_zip_open(zip, stream, MZ_OPEN_MODE_READ)) {
|
|
status = ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s",
|
|
strerror(errno ? errno : EIO));
|
|
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);
|
|
}
|
|
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;
|
|
}
|