feat(tuku): zip 容器读取器核心(纯 C + zlib,central directory 校验/单条目按需解压)
This commit is contained in:
1 parent
6caf2bc6e6
commit
fbd4db3aec
5 files changed
+1318
No files matched your search
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 tuku zip 读取器的宿主测试样例(真实 zipfile + 定点字节修补)。
|
||||
|
||||
用法: python3 make_zip_samples.py <out_dir>
|
||||
|
||||
生成:
|
||||
basic.zip 5 条目:stored 文本 / deflate 大二进制 / 中文名 deflate /
|
||||
目录项 / 0 字节 deflate
|
||||
comment.zip 单条目 + 归档注释(EOCD 注释长度路径)
|
||||
empty.zip 0 条目
|
||||
truncated.zip basic.zip 砍半(EOCD 丢失)
|
||||
badmagic.zip 非 zip 字节
|
||||
badoffset.zip 首条目 local header 偏移改成 0xFFFFFF00(越界)
|
||||
badcrc.zip 首条目 CD 里 CRC 翻转(内容解出但校验失败)
|
||||
method99.zip 首条目压缩方式改成 99,CD 与局部头都改
|
||||
badlocal.zip 首条目局部头签名改成 "BAD!"
|
||||
bigsize.zip deflate 条目 CD comp_size 改成 8 MiB(数据越界)
|
||||
zip64claim.zip EOCD 总条目数改成 0xFFFF(zip64 标记)
|
||||
bigrand.zip 单条目 256 KiB 不可压 deflate(压缩流 > 64 KiB,覆盖分块 inflate)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
PAGE2 = (b"MEOW" * 10 + bytes(range(256))) * 180 # 53280 字节,可压缩
|
||||
HELLO = b"hello zip\n" * 3
|
||||
CJK_NAME = "sub/dir/\u4e2d\u6587\u540d.txt"
|
||||
CJK_DATA = "\u4e2d\u6587\u5185\u5bb9\n".encode("utf-8")
|
||||
BIGRAND_LEN = 262144
|
||||
|
||||
|
||||
def lcg_bytes(n):
|
||||
"""与 test_zip.c 的 build_rand 同一个 LCG,用于 256 KiB 不可压样例。"""
|
||||
x = 123456789
|
||||
out = bytearray(n)
|
||||
for i in range(n):
|
||||
x = (1103515245 * x + 12345) & 0x7FFFFFFF
|
||||
out[i] = (x >> 16) & 0xFF
|
||||
return bytes(out)
|
||||
|
||||
EOCD_SIG = b"PK\x05\x06"
|
||||
CD_SIG = b"PK\x01\x02"
|
||||
|
||||
|
||||
def write_basic(path):
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
z.writestr(zipfile.ZipInfo("hello.txt", (2024, 1, 1, 0, 0, 0)),
|
||||
HELLO, compress_type=zipfile.ZIP_STORED)
|
||||
z.writestr(zipfile.ZipInfo("page2.png", (2024, 1, 1, 0, 0, 0)),
|
||||
PAGE2, compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.writestr(zipfile.ZipInfo(CJK_NAME, (2024, 1, 1, 0, 0, 0)),
|
||||
CJK_DATA, compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.writestr(zipfile.ZipInfo("onlydir/", (2024, 1, 1, 0, 0, 0)),
|
||||
b"", compress_type=zipfile.ZIP_STORED)
|
||||
z.writestr(zipfile.ZipInfo("empty.bin", (2024, 1, 1, 0, 0, 0)),
|
||||
b"", compress_type=zipfile.ZIP_DEFLATED)
|
||||
|
||||
|
||||
def write_comment(path):
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
z.writestr("note.txt", b"commented\n", compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.comment = b"meow archive comment"
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, "rb") as f:
|
||||
return bytearray(f.read())
|
||||
|
||||
|
||||
def save(path, data):
|
||||
with open(path, "wb") as f:
|
||||
f.write(bytes(data))
|
||||
|
||||
|
||||
def eocd_info(data):
|
||||
i = data.rfind(EOCD_SIG)
|
||||
if i < 0:
|
||||
raise SystemExit("generator: no EOCD in fixture")
|
||||
total, cd_size, cd_off = struct.unpack_from("<HII", data, i + 10)
|
||||
return i, total, cd_size, cd_off
|
||||
|
||||
|
||||
def cd_records(data):
|
||||
_, total, _cd_size, cd_off = eocd_info(data)
|
||||
out = []
|
||||
p = cd_off
|
||||
for _ in range(total):
|
||||
if bytes(data[p:p + 4]) != CD_SIG:
|
||||
raise SystemExit("generator: bad CD signature")
|
||||
nlen, elen, clen = struct.unpack_from("<HHH", data, p + 28)
|
||||
loff = struct.unpack_from("<I", data, p + 42)[0]
|
||||
name = bytes(data[p + 46:p + 46 + nlen]).decode("utf-8")
|
||||
out.append({"off": p, "name": name, "local": loff})
|
||||
p += 46 + nlen + elen + clen
|
||||
return out
|
||||
|
||||
|
||||
def find_record(data, name):
|
||||
for r in cd_records(data):
|
||||
if r["name"] == name:
|
||||
return r
|
||||
raise SystemExit("generator: entry not found: " + name)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: make_zip_samples.py <out_dir>")
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
basic = os.path.join(out, "basic.zip")
|
||||
write_basic(basic)
|
||||
write_comment(os.path.join(out, "comment.zip"))
|
||||
with zipfile.ZipFile(os.path.join(out, "empty.zip"), "w"):
|
||||
pass
|
||||
|
||||
raw = load(basic)
|
||||
save(os.path.join(out, "truncated.zip"), raw[:len(raw) // 2])
|
||||
save(os.path.join(out, "badmagic.zip"), b"NOTAZIP!" * 100)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
struct.pack_into("<I", d, hello["off"] + 42, 0xFFFFFF00)
|
||||
save(os.path.join(out, "badoffset.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
crc = struct.unpack_from("<I", d, hello["off"] + 16)[0]
|
||||
struct.pack_into("<I", d, hello["off"] + 16, crc ^ 0xFFFFFFFF)
|
||||
save(os.path.join(out, "badcrc.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
struct.pack_into("<H", d, hello["off"] + 10, 99)
|
||||
struct.pack_into("<H", d, hello["local"] + 8, 99)
|
||||
save(os.path.join(out, "method99.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
d[hello["local"]:hello["local"] + 4] = b"BAD!"
|
||||
save(os.path.join(out, "badlocal.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
page2 = find_record(d, "page2.png")
|
||||
struct.pack_into("<I", d, page2["off"] + 20, 0x00800000)
|
||||
save(os.path.join(out, "bigsize.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
eocd, _total, _cd_size, _cd_off = eocd_info(d)
|
||||
struct.pack_into("<H", d, eocd + 10, 0xFFFF)
|
||||
save(os.path.join(out, "zip64claim.zip"), d)
|
||||
|
||||
with zipfile.ZipFile(os.path.join(out, "bigrand.zip"), "w") as z:
|
||||
z.writestr("rand.bin", lcg_bytes(BIGRAND_LEN),
|
||||
compress_type=zipfile.ZIP_DEFLATED)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# run_zip_tests.sh — 宿主 gcc 构建并运行 zip 容器读取器测试(纯 C + -lz,无 SDK)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
trap 'rm -f test/test_zip' EXIT
|
||||
gcc -Wall -Wextra -Werror -O1 -I. \
|
||||
test/test_zip.c logic/zip/zip.c \
|
||||
-lz -o test/test_zip
|
||||
./test/test_zip ./test/make_zip_samples.py
|
||||
@@ -0,0 +1,570 @@
|
||||
/* 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;
|
||||
}
|
||||
Reference in new issue
Block a user