feat(tuku): zip 容器读取器核心(纯 C + zlib,central directory 校验/单条目按需解压)

This commit is contained in:
AskaEth committed 2026-11-05 11:39:34 +08:00
1 parent 6caf2bc6e6
commit fbd4db3aec
5 files changed
+1318

No files matched your search

+493
View File
@@ -0,0 +1,493 @@
/* zip.c — zip 容器读取器实现,见 zip.h 的用法/线程/安全说明。 */
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include "zip.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>
#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";
}
}
+86
View File
@@ -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 <stddef.h>
#include <stdint.h>
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 */
+160
View File
@@ -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()
+9
View File
@@ -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
+570
View File
@@ -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;
}