/* logic/reader/archive_source.c — 档案册(CBZ/zip 当一本书):页源契约的第二种实现。 * * 契约、条目过滤与缓存纪律见 page_source.h / page_cache.h。实现要点: * - 页表在 init 时一次建好(mz_open 只读 central directory;只看名字,不解压); * - open(i) 才解压单条目:mz_read_alloc → page_cache 落盘 → 注入的解码回调按路径解码。 * 因此翻页/条漫两套消费代码与目录册完全一致(meta 走原始像素尺寸,open 走解码); * - meta(i) 按需解压该页到内存只读图片头(不落缓存),结果缓存在 meta_w/meta_h; * - 单个 mr_zip_book 的 mz_read* 由自带的互斥量串行化(解码线程与主线程 meta 会碰面)。 * * 线程:count/page_name 只读页表,可任意线程;open 可由解码线程调用;meta 按 M6 * 用法在主线程调用(条漫排版/当前页几何)。两个 mr_zip_book 实例互不影响, * 解压缓存(page_cache)是进程级共享的。 */ #include "logic/reader/page_source.h" #include "logic/reader/page_cache.h" #include "logic/zip/zip.h" #include #include #include #include #include /* ============================================================ */ /* 条目过滤:图片后缀 + 路径安全 + 非隐藏 + 非目录 */ /* ============================================================ */ static int has_control(const char *name, unsigned len) { for (unsigned i = 0; i < len; i++) { unsigned char c = (unsigned char)name[i]; if (c < 0x20 || c == 0x7F) return 1; } return 0; } /* 逐段检查:拒绝 ".." 段与隐藏段("._resource" 之类不参与成册)。 */ static int components_safe(const char *name) { const char *p = name; while (*p) { const char *end = strchr(p, '/'); size_t n = end ? (size_t)(end - p) : strlen(p); if (n == 2 && p[0] == '.' && p[1] == '.') return 0; if (n > 0 && p[0] == '.') return 0; if (!end) break; p = end + 1; } return 1; } int mr_zip_entry_is_page(const char *name, unsigned name_len) { if (!name || name_len == 0 || name_len >= MR_ZIP_MAX_ENTRY_NAME) return 0; if (strlen(name) != name_len) return 0; /* 内嵌 NUL:拒绝 */ if (name[0] == '/' || name[0] == '\\') return 0; /* 绝对路径 */ if (name_len >= 2 && name[1] == ':') return 0; /* Windows 盘符 */ if (strchr(name, '\\')) return 0; /* 反斜杠分隔符:一律拒绝 */ if (has_control(name, name_len)) return 0; if (name[name_len - 1] == '/') return 0; /* 目录条目 */ if (!components_safe(name)) return 0; return mr_path_is_image(name); } /* 页表排序:跨子目录按完整条目名自然序(中文按 UTF-8 字节序,数字段按数值)。 */ typedef struct page_ref { const char *name; int eidx; } page_ref; static int page_ref_cmp(const void *pa, const void *pb) { const page_ref *a = (const page_ref *)pa; const page_ref *b = (const page_ref *)pb; int c = mr_natural_cmp(a->name, b->name); if (c != 0) return c; return a->eidx < b->eidx ? -1 : (a->eidx > b->eidx ? 1 : 0); } /* ============================================================ */ /* 生命周期 */ /* ============================================================ */ static int entry_is_page(const mz_entry *e) { if (!e || mz_entry_is_dir(e)) return 0; if (e->flags & 0x0001u) return 0; /* 加密条目:读不了,不列 */ if (!mr_zip_entry_is_page(e->name, e->name_len)) return 0; if (e->uncomp_size == 0) return 0; if (e->uncomp_size > (uint64_t)MR_ZIP_PAGE_MAX_BYTES) return 0; /* zip 炸弹 */ return 1; } void mr_zip_book_free(mr_zip_book *zb) { if (!zb) return; if (zb->arch) { mz_close((mz_archive *)zb->arch); zb->arch = NULL; } if (zb->lock) { pthread_mutex_destroy((pthread_mutex_t *)zb->lock); free(zb->lock); zb->lock = NULL; } free(zb->order); zb->order = NULL; free(zb->meta_w); zb->meta_w = NULL; free(zb->meta_h); zb->meta_h = NULL; zb->count = 0; } int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim, mr_book_load_fn load, mr_book_unload_fn unload, void *ud) { if (!zb) return -1; memset(zb, 0, sizeof *zb); if (!path || !path[0]) return -1; if (strlen(path) >= sizeof zb->path) return -1; if (maxdim <= 0) maxdim = 720; mz_archive *z = NULL; if (mz_open(path, &z) != MZ_OK || !z) return -1; pthread_mutex_t *lk = (pthread_mutex_t *)malloc(sizeof *lk); if (!lk) { mz_close(z); return -1; } pthread_mutex_init(lk, NULL); snprintf(zb->path, sizeof zb->path, "%s", path); zb->arch = z; zb->lock = lk; zb->maxdim = maxdim; zb->load = load; zb->unload = unload; zb->ud = ud; struct stat st; if (stat(path, &st) == 0) { zb->mtime = (long long)st.st_mtime; zb->size = (long long)st.st_size; } int n = mz_count(z); page_ref *refs = NULL; int nref = 0, cap = 0; for (int i = 0; i < n && nref < MR_BOOK_MAX_PAGES; i++) { const mz_entry *e = mz_entry_at(z, i); if (!entry_is_page(e)) continue; if (nref >= cap) { int nc = cap ? cap * 2 : 32; page_ref *nr = (page_ref *)realloc(refs, (size_t)nc * sizeof *nr); if (!nr) { free(refs); mr_zip_book_free(zb); return -1; } refs = nr; cap = nc; } refs[nref].name = e->name; refs[nref].eidx = i; nref++; } if (nref > 1) qsort(refs, (size_t)nref, sizeof *refs, page_ref_cmp); if (nref > 0) { zb->order = (int *)malloc((size_t)nref * sizeof *zb->order); if (!zb->order) { free(refs); mr_zip_book_free(zb); return -1; } for (int i = 0; i < nref; i++) zb->order[i] = refs[i].eidx; } free(refs); zb->count = nref; return nref; } /* ============================================================ */ /* 页 → 缓存文件 */ /* ============================================================ */ static const mz_entry *page_entry(mr_zip_book *zb, int index) { if (!zb || !zb->arch || !zb->order) return NULL; if (index < 0 || index >= zb->count) return NULL; return mz_entry_at((mz_archive *)zb->arch, zb->order[index]); } int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz) { if (!out || outsz <= 0) return -1; out[0] = '\0'; const mz_entry *e = page_entry(zb, index); if (!e) return -1; char path[MR_CACHE_FILE_MAX]; if (mr_cache_path(zb->path, zb->mtime, zb->size, e->name, index, path, (int)sizeof path) != 0) return -1; if (mr_cache_touch(path)) { snprintf(out, (size_t)outsz, "%s", path); return 0; } pthread_mutex_lock((pthread_mutex_t *)zb->lock); void *buf = NULL; size_t len = 0; int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len); pthread_mutex_unlock((pthread_mutex_t *)zb->lock); if (rc != MZ_OK || !buf) { free(buf); return -1; } int ok = mr_cache_store(path, buf, len); free(buf); if (!ok) return -1; snprintf(out, (size_t)outsz, "%s", path); return 0; } void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz) { if (!out || outsz <= 0) return; out[0] = '\0'; const mz_entry *e = page_entry((mr_zip_book *)zb, index); if (!e) return; snprintf(out, (size_t)outsz, "%s", e->name); } /* ============================================================ */ /* 书库辅助:数页 / 取封面 */ /* ============================================================ */ int mr_zip_count_pages(const char *path) { if (!path || !path[0]) return 0; mz_archive *z = NULL; if (mz_open(path, &z) != MZ_OK || !z) return 0; int n = mz_count(z), pages = 0; for (int i = 0; i < n && pages < MR_BOOK_MAX_PAGES; i++) if (entry_is_page(mz_entry_at(z, i))) pages++; mz_close(z); return pages; } int mr_zip_cover_path(const char *path, char *out, int outsz) { if (!out || outsz <= 0) return 0; out[0] = '\0'; if (!path || !path[0]) return 0; mr_zip_book zb; int n = mr_zip_book_init(&zb, path, 0, NULL, NULL, NULL); int ok = 0; if (n > 0) ok = (mr_zip_book_page_path(&zb, 0, out, outsz) == 0); mr_zip_book_free(&zb); return ok; } /* ============================================================ */ /* mr_book_ops 适配器(档案册) */ /* ============================================================ */ static int zip_ops_count(void *ctx) { mr_zip_book *zb = (mr_zip_book *)ctx; return zb ? zb->count : 0; } /* meta 按需解压头探测;结果缓存,每页只解压一次。主线程调用(M6 用法)。 */ static int zip_ops_meta(void *ctx, int index, int *w, int *h) { mr_zip_book *zb = (mr_zip_book *)ctx; if (w) *w = 0; if (h) *h = 0; if (!zb || index < 0 || index >= zb->count) return 0; if (!zb->meta_w) { int *mw = (int *)calloc((size_t)zb->count, sizeof *mw); int *mh = (int *)calloc((size_t)zb->count, sizeof *mh); if (!mw || !mh) { free(mw); free(mh); return 0; } zb->meta_w = mw; zb->meta_h = mh; } if (zb->meta_w[index] > 0 && zb->meta_h[index] > 0) { if (w) *w = zb->meta_w[index]; if (h) *h = zb->meta_h[index]; return 1; } const mz_entry *e = page_entry(zb, index); if (!e) return 0; pthread_mutex_lock((pthread_mutex_t *)zb->lock); void *buf = NULL; size_t len = 0; int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len); pthread_mutex_unlock((pthread_mutex_t *)zb->lock); if (rc != MZ_OK || !buf) { free(buf); return 0; } int pw = 0, ph = 0; int known = mr_probe_size_mem(buf, len, &pw, &ph); free(buf); if (!known) return 0; zb->meta_w[index] = pw; zb->meta_h[index] = ph; if (w) *w = pw; if (h) *h = ph; return 1; } static meow_img *zip_ops_open(void *ctx, int index) { mr_zip_book *zb = (mr_zip_book *)ctx; if (!zb || !zb->load) return NULL; char path[MR_CACHE_FILE_MAX]; if (mr_zip_book_page_path(zb, index, path, (int)sizeof path) != 0) return NULL; return zb->load(path, zb->ud, zb->maxdim); } static void zip_ops_close(void *ctx, meow_img *img) { mr_zip_book *zb = (mr_zip_book *)ctx; if (zb && zb->unload && img) zb->unload(img); } static void zip_ops_destroy(void *ctx) { mr_zip_book *zb = (mr_zip_book *)ctx; if (zb) mr_zip_book_free(zb); } static const mr_book_ops g_zip_ops = { zip_ops_count, zip_ops_meta, zip_ops_open, zip_ops_close, zip_ops_destroy, }; void mr_zip_book_bind(mr_book *b, mr_zip_book *zb) { if (!b) return; b->ops = zb ? &g_zip_ops : NULL; b->ctx = zb; } /* ============================================================ */ /* 统一开册 */ /* ============================================================ */ int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim, mr_book_load_fn load, mr_book_unload_fn unload, void *ud) { if (b) { b->ops = NULL; b->ctx = NULL; } if (!b || !h) return -1; memset(h, 0, sizeof *h); if (!path || !path[0]) return -1; int n; /* 目录名也可能以 .cbz 结尾(罕见):只有普通文件才当档案册。 */ int is_archive = mr_path_is_archive(path); if (is_archive) { struct stat st; if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) is_archive = 0; } if (is_archive) { n = mr_zip_book_init(&h->u.zip, path, maxdim, load, unload, ud); if (n < 0) return -1; h->kind = MR_BOOK_KIND_ZIP; if (n > 0) mr_zip_book_bind(b, &h->u.zip); } else { n = mr_dir_book_init(&h->u.dir, path, maxdim, load, unload, ud); if (n < 0) return -1; h->kind = MR_BOOK_KIND_DIR; if (n > 0) mr_dir_book_bind(b, &h->u.dir); } return n; } void mr_book_close_auto(mr_book *b, mr_book_handle *h) { if (b) mr_book_destroy(b); if (!h) return; if (h->kind == MR_BOOK_KIND_ZIP) mr_zip_book_free(&h->u.zip); else if (h->kind == MR_BOOK_KIND_DIR) mr_dir_book_free(&h->u.dir); memset(h, 0, sizeof *h); }