/* test_zip.c — tuku zip 容器读取器宿主测试(纯 C,无 SDK/硬件,链 -lz)。 * * 样例由 test/make_zip_samples.py 用 python3 zipfile 现场生成(真实工具产物), * 再对拷贝做定点字节修补覆盖坏包分支。 * * 覆盖: * T1 basic.zip:条目枚举顺序/名字原始 UTF-8 字节/压缩方式/尺寸/目录项; * stored 直拷、deflate 解压、中文名、0 字节 stored 与 0 字节 deflate; * mz_find / mz_entry_is_dir / mz_read_alloc / MZ_ERR_BUF / 参数校验。 * T2 comment.zip:EOCD 归档注释不影响开包。 * T3 empty.zip:0 条目可开可枚举。 * T4 坏包容错:截断、非 zip、CD 偏移越界、zip64 标记、坏局部头 magic、 * 数据尺寸越界、CRC 不符、压缩方式不支持——全部返回错误码且不崩。 * * 编译(在 tuku/ 下): * gcc -Wall -Wextra -Werror -O1 -I. test/test_zip.c logic/zip/zip.c -lz \ * -o test/test_zip && ./test/test_zip * 或直接 test/run_zip_tests.sh。 */ #define _POSIX_C_SOURCE 200809L #include "logic/zip/zip.h" #include #include #include #include #include #define PATH_CAP 1024 #define PAGE2_LEN 53280 #define PAGE2_UNIT 296 #define PAGE2_REPS 180 #define BIGRAND_LEN 262144 #define CJK_NAME "sub/dir/\xe4\xb8\xad\xe6\x96\x87\xe5\x90\x8d.txt" #define CJK_DATA "\xe4\xb8\xad\xe6\x96\x87\xe5\x86\x85\xe5\xae\xb9\n" #define HELLO_DATA "hello zip\nhello zip\nhello zip\n" static int g_pass; static int g_fail; static char g_dir[PATH_CAP]; static const char *g_argv0; #define CHECK(cond, ...) do { \ if (cond) { \ g_pass++; \ } else { \ g_fail++; \ printf("FAIL %s:%d ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static const char *fixtures[] = { "basic.zip", "comment.zip", "empty.zip", "truncated.zip", "badmagic.zip", "badoffset.zip", "badcrc.zip", "method99.zip", "badlocal.zip", "bigsize.zip", "zip64claim.zip", "bigrand.zip", "fuzz.zip", }; static void join_path(char *out, size_t cap, const char *name) { snprintf(out, cap, "%s/%s", g_dir, name); } static uint8_t *slurp_file(const char *path, size_t *len) { FILE *f = fopen(path, "rb"); if (!f) return NULL; if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } long end = ftell(f); if (end < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; } uint8_t *buf = malloc((size_t)end ? (size_t)end : 1); if (!buf) { fclose(f); return NULL; } size_t got = fread(buf, 1, (size_t)end, f); fclose(f); if (got != (size_t)end) { free(buf); return NULL; } *len = got; return buf; } static int dump_file(const char *path, const uint8_t *buf, size_t len) { FILE *f = fopen(path, "wb"); if (!f) return 0; size_t put = fwrite(buf, 1, len, f); return fclose(f) == 0 && put == len; } static uint32_t fuzz_next(uint32_t *state) { *state = *state * 1103515245u + 12345u; return *state; } static const char *find_generator(char *buf, size_t cap) { static const char *fallbacks[] = { "test/make_zip_samples.py", "make_zip_samples.py", }; const char *a0 = g_argv0 ? g_argv0 : ""; const char *slash = strrchr(a0, '/'); if (a0[0] && strcmp(a0, "-") != 0 && slash) { snprintf(buf, cap, "%.*s/make_zip_samples.py", (int)(slash - a0), a0); if (access(buf, R_OK) == 0) return buf; } for (size_t i = 0; i < sizeof fallbacks / sizeof fallbacks[0]; i++) { if (access(fallbacks[i], R_OK) == 0) return fallbacks[i]; } return fallbacks[0]; } static int run_generator(const char *gen) { char cmd[PATH_CAP * 2 + 32]; snprintf(cmd, sizeof cmd, "python3 '%s' '%s'", gen, g_dir); int st = system(cmd); if (st == -1) return 0; return WIFEXITED(st) && WEXITSTATUS(st) == 0; } static mz_archive *open_ok(const char *name, int line) { char path[PATH_CAP]; join_path(path, sizeof path, name); mz_archive *z = NULL; int rc = mz_open(path, &z); if (rc != MZ_OK || !z) { g_fail++; printf("FAIL %s:%d open %s rc=%d (%s)\n", __FILE__, line, name, rc, mz_strerror(rc)); if (z) mz_close(z); return NULL; } g_pass++; return z; } static void expect_open_err(const char *name, int want, int line) { char path[PATH_CAP]; join_path(path, sizeof path, name); mz_archive *z = NULL; int rc = mz_open(path, &z); CHECK(rc == want, "open %s: want %d (%s), got %d (%s)", name, want, mz_strerror(want), rc, mz_strerror(rc)); CHECK(z == NULL, "open %s failed but *out != NULL", name); if (z) mz_close(z); (void)line; } static void build_page2(uint8_t *buf) { uint8_t *w = buf; for (int r = 0; r < PAGE2_REPS; r++) { for (int i = 0; i < 10; i++) { memcpy(w, "MEOW", 4); w += 4; } for (int b = 0; b < 256; b++) *w++ = (uint8_t)b; } } static void build_rand(uint8_t *buf) { uint32_t x = 123456789u; for (int i = 0; i < BIGRAND_LEN; i++) { x = (1103515245u * x + 12345u) & 0x7FFFFFFFu; buf[i] = (uint8_t)(x >> 16); } } static void test_basic(void) { mz_archive *z = open_ok("basic.zip", __LINE__); if (!z) return; CHECK(mz_count(z) == 5, "count=%d", mz_count(z)); const mz_entry *e0 = mz_entry_at(z, 0); const mz_entry *e1 = mz_entry_at(z, 1); const mz_entry *e2 = mz_entry_at(z, 2); const mz_entry *e3 = mz_entry_at(z, 3); const mz_entry *e4 = mz_entry_at(z, 4); CHECK(e0 && e1 && e2 && e3 && e4, "entry_at returned NULL"); if (!(e0 && e1 && e2 && e3 && e4)) { mz_close(z); return; } CHECK(strcmp(e0->name, "hello.txt") == 0, "e0 name=%s", e0->name); CHECK(e0->method == 0, "hello method=%u", e0->method); CHECK(e0->comp_size == 30 && e0->uncomp_size == 30, "hello sizes comp=%llu uncomp=%llu", (unsigned long long)e0->comp_size, (unsigned long long)e0->uncomp_size); CHECK(mz_entry_is_dir(e0) == 0, "hello treated as dir"); CHECK(strcmp(e1->name, "page2.png") == 0, "e1 name=%s", e1->name); CHECK(e1->method == 8, "page2 method=%u", e1->method); CHECK(e1->uncomp_size == PAGE2_LEN, "page2 uncomp=%llu", (unsigned long long)e1->uncomp_size); CHECK(e1->comp_size < e1->uncomp_size, "page2 not compressed: %llu/%llu", (unsigned long long)e1->comp_size, (unsigned long long)e1->uncomp_size); CHECK(strcmp(e2->name, CJK_NAME) == 0, "cjk name bytes differ: %s", e2->name); CHECK(e2->name_len == strlen(CJK_NAME), "cjk name_len=%u", e2->name_len); CHECK(e2->method == 8, "cjk method=%u", e2->method); CHECK((e2->flags & 0x0800u) != 0, "cjk UTF-8 flag missing: flags=0x%x", e2->flags); CHECK(e2->uncomp_size == strlen(CJK_DATA), "cjk uncomp=%llu", (unsigned long long)e2->uncomp_size); CHECK(strcmp(e3->name, "onlydir/") == 0, "e3 name=%s", e3->name); CHECK(mz_entry_is_dir(e3) == 1, "onlydir/ not dir"); CHECK(e3->method == 0 && e3->uncomp_size == 0 && e3->comp_size == 0, "onlydir sizes/method wrong"); CHECK(strcmp(e4->name, "empty.bin") == 0, "e4 name=%s", e4->name); CHECK(e4->method == 8 && e4->uncomp_size == 0, "empty.bin method=%u uncomp=%llu", e4->method, (unsigned long long)e4->uncomp_size); CHECK(mz_find(z, "page2.png") == e1, "mz_find page2 mismatch"); CHECK(mz_find(z, "onlydir/") == e3, "mz_find onlydir mismatch"); CHECK(mz_find(z, "missing.txt") == NULL, "mz_find missing != NULL"); CHECK(mz_entry_at(z, -1) == NULL, "entry_at(-1) != NULL"); CHECK(mz_entry_at(z, 5) == NULL, "entry_at(count) != NULL"); uint8_t buf[64]; size_t n = 999; CHECK(mz_read(z, e0, buf, sizeof buf, &n) == MZ_OK, "read hello rc"); CHECK(n == 30, "read hello n=%zu", n); CHECK(n == 30 && memcmp(buf, HELLO_DATA, 30) == 0, "hello content"); uint8_t *page = malloc(PAGE2_LEN); uint8_t *expect = malloc(PAGE2_LEN); CHECK(page && expect, "malloc page2"); if (page && expect) { build_page2(expect); n = 0; CHECK(mz_read(z, e1, page, PAGE2_LEN, &n) == MZ_OK, "read page2 rc"); CHECK(n == PAGE2_LEN, "read page2 n=%zu", n); CHECK(n == PAGE2_LEN && memcmp(page, expect, PAGE2_LEN) == 0, "page2 content"); } free(page); free(expect); n = 999; CHECK(mz_read(z, e2, buf, 4, &n) == MZ_ERR_BUF, "cjk small buf rc"); CHECK(n == 0, "buf error left out_size=%zu", n); uint8_t cjk[64]; n = 0; CHECK(mz_read(z, e2, cjk, sizeof cjk, &n) == MZ_OK, "read cjk rc"); CHECK(n == strlen(CJK_DATA) && memcmp(cjk, CJK_DATA, strlen(CJK_DATA)) == 0, "cjk content"); uint8_t dummy = 0xAA; n = 999; CHECK(mz_read(z, e3, &dummy, sizeof dummy, &n) == MZ_OK, "read dir entry"); CHECK(n == 0 && dummy == 0xAA, "dir entry produced data"); n = 999; CHECK(mz_read(z, e4, &dummy, sizeof dummy, &n) == MZ_OK, "read empty deflate"); CHECK(n == 0, "empty deflate n=%zu", n); void *alloc_buf = NULL; n = 0; CHECK(mz_read_alloc(z, e0, &alloc_buf, &n) == MZ_OK, "read_alloc rc"); CHECK(alloc_buf && n == 30 && memcmp(alloc_buf, HELLO_DATA, 30) == 0, "read_alloc content"); free(alloc_buf); n = 0; CHECK(mz_read(z, e0, NULL, 10, &n) == MZ_ERR_ARG, "NULL buf rc"); CHECK(mz_read(NULL, e0, buf, sizeof buf, &n) == MZ_ERR_ARG, "NULL archive rc"); CHECK(mz_read(z, NULL, buf, sizeof buf, &n) == MZ_ERR_ARG, "NULL entry rc"); CHECK(mz_read_alloc(z, e0, NULL, &n) == MZ_ERR_ARG, "read_alloc NULL out"); mz_close(z); } static void test_comment(void) { mz_archive *z = open_ok("comment.zip", __LINE__); if (!z) return; CHECK(mz_count(z) == 1, "comment count=%d", mz_count(z)); const mz_entry *e = mz_find(z, "note.txt"); CHECK(e != NULL, "comment entry missing"); if (e) { uint8_t buf[32]; size_t n = 0; CHECK(mz_read(z, e, buf, sizeof buf, &n) == MZ_OK, "comment read rc"); CHECK(n == 10 && memcmp(buf, "commented\n", 10) == 0, "comment content n=%zu", n); } mz_close(z); } static void test_empty(void) { mz_archive *z = open_ok("empty.zip", __LINE__); if (!z) return; CHECK(mz_count(z) == 0, "empty count=%d", mz_count(z)); CHECK(mz_entry_at(z, 0) == NULL, "empty entry_at(0) != NULL"); CHECK(mz_find(z, "x") == NULL, "empty find != NULL"); mz_close(z); } static void test_bad_archives(void) { expect_open_err("truncated.zip", MZ_ERR_FORMAT, __LINE__); expect_open_err("badmagic.zip", MZ_ERR_FORMAT, __LINE__); expect_open_err("badoffset.zip", MZ_ERR_TRUNCATED, __LINE__); expect_open_err("zip64claim.zip", MZ_ERR_UNSUPPORTED, __LINE__); mz_archive *z = open_ok("badcrc.zip", __LINE__); if (z) { uint8_t buf[64]; size_t n = 0; CHECK(mz_read(z, mz_find(z, "hello.txt"), buf, sizeof buf, &n) == MZ_ERR_CRC, "badcrc rc"); mz_close(z); } z = open_ok("method99.zip", __LINE__); if (z) { uint8_t buf[64]; size_t n = 0; CHECK(mz_read(z, mz_find(z, "hello.txt"), buf, sizeof buf, &n) == MZ_ERR_UNSUPPORTED, "method99 rc"); mz_close(z); } z = open_ok("badlocal.zip", __LINE__); if (z) { uint8_t buf[64]; size_t n = 0; CHECK(mz_read(z, mz_find(z, "hello.txt"), buf, sizeof buf, &n) == MZ_ERR_FORMAT, "badlocal rc"); mz_close(z); } z = open_ok("bigsize.zip", __LINE__); if (z) { uint8_t *buf = malloc(PAGE2_LEN); size_t n = 0; CHECK(buf != NULL, "malloc bigsize buf"); if (buf) { CHECK(mz_read(z, mz_find(z, "page2.png"), buf, PAGE2_LEN, &n) == MZ_ERR_TRUNCATED, "bigsize rc"); free(buf); } mz_close(z); } } static void test_bigrand(void) { mz_archive *z = open_ok("bigrand.zip", __LINE__); if (!z) return; CHECK(mz_count(z) == 1, "bigrand count=%d", mz_count(z)); const mz_entry *e = mz_find(z, "rand.bin"); CHECK(e != NULL, "bigrand entry missing"); if (!e) { mz_close(z); return; } CHECK(e->uncomp_size == BIGRAND_LEN, "bigrand uncomp=%llu", (unsigned long long)e->uncomp_size); CHECK(e->comp_size > 65536, "bigrand comp=%llu not multi-chunk", (unsigned long long)e->comp_size); uint8_t *buf = malloc(BIGRAND_LEN); uint8_t *expect = malloc(BIGRAND_LEN); CHECK(buf && expect, "malloc bigrand"); if (buf && expect) { build_rand(expect); size_t n = 0; CHECK(mz_read(z, e, buf, BIGRAND_LEN, &n) == MZ_OK, "bigrand read rc"); CHECK(n == BIGRAND_LEN, "bigrand n=%zu", n); CHECK(n == BIGRAND_LEN && memcmp(buf, expect, BIGRAND_LEN) == 0, "bigrand content"); } free(buf); free(expect); mz_close(z); } /* 对 basic.zip 做确定性字节翻转/截断,断言开包与解压全程不崩且错误码可用。 */ static void test_fuzz(void) { char zip_path[PATH_CAP]; char fuzz_path[PATH_CAP]; join_path(zip_path, sizeof zip_path, "basic.zip"); join_path(fuzz_path, sizeof fuzz_path, "fuzz.zip"); size_t base_len = 0; uint8_t *base = slurp_file(zip_path, &base_len); CHECK(base != NULL, "fuzz slurp basic.zip"); if (!base) return; uint8_t *mut = malloc(base_len); CHECK(mut != NULL, "fuzz malloc"); if (!mut) { free(base); return; } uint32_t seed = 0xC0FFEEu; int opened = 0; int rejected = 0; int bad_rc = 0; for (int it = 0; it < 100; it++) { size_t len = base_len; memcpy(mut, base, base_len); int flips = 1 + (int)(fuzz_next(&seed) % 8); for (int k = 0; k < flips; k++) { size_t pos = fuzz_next(&seed) % base_len; mut[pos] ^= (uint8_t)(fuzz_next(&seed) & 0xFF); } if (it % 3 == 2) len = fuzz_next(&seed) % (base_len + 1); if (!dump_file(fuzz_path, mut, len)) { CHECK(0, "fuzz dump iteration %d", it); continue; } mz_archive *z = NULL; int rc = mz_open(fuzz_path, &z); if (rc == MZ_OK && z) { opened++; for (int i = 0; i < mz_count(z); i++) { const mz_entry *e = mz_entry_at(z, i); if (!e || e->uncomp_size > (1u << 20)) continue; size_t cap = (size_t)e->uncomp_size; uint8_t *b = malloc(cap ? cap : 1); if (!b) continue; size_t n = 0; int rrc = mz_read(z, e, b, cap, &n); if (rrc > MZ_OK || rrc < MZ_ERR_CRC) bad_rc++; if (rrc == MZ_OK && n != (size_t)e->uncomp_size) bad_rc++; free(b); } mz_close(z); } else { rejected++; if (z) bad_rc++; } } free(mut); free(base); unlink(fuzz_path); CHECK(rejected > 0, "fuzz: all 100 corrupt archives opened"); CHECK(opened > 0, "fuzz: no corrupted archive reached read path"); CHECK(bad_rc == 0, "fuzz: %d out-of-contract results", bad_rc); } static void test_api(void) { char path[PATH_CAP]; mz_archive *z = (mz_archive *)&g_dir; CHECK(mz_open(NULL, &z) == MZ_ERR_ARG, "open(NULL path) rc"); CHECK(z == NULL, "open(NULL path) left *out set"); CHECK(mz_open("x", NULL) == MZ_ERR_ARG, "open(NULL out) rc"); join_path(path, sizeof path, "no_such_file.zip"); z = (mz_archive *)&g_dir; CHECK(mz_open(path, &z) == MZ_ERR_IO, "open(missing) rc"); CHECK(z == NULL, "open(missing) left *out set"); CHECK(mz_count(NULL) == 0, "count(NULL)"); CHECK(mz_entry_at(NULL, 0) == NULL, "entry_at(NULL)"); CHECK(mz_find(NULL, "x") == NULL, "find(NULL)"); CHECK(mz_entry_is_dir(NULL) == 0, "is_dir(NULL)"); mz_close(NULL); const int codes[] = {MZ_OK, MZ_ERR_IO, MZ_ERR_FORMAT, MZ_ERR_TRUNCATED, MZ_ERR_UNSUPPORTED, MZ_ERR_BUF, MZ_ERR_MEM, MZ_ERR_ARG, MZ_ERR_CRC, 12345}; for (size_t i = 0; i < sizeof codes / sizeof codes[0]; i++) { const char *s = mz_strerror(codes[i]); CHECK(s && s[0] != '\0', "strerror(%d) empty", codes[i]); } } static void cleanup(void) { char path[PATH_CAP]; for (size_t i = 0; i < sizeof fixtures / sizeof fixtures[0]; i++) { join_path(path, sizeof path, fixtures[i]); unlink(path); } rmdir(g_dir); } static int make_temp_dir(void) { const char *bases[3]; int nb = 0; const char *env = getenv("TMPDIR"); if (env && env[0]) bases[nb++] = env; bases[nb++] = "/data/data/com.termux/files/usr/tmp"; bases[nb++] = "/tmp"; for (int i = 0; i < nb; i++) { char tmpl[PATH_CAP]; snprintf(tmpl, sizeof tmpl, "%s/tuku_zip_XXXXXX", bases[i]); if (mkdtemp(tmpl)) { snprintf(g_dir, sizeof g_dir, "%s", tmpl); return 1; } } return 0; } int main(int argc, char **argv) { char gen_buf[PATH_CAP]; const char *gen; g_argv0 = argv[0]; gen = argc > 1 ? argv[1] : find_generator(gen_buf, sizeof gen_buf); if (!make_temp_dir()) { perror("mkdtemp"); return 2; } if (!run_generator(gen)) { printf("generator failed: %s\n", gen); cleanup(); return 2; } test_basic(); test_comment(); test_empty(); test_bad_archives(); test_bigrand(); test_fuzz(); test_api(); cleanup(); printf("zip tests: %d passed, %d failed\n", g_pass, g_fail); return g_fail == 0 ? 0 : 1; }