Files
MeowOS-SDK/tuku/test/test_zip.c
T

571 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#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;
}