diff --git a/tuku/logic/zip/zip.c b/tuku/logic/zip/zip.c new file mode 100644 index 0000000..dd60b39 --- /dev/null +++ b/tuku/logic/zip/zip.c @@ -0,0 +1,493 @@ +/* zip.c — zip 容器读取器实现,见 zip.h 的用法/线程/安全说明。 */ +#define _POSIX_C_SOURCE 200809L +#define _FILE_OFFSET_BITS 64 + +#include "zip.h" + +#include +#include +#include +#include +#include + +#define MZ_SIG_LOCAL 0x04034b50u +#define MZ_SIG_CD 0x02014b50u +#define MZ_SIG_EOCD 0x06054b50u +#define MZ_LOCAL_MIN 30u +#define MZ_CD_MIN 46u +#define MZ_EOCD_MIN 22u +#define MZ_COMMENT_MAX 65535u +#define MZ_IN_CHUNK 65536u +#define MZ_CRC_CHUNK (1u << 24) + +struct mz_archive { + FILE *fp; + uint64_t file_size; + int count; + mz_entry *entries; + char *names; +}; + +static uint16_t rd16(const uint8_t *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static uint32_t rd32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static int u64_add(uint64_t a, uint64_t b, uint64_t *out) +{ + if (a > UINT64_MAX - b) return 0; + *out = a + b; + return 1; +} + +static int seek_to(FILE *fp, uint64_t off) +{ + off_t o = (off_t)off; + if ((uint64_t)o != off) return MZ_ERR_UNSUPPORTED; + return fseeko(fp, o, SEEK_SET) == 0 ? MZ_OK : MZ_ERR_IO; +} + +static int read_exact(FILE *fp, void *buf, size_t len) +{ + if (fread(buf, 1, len, fp) == len) return MZ_OK; + return ferror(fp) ? MZ_ERR_IO : MZ_ERR_TRUNCATED; +} + +static int read_at(FILE *fp, uint64_t off, void *buf, size_t len) +{ + int rc = seek_to(fp, off); + if (rc != MZ_OK) return rc; + return read_exact(fp, buf, len); +} + +static int get_file_size(FILE *fp, uint64_t *size) +{ + if (fseeko(fp, 0, SEEK_END) != 0) return MZ_ERR_IO; + off_t end = ftello(fp); + if (end < 0) return MZ_ERR_IO; + *size = (uint64_t)end; + return MZ_OK; +} + +/* 从文件尾向前找 EOCD 签名;只接受注释长度不越过文件尾的候选。 */ +static int find_eocd(FILE *fp, uint64_t file_size, uint64_t *eocd_off, + uint8_t eocd[MZ_EOCD_MIN]) +{ + if (file_size < MZ_EOCD_MIN) return MZ_ERR_TRUNCATED; + + uint64_t tail_len = file_size; + if (tail_len > (uint64_t)MZ_EOCD_MIN + MZ_COMMENT_MAX) + tail_len = (uint64_t)MZ_EOCD_MIN + MZ_COMMENT_MAX; + uint64_t tail_off = file_size - tail_len; + + uint8_t *tail = malloc((size_t)tail_len); + if (!tail) return MZ_ERR_MEM; + int rc = read_at(fp, tail_off, tail, (size_t)tail_len); + if (rc == MZ_OK) { + rc = MZ_ERR_FORMAT; + for (uint64_t i = tail_len - MZ_EOCD_MIN + 1; i-- > 0; ) { + if (rd32(tail + i) != MZ_SIG_EOCD) continue; + uint64_t pos = tail_off + i; + uint64_t end = 0; + if (!u64_add(pos, MZ_EOCD_MIN, &end)) continue; + if (!u64_add(end, rd16(tail + i + 20), &end)) continue; + if (end > file_size) continue; + memcpy(eocd, tail + i, MZ_EOCD_MIN); + *eocd_off = pos; + rc = MZ_OK; + break; + } + } + free(tail); + return rc; +} + +/* 解析 central directory;名字先指向 cd 缓冲,成功后在 names_out 里做一份连续副本。 */ +static int parse_cd(const uint8_t *cd, uint32_t cd_size, int declared, + mz_entry *entries, char **names_out) +{ + const uint8_t *p = cd; + const uint8_t *end = cd + cd_size; + uint64_t names_total = 0; + int n = 0; + int rc = MZ_OK; + + while (n < declared) { + if ((size_t)(end - p) < MZ_CD_MIN || rd32(p) != MZ_SIG_CD) { + rc = MZ_ERR_FORMAT; + break; + } + uint16_t flags = rd16(p + 8); + uint16_t method = rd16(p + 10); + uint32_t crc = rd32(p + 16); + uint32_t comp = rd32(p + 20); + uint32_t uncomp = rd32(p + 24); + uint16_t nlen = rd16(p + 28); + uint16_t elen = rd16(p + 30); + uint16_t clen = rd16(p + 32); + uint16_t disk = rd16(p + 34); + uint32_t loff = rd32(p + 42); + uint64_t rec = (uint64_t)MZ_CD_MIN + nlen + elen + clen; + + if (rec > (uint64_t)(end - p)) { + rc = MZ_ERR_FORMAT; + break; + } + if (disk != 0) { + rc = MZ_ERR_UNSUPPORTED; + break; + } + if (comp == 0xFFFFFFFFu || uncomp == 0xFFFFFFFFu || loff == 0xFFFFFFFFu) { + rc = MZ_ERR_UNSUPPORTED; + break; + } + + mz_entry *e = &entries[n]; + e->name = (const char *)(p + MZ_CD_MIN); + e->name_len = nlen; + e->method = method; + e->flags = flags; + e->crc = crc; + e->comp_size = comp; + e->uncomp_size = uncomp; + e->local_offset = loff; + + names_total += (uint64_t)nlen + 1; + n++; + p += (size_t)rec; + } + + if (rc == MZ_OK) { + if (n != declared) { + rc = MZ_ERR_FORMAT; + } else if (names_total > (uint64_t)SIZE_MAX) { + rc = MZ_ERR_MEM; + } else { + char *arena = malloc((size_t)names_total); + if (!arena) { + rc = MZ_ERR_MEM; + } else { + char *w = arena; + for (int i = 0; i < n; i++) { + memcpy(w, entries[i].name, entries[i].name_len); + w[entries[i].name_len] = '\0'; + entries[i].name = w; + w += entries[i].name_len + 1; + } + *names_out = arena; + } + } + } + return rc; +} + +static int load_cd(mz_archive *z, uint64_t cd_off, uint32_t cd_size, int declared) +{ + uint8_t *cd = malloc(cd_size); + if (!cd) return MZ_ERR_MEM; + z->entries = calloc((size_t)declared, sizeof *z->entries); + if (!z->entries) { + free(cd); + return MZ_ERR_MEM; + } + + int rc = read_at(z->fp, cd_off, cd, cd_size); + if (rc == MZ_OK) rc = parse_cd(cd, cd_size, declared, z->entries, &z->names); + if (rc == MZ_OK) { + z->count = declared; + for (int i = 0; i < declared; i++) { + uint64_t end = 0; + if (!u64_add(z->entries[i].local_offset, MZ_LOCAL_MIN, &end) || + end > z->file_size) { + rc = MZ_ERR_TRUNCATED; + break; + } + } + } + free(cd); + return rc; +} + +int mz_open(const char *path, mz_archive **out) +{ + if (!out) return MZ_ERR_ARG; + *out = NULL; + if (!path) return MZ_ERR_ARG; + + mz_archive *z = calloc(1, sizeof *z); + if (!z) return MZ_ERR_MEM; + z->fp = fopen(path, "rb"); + if (!z->fp) { + free(z); + return MZ_ERR_IO; + } + + int rc = get_file_size(z->fp, &z->file_size); + uint8_t eocd[MZ_EOCD_MIN]; + uint64_t eocd_off = 0; + uint16_t n_total = 0; + uint32_t cd_size = 0; + uint32_t cd_off = 0; + + if (rc == MZ_OK) rc = find_eocd(z->fp, z->file_size, &eocd_off, eocd); + if (rc == MZ_OK) { + uint16_t disk_no = rd16(eocd + 4); + uint16_t cd_disk = rd16(eocd + 6); + uint16_t n_disk = rd16(eocd + 8); + n_total = rd16(eocd + 10); + cd_size = rd32(eocd + 12); + cd_off = rd32(eocd + 16); + + if (disk_no != 0 || cd_disk != 0) { + rc = MZ_ERR_UNSUPPORTED; + } else if (n_total == 0xFFFFu || n_disk == 0xFFFFu || + cd_size == 0xFFFFFFFFu || cd_off == 0xFFFFFFFFu) { + rc = MZ_ERR_UNSUPPORTED; /* zip64:EOCD 字段饱和 */ + } else if (n_disk != n_total) { + rc = MZ_ERR_FORMAT; + } else { + uint64_t cd_end = 0; + if (!u64_add(cd_off, cd_size, &cd_end) || cd_end > eocd_off) { + rc = MZ_ERR_TRUNCATED; + } else if (n_total > 0 && (uint64_t)cd_size < (uint64_t)n_total * MZ_CD_MIN) { + rc = MZ_ERR_FORMAT; + } else if ((uint64_t)cd_size > (uint64_t)SIZE_MAX) { + rc = MZ_ERR_UNSUPPORTED; + } + } + } + if (rc == MZ_OK && n_total > 0) rc = load_cd(z, cd_off, cd_size, n_total); + + if (rc != MZ_OK) { + free(z->entries); + free(z->names); + fclose(z->fp); + free(z); + return rc; + } + *out = z; + return MZ_OK; +} + +void mz_close(mz_archive *z) +{ + if (!z) return; + if (z->fp) fclose(z->fp); + free(z->entries); + free(z->names); + free(z); +} + +int mz_count(const mz_archive *z) +{ + return z ? z->count : 0; +} + +const mz_entry *mz_entry_at(const mz_archive *z, int index) +{ + if (!z || index < 0 || index >= z->count) return NULL; + return &z->entries[index]; +} + +const mz_entry *mz_find(const mz_archive *z, const char *name) +{ + if (!z || !name) return NULL; + for (int i = 0; i < z->count; i++) { + if (strcmp(z->entries[i].name, name) == 0) return &z->entries[i]; + } + return NULL; +} + +int mz_entry_is_dir(const mz_entry *e) +{ + return e && e->name_len > 0 && e->name[e->name_len - 1] == '/'; +} + +static uint32_t crc_buf(const uint8_t *p, size_t n) +{ + uLong c = crc32(0L, Z_NULL, 0); + while (n > 0) { + uInt chunk = n > MZ_CRC_CHUNK ? MZ_CRC_CHUNK : (uInt)n; + c = crc32(c, p, chunk); + p += chunk; + n -= chunk; + } + return (uint32_t)c; +} + +/* 从 data_off 流式 inflate comp_size 字节到 buf(容量 buf_cap = 声明解压尺寸)。 + * 输出多于声明尺寸或流提前结束都算 FORMAT/TRUNCATED,不整块读取压缩数据。 */ +static int inflate_entry(FILE *fp, uint64_t data_off, uint64_t comp_size, + uint8_t *buf, size_t buf_cap, size_t *out_len) +{ + z_stream z; + memset(&z, 0, sizeof z); + if (inflateInit2(&z, -MAX_WBITS) != Z_OK) return MZ_ERR_MEM; + + uint8_t *in = malloc(MZ_IN_CHUNK); + if (!in) { + inflateEnd(&z); + return MZ_ERR_MEM; + } + + int rc = seek_to(fp, data_off); + size_t produced = 0; + uint64_t consumed = 0; + uint8_t scratch = 0; + int stalls = 0; + + while (rc == MZ_OK) { + if (z.avail_in == 0) { + if (consumed >= comp_size) { + rc = MZ_ERR_TRUNCATED; + break; + } + uint64_t remain = comp_size - consumed; + size_t want = remain > MZ_IN_CHUNK ? MZ_IN_CHUNK : (size_t)remain; + rc = read_exact(fp, in, want); + if (rc != MZ_OK) break; + z.next_in = in; + z.avail_in = (uInt)want; + consumed += want; + } + + uInt avail; + if (produced < buf_cap) { + size_t room = buf_cap - produced; + z.next_out = buf + produced; + avail = room > UINT_MAX ? UINT_MAX : (uInt)room; + } else { + z.next_out = &scratch; /* 声明尺寸之外的输出 → 直接判坏流 */ + avail = 1; + } + z.avail_out = avail; + + uInt in_before = z.avail_in; + int zr = inflate(&z, Z_NO_FLUSH); + size_t made = (size_t)avail - (size_t)z.avail_out; + + if (made > buf_cap - produced) { + rc = MZ_ERR_FORMAT; + break; + } + produced += made; + + if (zr == Z_STREAM_END) break; + if (zr == Z_OK) { + if (made == 0 && z.avail_in == in_before) { + if (++stalls > 8 || (consumed >= comp_size && z.avail_in == 0)) { + rc = MZ_ERR_TRUNCATED; + break; + } + } else { + stalls = 0; + } + continue; + } + if (zr == Z_BUF_ERROR && z.avail_in == 0 && consumed >= comp_size) { + rc = MZ_ERR_TRUNCATED; + break; + } + rc = MZ_ERR_FORMAT; + break; + } + + inflateEnd(&z); + free(in); + if (rc == MZ_OK) *out_len = produced; + return rc; +} + +int mz_read(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size, + size_t *out_size) +{ + if (out_size) *out_size = 0; + if (!z || !z->fp || !e) return MZ_ERR_ARG; + if (!buf && buf_size > 0) return MZ_ERR_ARG; + if (e->flags & 1u) return MZ_ERR_UNSUPPORTED; + if (e->method != 0 && e->method != 8) return MZ_ERR_UNSUPPORTED; + if (e->uncomp_size > (uint64_t)SIZE_MAX) return MZ_ERR_UNSUPPORTED; + + size_t need = (size_t)e->uncomp_size; + if (need > buf_size) return MZ_ERR_BUF; + + uint8_t lh[MZ_LOCAL_MIN]; + int rc = read_at(z->fp, e->local_offset, lh, sizeof lh); + if (rc != MZ_OK) return rc; + if (rd32(lh) != MZ_SIG_LOCAL) return MZ_ERR_FORMAT; + if (rd16(lh + 8) != e->method) return MZ_ERR_FORMAT; + + uint64_t data_off = 0; + uint64_t data_end = 0; + if (!u64_add(e->local_offset, MZ_LOCAL_MIN, &data_off) || + !u64_add(data_off, rd16(lh + 26), &data_off) || + !u64_add(data_off, rd16(lh + 28), &data_off) || + !u64_add(data_off, e->comp_size, &data_end) || + data_end > z->file_size) { + return MZ_ERR_TRUNCATED; + } + + if (e->method == 0) { + if (e->comp_size != e->uncomp_size) return MZ_ERR_FORMAT; + if (need > 0) { + rc = read_at(z->fp, data_off, buf, need); + if (rc != MZ_OK) return rc; + } + if (crc_buf((const uint8_t *)buf, need) != e->crc) return MZ_ERR_CRC; + } else { + size_t made = 0; + rc = inflate_entry(z->fp, data_off, e->comp_size, (uint8_t *)buf, need, &made); + if (rc != MZ_OK) return rc; + if (made != need) return MZ_ERR_FORMAT; + if (crc_buf((const uint8_t *)buf, made) != e->crc) return MZ_ERR_CRC; + } + + if (out_size) *out_size = need; + return MZ_OK; +} + +int mz_read_alloc(mz_archive *z, const mz_entry *e, void **out_buf, + size_t *out_size) +{ + if (!out_buf || !out_size) return MZ_ERR_ARG; + *out_buf = NULL; + *out_size = 0; + if (!z || !e) return MZ_ERR_ARG; + if (e->uncomp_size > (uint64_t)SIZE_MAX) return MZ_ERR_UNSUPPORTED; + + size_t need = (size_t)e->uncomp_size; + uint8_t *p = malloc(need ? need : 1); + if (!p) return MZ_ERR_MEM; + + size_t n = 0; + int rc = mz_read(z, e, p, need, &n); + if (rc != MZ_OK) { + free(p); + return rc; + } + *out_buf = p; + *out_size = n; + return MZ_OK; +} + +const char *mz_strerror(int err) +{ + switch (err) { + case MZ_OK: return "ok"; + case MZ_ERR_IO: return "I/O error"; + case MZ_ERR_FORMAT: return "malformed archive"; + case MZ_ERR_TRUNCATED: return "archive truncated or offset out of bounds"; + case MZ_ERR_UNSUPPORTED: return "unsupported zip feature"; + case MZ_ERR_BUF: return "output buffer too small"; + case MZ_ERR_MEM: return "out of memory"; + case MZ_ERR_ARG: return "invalid argument"; + case MZ_ERR_CRC: return "CRC mismatch"; + default: return "unknown error"; + } +} diff --git a/tuku/logic/zip/zip.h b/tuku/logic/zip/zip.h new file mode 100644 index 0000000..845884e --- /dev/null +++ b/tuku/logic/zip/zip.h @@ -0,0 +1,86 @@ +/* zip.h — 纯 C zip 容器读取器(只依赖 libz 的 inflate/crc32 + stdio)。 + * + * 定位:tuku 漫画阅读器的 CBZ/zip 容器层——开包、列目录、按需解压单条目。 + * 不依赖 meow_ui.h / SDK / tuku 其它逻辑,宿主可裸测(见 tuku/test/test_zip.c)。 + * + * 用法: + * mz_archive *z = NULL; + * int rc = mz_open("book.cbz", &z); + * if (rc != MZ_OK) { fprintf(stderr, "%s\n", mz_strerror(rc)); return; } + * for (int i = 0; i < mz_count(z); i++) { + * const mz_entry *e = mz_entry_at(z, i); + * if (mz_entry_is_dir(e) || !is_image_name(e->name)) continue; + * size_t n = 0; + * void *px = NULL; + * if (mz_read_alloc(z, e, &px, &n) == MZ_OK) { decode(px, n); free(px); } + * } + * mz_close(z); + * + * 线程约束:单个 mz_archive 不是线程安全的(共享 FILE* 位置与条目表只读); + * 多线程请各自 mz_open 自己的 archive,或由调用方加锁。模块内无全局可变状态。 + * + * 内存:mz_open 只把 central directory 的条目元数据(条目表 + 名字副本)读进内存, + * 不展开条目数据。mz_read 解压到调用方缓冲(缓冲需 >= 该条目 uncomp_size); + * mz_read_alloc 只为单个条目分配一次。 + * + * 安全:所有偏移/长度都按文件实际大小校验;越界、截断、坏 magic、坏 CRC 一律 + * 返回错误码而不崩溃。条目名按原始字节透传(通常是 UTF-8,不做规范化/解码)。 + * 名字可能是 ".."、绝对路径或反斜杠——调用方若把它落到文件系统,必须自行拒绝 + * 越界路径。加密、zip64、分卷压缩包明确返回 MZ_ERR_UNSUPPORTED。 + * + * 错误码均为负值;MZ_OK 为 0。mz_strerror 对所有已知码返回静态字符串。 + */ +#ifndef TUKU_LOGIC_ZIP_ZIP_H +#define TUKU_LOGIC_ZIP_ZIP_H + +#include +#include + +enum { + MZ_OK = 0, + MZ_ERR_IO = -1, /* 文件读写失败(打开/seek/read) */ + MZ_ERR_FORMAT = -2, /* 结构非法:坏 magic、central directory 不一致、流损坏 */ + MZ_ERR_TRUNCATED = -3, /* 声明的偏移/长度越过文件尾(含局部头越界) */ + MZ_ERR_UNSUPPORTED = -4, /* 加密 / zip64 / 分卷 / 非 stored-deflate 压缩方式 */ + MZ_ERR_BUF = -5, /* 调用方缓冲小于条目 uncomp_size */ + MZ_ERR_MEM = -6, /* 内存分配失败 */ + MZ_ERR_ARG = -7, /* 空指针 / 越界 index 等调用错误 */ + MZ_ERR_CRC = -8 /* 解压结果 CRC-32 与 central directory 不符 */ +}; + +typedef struct mz_entry { + const char *name; /* NUL 结尾;内容为 zip 内原始字节,生命周期同 archive */ + uint32_t name_len; /* 原始名字字节数(不含结尾 NUL,name 中可能有内嵌 NUL) */ + uint16_t method; /* 0=stored, 8=deflate;其它值在 mz_read 时返回 UNSUPPORTED */ + uint16_t flags; /* zip general purpose flags(bit0=加密,bit11=UTF-8 名) */ + uint32_t crc; /* 条目内容 CRC-32 */ + uint64_t comp_size; /* 压缩后字节数(archive 内) */ + uint64_t uncomp_size; /* 原始字节数 */ + uint64_t local_offset; /* local file header 在 archive 内的偏移 */ +} mz_entry; + +typedef struct mz_archive mz_archive; + +/* 打开 zip。成功时 *out 为新 archive(mz_close 释放);失败时 *out == NULL。 */ +int mz_open(const char *path, mz_archive **out); +void mz_close(mz_archive *z); + +int mz_count(const mz_archive *z); +const mz_entry *mz_entry_at(const mz_archive *z, int index); /* 越界返回 NULL */ +const mz_entry *mz_find(const mz_archive *z, const char *name); /* 精确 strcmp,找不到 NULL */ + +/* 目录条目(名字以 '/' 结尾)。注意 zip 常见但没有强制要求目录条目存在。 */ +int mz_entry_is_dir(const mz_entry *e); + +/* 解压单条目到 buf。buf_size 必须 >= e->uncomp_size,否则 MZ_ERR_BUF。 + * 成功时 *out_size = uncomp_size(可为 0)。out_size 可为 NULL。 + * 校验 CRC 与声明尺寸;失败时 buf 内容不可用。 */ +int mz_read(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size, size_t *out_size); + +/* mz_read 的便捷版:malloc 恰好 uncomp_size 字节(0 长度条目分配 1 字节)。 + * 成功时 *out_buf 由调用方 free。 */ +int mz_read_alloc(mz_archive *z, const mz_entry *e, void **out_buf, size_t *out_size); + +const char *mz_strerror(int err); + +#endif /* TUKU_LOGIC_ZIP_ZIP_H */ diff --git a/tuku/test/make_zip_samples.py b/tuku/test/make_zip_samples.py new file mode 100644 index 0000000..ccf1885 --- /dev/null +++ b/tuku/test/make_zip_samples.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""生成 tuku zip 读取器的宿主测试样例(真实 zipfile + 定点字节修补)。 + +用法: python3 make_zip_samples.py + +生成: + 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("") + 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(" +#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; +}