Files
LisherSong--ps5-web-file-ma…/tests/test_zip_extract.c
T
Songlx516 da565ccb7c feat(zip): extract multi-file ZIP volume sets
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.
2026-09-12 16:13:20 +08:00

782 lines
26 KiB
C

/* Host test suite for the ZIP extraction engine.
Build: see run-tests.sh (uses gcc + the MinGW POSIX shim). */
#include "../src/zip_extract.h"
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Pulled in by the test build only (see run-tests.sh). */
#include "posix_compat.h"
static const char *g_fixtures;
static char g_work[4096];
static int g_failures;
static int g_checks;
static void
fixture_path(char *out, size_t size, const char *name) {
snprintf(out, size, "%s/%s", g_fixtures, name);
}
static void
work_path(char *out, size_t size, const char *name) {
snprintf(out, size, "%s/%s", g_work, name);
}
static void
check(int ok, const char *what) {
g_checks++;
if(!ok) {
g_failures++;
printf(" FAIL %s\n", what);
}
}
static int
exists(const char *path) {
struct stat st;
return !stat(path, &st);
}
static int
read_text(const char *path, char *buf, size_t size) {
FILE *f = fopen(path, "rb");
size_t n;
if(!f) {
return -1;
}
n = fread(buf, 1, size - 1, f);
buf[n] = 0;
fclose(f);
return 0;
}
/* Reads binary content (entries may contain NUL bytes, so read_text cannot be
used to verify them). Returns the number of bytes read, or -1. */
static long
read_bytes(const char *path, unsigned char *buf, size_t size) {
FILE *f = fopen(path, "rb");
size_t n;
if(!f) {
return -1;
}
n = fread(buf, 1, size, f);
fclose(f);
return (long)n;
}
static int
remove_dir(const char *path) { DIR *dir = opendir(path);
struct dirent *ent;
if(!dir) {
return rmdir(path);
}
while((ent = readdir(dir))) {
char child[4096];
struct stat st;
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
continue;
}
snprintf(child, sizeof(child), "%s/%s", path, ent->d_name);
if(!stat(child, &st) && S_ISDIR(st.st_mode)) {
remove_dir(child);
} else {
unlink(child);
}
}
closedir(dir);
return rmdir(path);
}
static int
make_dirs(const char *path) {
char buf[4096];
char *slash;
struct stat st;
if(!*path || !stat(path, &st)) {
return 0;
}
snprintf(buf, sizeof(buf), "%s", path);
for(slash = buf + 1; *slash; slash++) {
if(*slash != '/') {
continue;
}
*slash = 0;
if(mkdir(buf, 0777) && errno != EEXIST) {
return -1;
}
*slash = '/';
}
return mkdir(buf, 0777) && errno != EEXIST ? -1 : 0;
}
static int
write_text(const char *path, const char *content) {
FILE *f = fopen(path, "wb");
if(!f) {
return -1;
}
fputs(content, f);
fclose(f);
return 0;
}
static int
count_staging_leftovers(const char *dir) {
DIR *d = opendir(dir);
struct dirent *ent;
int count = 0;
if(!d) {
return 0;
}
while((ent = readdir(d))) {
if(!strncmp(ent->d_name, ".wfm-extract-", 13)) {
count++;
}
}
closedir(d);
return count;
}
/**************************************************************************/
typedef struct {
int cancel_after_progress;
zipx_progress_t last;
int reports;
char last_current[512];
} test_ctx_t;
static int
cb_cancel(void *userdata) {
test_ctx_t *t = userdata;
return t->cancel_after_progress && t->reports >= t->cancel_after_progress;
}
static void
cb_progress(void *userdata, const zipx_progress_t *p) {
test_ctx_t *t = userdata;
t->reports++;
t->last = *p;
if(p->current) {
snprintf(t->last_current, sizeof(t->last_current), "%s", p->current);
}
}
static zipx_status_t
run(const char *fixture, const char *dst_name, zipx_conflict_t conflict,
const zipx_limits_t *limits, test_ctx_t *t, zipx_result_t *res) {
char zip[4096];
char dst[4096];
fixture_path(zip, sizeof(zip), fixture);
work_path(dst, sizeof(dst), dst_name);
return zipx_extract(zip, dst, conflict, limits, cb_cancel, cb_progress,
t, res);
}
static void
expect_ok(zipx_result_t *res, zipx_status_t status, const char *label) {
check(status == ZIPX_OK, label);
if(status != ZIPX_OK) {
printf(" status=%s (%d) %s / %s\n", zipx_status_string(status),
status, res->message, res->detail);
}
}
static void
expect_status(zipx_result_t *res, zipx_status_t status,
zipx_status_t expected, const char *label) {
check(status == expected, label);
if(status != expected) {
printf(" expected %s, got %s (%d) %s / %s\n",
zipx_status_string(expected), zipx_status_string(status), status,
res->message, res->detail);
}
}
/**************************************************************************/
/* Verifies the entries all three split layouts share: the same archive was
sliced three ways, so every extraction has to produce identical content. */
static void
check_split_content(const char *dst_name, const char *label) {
static unsigned char data[16384];
char dst[4096];
char file[4224];
char buf[512];
long n;
size_t i;
int ok = 1;
work_path(dst, sizeof(dst), dst_name);
check(exists(dst), label);
snprintf(file, sizeof(file), "%s/readme.txt", dst);
n = read_bytes(file, (unsigned char *)buf, sizeof(buf));
check(n == 440 && !memcmp(buf, "split archive fixture\n", 22),
" readme.txt content");
snprintf(file, sizeof(file), "%s/sub/data.bin", dst);
n = read_bytes(file, data, 10240);
if(n != 10240) {
ok = 0;
} else {
for(i = 0; i < 10240; i++) {
if(data[i] != (unsigned char)(i % 256)) {
ok = 0;
break;
}
}
}
check(ok, " sub/data.bin binary content (10 KiB across volumes)");
snprintf(file, sizeof(file), "%s/sub/deep/more.bin", dst);
n = read_bytes(file, data, 12000);
check(n == 12000 && data[0] == 'A' && data[3] == 'D',
" sub/deep/more.bin content");
}
static void
test_volumes(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
printf("split volumes\n");
/* name.zip.001 style (7-Zip byte split), picked from the first volume. */
expect_ok(&res, run("plain.zip.001", "out_vol_plain_a", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"byte split: extract from part 001");
check_split_content("out_vol_plain_a", " output tree exists");
/* Same set, but the user clicked the last volume this time. */
expect_ok(&res, run("plain.zip.003", "out_vol_plain_b", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"byte split: extract from part 003");
check_split_content("out_vol_plain_b", " output tree exists");
/* name.partN.zip style, picked from the middle volume. */
expect_ok(&res, run("parts.part2.zip", "out_vol_parts", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"partN split: extract from part2");
check_split_content("out_vol_parts", " output tree exists");
/* name.z01 ... name.zip: offsets are relative to each disk, so this one
exercises the disk-aware volume stream. */
expect_ok(&res, run("disks.z01", "out_vol_disks_a", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"split disks: extract from z01");
check_split_content("out_vol_disks_a", " output tree exists");
expect_ok(&res, run("disks.zip", "out_vol_disks_b", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"split disks: extract from the final volume");
check_split_content("out_vol_disks_b", " output tree exists");
/* A plain archive must still take the ordinary path. */
expect_ok(&res, run("split_single.zip", "out_vol_single", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
"plain archive still extracts (no volume set detected)");
check_split_content("out_vol_single", " output tree exists");
/* Broken sets must fail with the exact missing volume, not a vague error. */
expect_status(&res, run("gap.zip.001", "out_vol_gap", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
ZIPX_ERR_OPEN, "incomplete set: reported as an open failure");
check(strstr(res.message, "gap.zip.002") != NULL,
" error names the missing volume");
printf(" message: %s\n", res.message);
expect_status(&res, run("broken.zip.001", "out_vol_broken", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
ZIPX_ERR_OPEN, "lone first volume: reported as an open failure");
check(strstr(res.message, "no other volumes") != NULL,
" error says the remaining volumes are missing");
printf(" message: %s\n", res.message);
work_path(dst, sizeof(dst), "out_vol_gap");
check(!exists(dst), "nothing was written for the incomplete set");
}
/**************************************************************************/
static void
test_basic(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[64];
printf("basic (deflate + nested dirs + empty dir)\n");
expect_ok(&res, run("basic.zip", "out_basic", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"extract succeeds");
work_path(dst, sizeof(dst), "out_basic");
check(exists(dst), "destination created");
snprintf(file, sizeof(file), "%s/root.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "root content"),
"root.txt content");
snprintf(file, sizeof(file), "%s/dir/nested.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "nested content"),
"nested.txt content");
snprintf(file, sizeof(file), "%s/dir/deep/deeper.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "deeper content"),
"deeper.txt content");
snprintf(file, sizeof(file), "%s/empty_dir", dst);
check(exists(file), "empty directory created");
check(res.entries_total == 4, "entry count reported");
check(res.bytes_total > 0, "byte total reported");
check(count_staging_leftovers(dst) == 0, "no staging leftovers inside dst");
}
static void
test_stored(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[2048];
size_t i;
int ok = 1;
printf("stored (no compression)\n");
expect_ok(&res, run("stored.zip", "out_stored", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"extract succeeds");
work_path(dst, sizeof(dst), "out_stored");
snprintf(file, sizeof(file), "%s/stored.txt", dst);
if(read_text(file, buf, sizeof(buf))) {
ok = 0;
} else {
for(i = 0; i < 1400; i++) {
if(buf[i] != "stored content"[i % 14]) {
ok = 0;
break;
}
}
}
check(ok, "stored content matches");
}
static void
test_zip64(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
struct stat st;
printf("zip64\n");
expect_ok(&res, run("zip64.zip", "out_zip64", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"extract succeeds");
work_path(dst, sizeof(dst), "out_zip64");
snprintf(file, sizeof(file), "%s/big.bin", dst);
check(!stat(file, &st) && st.st_size == 4096, "zip64 size");
}
static void
test_unicode(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[64];
printf("utf-8 entry names\n");
expect_ok(&res, run("unicode.zip", "out_unicode", ZIPX_CONFLICT_FAIL, NULL,
&t, &res), "extract succeeds");
work_path(dst, sizeof(dst), "out_unicode");
snprintf(file, sizeof(file), "%s/\xe4\xb8\xad\xe6\x96\x87\xe7\x9b\xae\xe5\xbd\x95/\xe6\x96\x87\xe4\xbb\xb6.txt", dst);
check(!read_text(file, buf, sizeof(buf)) &&
!strcmp(buf, "unicode content"), "chinese path content");
}
static void
test_conflict_fail(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[64];
printf("conflict policy: fail\n");
work_path(dst, sizeof(dst), "out_conflict_fail");
remove_dir(dst);
make_dirs(dst);
snprintf(file, sizeof(file), "%s/shared.txt", dst);
write_text(file, "original");
expect_status(&res, run("conflict.zip", "out_conflict_fail",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_CONFLICT, "existing file fails the task");
snprintf(file, sizeof(file), "%s/shared.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "original"),
"existing file untouched");
snprintf(file, sizeof(file), "%s/shareddir", dst);
check(!exists(file), "nothing published");
}
static void
test_conflict_overwrite(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[64];
printf("conflict policy: overwrite\n");
work_path(dst, sizeof(dst), "out_overwrite");
remove_dir(dst);
make_dirs(dst);
/* An existing directory where the archive has a file must still conflict,
even under OVERWRITE (a directory is never replaced by a file). */
snprintf(file, sizeof(file), "%s/shared.txt", dst);
make_dirs(file);
expect_status(&res, run("conflict.zip", "out_overwrite",
ZIPX_CONFLICT_OVERWRITE, NULL, &t, &res),
ZIPX_ERR_CONFLICT, "existing directory still blocks overwrite");
snprintf(file, sizeof(file), "%s/shared.txt", dst);
check(exists(file), "directory untouched while it conflicts");
remove_dir(dst);
make_dirs(dst);
snprintf(file, sizeof(file), "%s/shared.txt", dst);
write_text(file, "original");
snprintf(file, sizeof(file), "%s/shareddir", dst);
remove_dir(file);
expect_ok(&res, run("conflict.zip", "out_overwrite",
ZIPX_CONFLICT_OVERWRITE, NULL, &t, &res),
"overwrite succeeds without a directory clash");
snprintf(file, sizeof(file), "%s/shared.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "from zip"),
"file replaced");
}
static void
test_conflict_merge(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
char buf[64];
printf("conflict policy: merge\n");
work_path(dst, sizeof(dst), "out_merge");
remove_dir(dst);
make_dirs(dst);
snprintf(file, sizeof(file), "%s/shareddir", dst);
make_dirs(file);
snprintf(file, sizeof(file), "%s/shareddir/inner.txt", dst);
check(!write_text(file, "original inner"), "prepare merge destination");
expect_ok(&res, run("conflict.zip", "out_merge", ZIPX_CONFLICT_MERGE, NULL,
&t, &res), "merge succeeds");
snprintf(file, sizeof(file), "%s/shareddir/inner.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "original inner"),
"existing file preserved by merge");
snprintf(file, sizeof(file), "%s/shareddir/added.txt", dst);
check(!read_text(file, buf, sizeof(buf)) &&
!strcmp(buf, "added from zip"), "new sibling merged in");
snprintf(file, sizeof(file), "%s/shared.txt", dst);
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "from zip"),
"top level file published");
}
static void
test_unsafe_names(void) {
zipx_result_t res;
test_ctx_t t = {0};
char parent[4096];
char file[4224];
printf("path traversal variants\n");
expect_status(&res, run("traversal.zip", "out_traversal",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_UNSAFE_NAME, "../ entry rejected");
expect_status(&res, run("traversal_bs.zip", "out_traversal_bs",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_UNSAFE_NAME, "..\\ entry rejected");
expect_status(&res, run("absolute.zip", "out_absolute",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_UNSAFE_NAME, "absolute entry rejected");
expect_status(&res, run("drive.zip", "out_drive", ZIPX_CONFLICT_FAIL, NULL,
&t, &res),
ZIPX_ERR_UNSAFE_NAME, "drive letter entry rejected");
work_path(parent, sizeof(parent), ".");
snprintf(file, sizeof(file), "%s/evil.txt", parent);
check(!exists(file), "no file escaped into the work directory");
snprintf(file, sizeof(file), "%s/out_traversal", parent);
check(!exists(file), "no destination created for a rejected archive");
}
static void
test_duplicates(void) {
zipx_result_t res;
test_ctx_t t = {0};
printf("duplicate and clashing entries\n");
expect_status(&res, run("duplicate.zip", "out_duplicate",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_DUPLICATE, "duplicate file rejected");
expect_status(&res, run("clash.zip", "out_clash", ZIPX_CONFLICT_FAIL, NULL,
&t, &res),
ZIPX_ERR_DUPLICATE, "file used as a directory rejected");
}
static void
test_special_entries(void) {
zipx_result_t res;
test_ctx_t t = {0};
printf("symlink and fifo entries\n");
expect_status(&res, run("symlink.zip", "out_symlink", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
ZIPX_ERR_SPECIAL, "symlink rejected");
expect_status(&res, run("fifo.zip", "out_fifo", ZIPX_CONFLICT_FAIL, NULL,
&t, &res),
ZIPX_ERR_SPECIAL, "fifo rejected");
}
static void
test_unsupported(void) {
zipx_result_t res;
test_ctx_t t = {0};
const zipx_limits_t *base = zipx_default_limits();
zipx_limits_t limits;
printf("encrypted, corrupt and truncated archives\n");
expect_status(&res, run("encrypted.zip", "out_encrypted",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
ZIPX_ERR_UNSUPPORTED, "encrypted entry rejected");
expect_status(&res, run("bad_crc.zip", "out_bad_crc", ZIPX_CONFLICT_FAIL,
NULL, &t, &res),
ZIPX_ERR_CRC, "crc mismatch detected");
check(run("truncated.zip", "out_truncated", ZIPX_CONFLICT_FAIL, NULL, &t, &res) !=
ZIPX_OK, "truncated archive rejected");
check(run("notazip.zip", "out_notazip", ZIPX_CONFLICT_FAIL, NULL, &t, &res) !=
ZIPX_OK, "non-zip file rejected");
printf("limits\n");
limits = *base;
limits.max_entries = 2;
expect_status(&res, run("many.zip", "out_limit_entries",
ZIPX_CONFLICT_FAIL, &limits, &t, &res),
ZIPX_ERR_LIMIT_ENTRIES, "entry limit enforced");
limits = *base;
limits.max_ratio = 10;
limits.ratio_min_bytes = 1024; /* floor below bomb.zip's 4 MiB */
expect_status(&res, run("bomb.zip", "out_limit_ratio", ZIPX_CONFLICT_FAIL,
&limits, &t, &res),
ZIPX_ERR_LIMIT_RATIO, "compression ratio limit enforced");
limits = *base;
limits.max_file_bytes = 1024;
expect_status(&res, run("zip64.zip", "out_limit_file", ZIPX_CONFLICT_FAIL,
&limits, &t, &res),
ZIPX_ERR_LIMIT_FILE, "single file limit enforced");
limits = *base;
limits.max_total_bytes = 1000; /* many.zip totals ~1390 bytes */
expect_status(&res, run("many.zip", "out_limit_total", ZIPX_CONFLICT_FAIL,
&limits, &t, &res),
ZIPX_ERR_LIMIT_TOTAL, "total size limit enforced");
limits = *base;
limits.max_depth = 1;
expect_status(&res, run("basic.zip", "out_limit_depth", ZIPX_CONFLICT_FAIL,
&limits, &t, &res),
ZIPX_ERR_LIMIT_DEPTH, "depth limit enforced");
}
static void
test_cancel(void) {
zipx_result_t res;
test_ctx_t t;
char dst[4096];
printf("cancel leaves nothing behind\n");
memset(&t, 0, sizeof(t));
t.cancel_after_progress = 1;
expect_status(&res, run("many.zip", "out_cancel", ZIPX_CONFLICT_FAIL, NULL,
&t, &res),
ZIPX_ERR_CANCELED, "cancel honored");
work_path(dst, sizeof(dst), "out_cancel");
check(!exists(dst), "no destination after cancel");
work_path(dst, sizeof(dst), ".");
check(count_staging_leftovers(dst) == 0, "no staging left after cancel");
}
static void
test_many_files(void) {
zipx_result_t res;
test_ctx_t t = {0};
char dst[4096];
char file[4224];
int missing = 0;
int i;
printf("500 file archive\n");
expect_ok(&res, run("many.zip", "out_many", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"extract succeeds");
work_path(dst, sizeof(dst), "out_many");
for(i = 0; i < 500; i++) {
snprintf(file, sizeof(file), "%s/many/f%03d.txt", dst, i);
if(!exists(file)) {
missing++;
}
}
check(!missing, "all 500 files present");
check(res.entries_total == 500, "entry count reported");
check(res.bytes_total > 0, "byte total reported");
}
static void
test_large_profile(void) {
zipx_result_t res;
test_ctx_t t = {0};
const zipx_limits_t *base = zipx_default_limits();
const zipx_limits_t *large = zipx_limits_profile(ZIPX_LIMITS_LARGE);
zipx_limits_t tight;
printf("large-file profile\n");
/* The profile must exist and be a real struct, distinct from default. */
check(large != NULL, "large profile returned");
check(large != base, "large profile differs from default");
check(zipx_limits_profile(ZIPX_LIMITS_DEFAULT) == base,
"ZIPX_LIMITS_DEFAULT == zipx_default_limits()");
/* Every cap in the large profile must be at least as large as the default
cap. The profile is strictly an upper bound, never a tighter one. */
check(large->max_entries > base->max_entries,
"max_entries greater than default");
check(large->max_total_bytes > base->max_total_bytes,
"max_total_bytes greater than default");
check(large->max_file_bytes > base->max_file_bytes,
"max_file_bytes greater than default");
check(large->max_ratio > base->max_ratio,
"max_ratio greater than default");
/* Concrete advertised numbers. If anyone ever changes the profile these
assertions keep the public promise honest. */
check(large->max_entries == 500000, "max_entries == 500000");
check(large->max_file_bytes == 1ULL * 1024 * 1024 * 1024 * 1024,
"max_file_bytes == 1 TiB");
check(large->max_total_bytes == 4ULL * 1024 * 1024 * 1024 * 1024,
"max_total_bytes == 4 TiB");
check(large->max_ratio == 1000, "max_ratio == 1000");
check(large->ratio_min_bytes == base->ratio_min_bytes,
"ratio_min_bytes same in both profiles");
check(base->ratio_min_bytes == 1ULL * 1024 * 1024 * 1024,
"ratio_min_bytes == 1 GiB");
/* Behaviour: medium_bomb.zip is 1 MiB of 0..255 cycled, compressing to
~4 KiB (ratio ~238). Default ratio cap 500 accepts it; large ratio
cap 1000 also accepts it. This shows that real-world high-ratio
archives (think raw image dumps, fat binaries) are not artificially
blocked by the relaxed default. */
printf("ratio cap\n");
expect_ok(&res, run("medium_bomb.zip", "out_ratio_medium_default",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"default ratio cap (500) accepts medium_bomb.zip (~238:1)");
expect_ok(&res, run("medium_bomb.zip", "out_ratio_medium_large",
ZIPX_CONFLICT_FAIL, large, &t, &res),
"large ratio cap (1000) accepts medium_bomb.zip (~238:1)");
/* bomb.zip is 4 MiB of identical 'A' bytes (~1026:1). Although its ratio
exceeds both caps, it is far below the 1 GiB ratio_min_bytes floor, so
it is accepted: small highly-compressible entries are common in
legitimate archives (zero-filled placeholders, sparse blobs) and are
harmless because writes are bounded by the declared size plus the real
free-space check. A true bomb's claimed size is what check_space() and
the total/file caps stop, not a small file's ratio. */
expect_ok(&res, run("bomb.zip", "out_ratio_bomb_default",
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
"default accepts small high-ratio bomb.zip (below ratio_min_bytes)");
expect_ok(&res, run("bomb.zip", "out_ratio_bomb_large",
ZIPX_CONFLICT_FAIL, large, &t, &res),
"large accepts small high-ratio bomb.zip (below ratio_min_bytes)");
/* The ratio screen still fires once the entry clears the floor: lowering
the floor below bomb.zip's 4 MiB re-arms the caps. */
tight = *base;
tight.max_ratio = 500;
tight.ratio_min_bytes = 1024;
expect_status(&res, run("bomb.zip", "out_ratio_bomb_floor",
ZIPX_CONFLICT_FAIL, &tight, &t, &res),
ZIPX_ERR_LIMIT_RATIO,
"ratio enforced once entry clears ratio_min_bytes");
/* Lowering the user's chosen ratio below the medium bomb's actual
ratio still rejects the archive (floor disabled to keep the fixture
small). The caps are still enforced; the profile just starts at a
higher number. */
tight = *large;
tight.max_ratio = 200;
tight.ratio_min_bytes = 0;
expect_status(&res, run("medium_bomb.zip", "out_ratio_medium_tight",
ZIPX_CONFLICT_FAIL, &tight, &t, &res),
ZIPX_ERR_LIMIT_RATIO,
"user-lowered ratio (200) rejects medium_bomb.zip (~238:1)");
/* Lowering the large profile's file cap below zip64.zip's 4 KiB still
rejects the archive. */
tight = *large;
tight.max_file_bytes = 1024;
expect_status(&res, run("zip64.zip", "out_large_file_cap",
ZIPX_CONFLICT_FAIL, &tight, &t, &res),
ZIPX_ERR_LIMIT_FILE,
"lowered large file cap still enforced");
}
int
main(int argc, char **argv) {
if(argc < 3) {
fprintf(stderr, "usage: %s <fixtures-dir> <work-dir>\n", argv[0]);
return 2;
}
g_fixtures = argv[1];
snprintf(g_work, sizeof(g_work), "%s", argv[2]);
remove_dir(g_work);
if(make_dirs(g_work)) {
fprintf(stderr, "cannot create work dir\n");
return 2;
}
test_basic();
test_stored();
test_zip64();
test_unicode();
test_conflict_fail();
test_conflict_overwrite();
test_conflict_merge();
test_unsafe_names();
test_duplicates();
test_special_entries();
test_unsupported();
test_cancel();
test_many_files();
test_large_profile();
test_volumes();
printf("\n%d checks, %d failures\n", g_checks, g_failures);
return g_failures ? 1 : 0;
}