reader(R2): EPUB/FB2/MOBI/CBZ 加载器落地
- rdsrc 统一文档源抽象:TXT/FB2/EPUB/MOBI/CBZ 都产出「UTF-8 段序列+目录+元数据+图片」, 与 R1 rdtxt 段契约一致;book.c 改喂 rd_src(文本)或图页(CBZ)。 - EPUB:miniz 解 ZIP → 自写 rdxml 回调解析 OPF(spine/manifest)+NCX/nav → 正文 XHTML 用 rdhtml 逐行粗解析(坏标签/未闭合/实体容错,不建 DOM),章节按 RDSRC_SEG_MAX 分片。 - FB2:rdxml 严格解析抽 section 段落 + base64 内嵌图;段题进正文+目录。 - MOBI/AZW3:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml_opt→ flow/resources),HTML 走 rdhtml 同款粗解析;LGPL-3.0 合规见 NOTICE(版权+全文+锁定 commit)。 - CBZ:miniz 解 ZIP → 自然序图片序列,rdimg(stb_image) 解码+降采样,阅读页图页模式。 - 目录显示与跳转(EPUB NCX/FB2 section/MOBI 标题);书架开放四格式;进度按 (段,字符偏移)(TXT/FB2/EPUB/MOBI)/页号(CBZ)。 - 宿主单测 loaders_smoke(四格式抽取/目录/图片/base64/页序)+ qemu-mipsel formats_smoke/book_smoke;R1 test_reader_core 58/0 不回归。 - build_reader.sh 集成 miniz/libmobi/stb/zlib 头;NOTICE 入仓;交叉 -Wall -Wextra 0 warning。 新 reader md5: e29036599300eb277b1c960a75b65dbf
This commit is contained in:
1 parent
10690bc348
commit
0294553096
27 files changed
+3440
-193
No files matched your search
@@ -0,0 +1,128 @@
|
||||
/* rdcbz.c — CBZ loader:miniz 解 ZIP → 图片序列(自然序)→ rd_src_page 逐页原始数据(调用方
|
||||
* stb_image 解码+降采样)。纯图阅读,不走排版器。无正文/目录元数据(标题=文件名)。
|
||||
*/
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdzip.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static void stl(char *d, int sz, const char *s)
|
||||
{
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
if (sz > 0) { if (n >= (size_t)sz) n = (size_t)sz - 1; memcpy(d, s, n); d[n] = 0; }
|
||||
}
|
||||
typedef struct {
|
||||
rdzip *zip;
|
||||
char **names; int *idx; int n;
|
||||
int cur; /* next 游标(无正文,恒 EOF) */
|
||||
char base[128];
|
||||
} cbz_impl;
|
||||
|
||||
static int has_img_ext(const char *name)
|
||||
{
|
||||
const char *d = strrchr(name, '.');
|
||||
if (!d) return 0;
|
||||
if (strcasecmp(d, ".jpg") == 0 || strcasecmp(d, ".jpeg") == 0 ||
|
||||
strcasecmp(d, ".png") == 0 || strcasecmp(d, ".gif") == 0 ||
|
||||
strcasecmp(d, ".bmp") == 0 || strcasecmp(d, ".webp") == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
/* 自然序:数字段按数值比较,其余按字节序 */
|
||||
static int natcmp(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *b) {
|
||||
if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) {
|
||||
/* 跳前导 0 */
|
||||
while (*a == '0') a++;
|
||||
while (*b == '0') b++;
|
||||
const char *sa = a, *sb = b;
|
||||
while (isdigit((unsigned char)*sa)) sa++;
|
||||
while (isdigit((unsigned char)*sb)) sb++;
|
||||
size_t la = (size_t)(sa - a), lb = (size_t)(sb - b);
|
||||
if (la != lb) return la < lb ? -1 : 1;
|
||||
int c = memcmp(a, b, la);
|
||||
if (c) return c;
|
||||
a = sa; b = sb;
|
||||
} else {
|
||||
unsigned char ca = (unsigned char)*a, cb = (unsigned char)*b;
|
||||
if (ca != cb) return ca < cb ? -1 : 1;
|
||||
a++; b++;
|
||||
}
|
||||
}
|
||||
return (*a ? 1 : (*b ? -1 : 0));
|
||||
}
|
||||
|
||||
static void cbz_free(rd_src *s)
|
||||
{
|
||||
cbz_impl *m = (cbz_impl *)s->impl;
|
||||
if (!m) return;
|
||||
if (m->zip) rdzip_close(m->zip);
|
||||
for (int i = 0; i < m->n; i++) free(m->names[i]);
|
||||
free(m->names);
|
||||
free(m->idx);
|
||||
free(m);
|
||||
s->impl = NULL;
|
||||
}
|
||||
static int cbz_reset(rd_src *s) { ((cbz_impl *)s->impl)->cur = 0; return 0; }
|
||||
static int cbz_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len) { (void)s;(void)buf;(void)cap;(void)len; return 1; }
|
||||
static int cbz_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len) { (void)s;(void)idx;(void)buf;(void)cap;(void)len; return 1; }
|
||||
static int cbz_toc_n(rd_src *s) { (void)s; return 0; }
|
||||
static int cbz_toc(rd_src *s, int i, rd_toc_entry *t) { (void)s;(void)i;(void)t; return -1; }
|
||||
static int cbz_img(rd_src *s, const char *ref, uint8_t **data, size_t *size) { (void)s;(void)ref;(void)data;(void)size; return -1; }
|
||||
static int cbz_pages(rd_src *s) { return ((cbz_impl *)s->impl)->n; }
|
||||
static int cbz_page(rd_src *s, int i, uint8_t **data, size_t *size)
|
||||
{
|
||||
cbz_impl *m = (cbz_impl *)s->impl;
|
||||
if (!m || i < 0 || i >= m->n || !data || !size) return -1;
|
||||
return rdzip_read(m->zip, m->idx[i], data, size);
|
||||
}
|
||||
|
||||
static int cbz_open(rd_src *s)
|
||||
{
|
||||
cbz_impl *m = (cbz_impl *)calloc(1, sizeof *m);
|
||||
if (!m) return -6;
|
||||
s->impl = m;
|
||||
const char *sl = strrchr(s->path, '/');
|
||||
if (sl) { strncpy(m->base, sl + 1, sizeof m->base - 1); m->base[sizeof m->base - 1] = 0; }
|
||||
else strncpy(m->base, s->path, sizeof m->base - 1), m->base[sizeof m->base - 1] = 0;
|
||||
char *dot = strrchr(m->base, '.'); if (dot) *dot = 0;
|
||||
stl(s->title, sizeof s->title, m->base);
|
||||
|
||||
if (rdzip_open(&m->zip, s->path) != 0) { cbz_free(s); return -2; }
|
||||
int cnt = rdzip_count(m->zip);
|
||||
if (cnt <= 0) { cbz_free(s); return -3; }
|
||||
m->names = (char **)calloc((size_t)cnt, sizeof(char *));
|
||||
m->idx = (int *)calloc((size_t)cnt, sizeof(int));
|
||||
if (!m->names || !m->idx) { cbz_free(s); return -6; }
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
char nm[512];
|
||||
if (rdzip_name(m->zip, i, nm, sizeof nm) != 0) continue;
|
||||
if (nm[0] && nm[strlen(nm)-1] == '/') continue; /* 目录项跳过 */
|
||||
if (!has_img_ext(nm)) continue;
|
||||
m->names[m->n] = strdup(nm);
|
||||
m->idx[m->n++] = i;
|
||||
}
|
||||
/* 自然序(按名字) */
|
||||
for (int i = 1; i < m->n; i++) {
|
||||
char *nvi = m->names[i]; int ivi = m->idx[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && natcmp(m->names[j], nvi) > 0) {
|
||||
m->names[j+1] = m->names[j]; m->idx[j+1] = m->idx[j]; j--;
|
||||
}
|
||||
m->names[j+1] = nvi; m->idx[j+1] = ivi;
|
||||
}
|
||||
if (m->n == 0) { cbz_free(s); return -4; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cbz_probe(const uint8_t *head, size_t n)
|
||||
{
|
||||
return (n >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 3 && head[3] == 4);
|
||||
}
|
||||
static const rd_src_vt cbz_vt = {
|
||||
cbz_free, cbz_reset, cbz_next, cbz_seg,
|
||||
cbz_toc_n, cbz_toc, cbz_img, cbz_pages, cbz_page,
|
||||
};
|
||||
const rd_src_loader rd_cbz_loader = { RD_FMT_CBZ, ".cbz", cbz_probe, cbz_open, &cbz_vt };
|
||||
@@ -0,0 +1,531 @@
|
||||
/* rdepub.c — EPUB loader:miniz 解 ZIP → rdxml 解析 OPF(spine/manifest)+NCX/nav(目录) →
|
||||
* 正文 XHTML 用 rdhtml 逐行粗解析(不喂 pugixml)→ 产出与 rdtxt 相同的 UTF-8 段契约。
|
||||
*
|
||||
* 内存纪律:ZIP 文件句柄驻留(磁盘),章节文本按需抽/粗解析/用完即释;只保留「段表 + 目录 +
|
||||
* 图片索引」轻量结构 → EPUB 大书不建全 DOM(对齐 READER_ENGINE_NOTES §4)。段表在 next() 期间
|
||||
* lazy 填充,seg(idx) 按段下标重解析章节切片,逐字节确定性。
|
||||
*/
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdzip.h"
|
||||
#include "rdxml.h"
|
||||
#include "rdhtml.h"
|
||||
#include "rdsb.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <strings.h>
|
||||
|
||||
/* 有界拷贝(truncate + NUL 终止;杜绝 -Wstringop-truncation) */
|
||||
static void stl(char *d, int sz, const char *s)
|
||||
{
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
if ((size_t)sz > 0) {
|
||||
if (n >= (size_t)sz) n = (size_t)sz - 1;
|
||||
memcpy(d, s, n); d[n] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 小工具 ---------------- */
|
||||
static void path_join(const char *base, const char *rel, char *out, int outsz)
|
||||
{
|
||||
char a[400]; stl(a, sizeof a, rel);
|
||||
if (a[0] != '/') {
|
||||
char b[384]; stl(b, sizeof b, base);
|
||||
size_t bl = strlen(b);
|
||||
if (bl && bl + 1 < sizeof b) { b[bl] = '/'; b[bl + 1] = 0; }
|
||||
size_t sl = strlen(a);
|
||||
if (bl + sl + 1 < sizeof b) memcpy(b + bl + 1, a, sl + 1);
|
||||
else if (sizeof b > 1) b[sizeof b - 1] = 0;
|
||||
stl(a, sizeof a, b);
|
||||
}
|
||||
char *stack[96]; int nstack = 0;
|
||||
char *save = NULL;
|
||||
for (char *tok = strtok_r(a, "/", &save); tok; tok = strtok_r(NULL, "/", &save)) {
|
||||
if (strcmp(tok, ".") == 0) continue;
|
||||
if (strcmp(tok, "..") == 0) { if (nstack > 0) nstack--; continue; }
|
||||
if (nstack < 96) stack[nstack++] = tok;
|
||||
}
|
||||
size_t o = 0; out[0] = 0;
|
||||
for (int i = 0; i < nstack; i++) {
|
||||
size_t L = strlen(stack[i]);
|
||||
if (o + L + 2 >= (size_t)outsz) break;
|
||||
if (o) out[o++] = '/';
|
||||
memcpy(out + o, stack[i], L); o += L;
|
||||
}
|
||||
out[o] = 0;
|
||||
}
|
||||
static void path_dir(const char *p, char *out, int outsz)
|
||||
{
|
||||
if (!p) { out[0] = 0; return; }
|
||||
const char *sl = strrchr(p, '/');
|
||||
if (!sl) { out[0] = 0; return; }
|
||||
size_t n = (size_t)(sl - p);
|
||||
if (n >= (size_t)outsz) n = (size_t)outsz - 1;
|
||||
memcpy(out, p, n); out[n] = 0;
|
||||
}
|
||||
static void path_frag(const char *src, char *path, int pathsz, char *frag, int fragsz)
|
||||
{
|
||||
const char *h = strchr(src, '#');
|
||||
if (h) {
|
||||
size_t pn = (size_t)(h - src);
|
||||
if (pn >= (size_t)pathsz) pn = (size_t)pathsz - 1;
|
||||
memcpy(path, src, pn); path[pn] = 0;
|
||||
stl(frag, fragsz, h + 1);
|
||||
} else {
|
||||
stl(path, pathsz, src);
|
||||
frag[0] = 0;
|
||||
}
|
||||
}
|
||||
static int attr(const rdxml_attr *a, int na, const char *name, char *out, int sz)
|
||||
{
|
||||
for (int i = 0; i < na; i++)
|
||||
if (strcmp(a[i].name, name) == 0) { stl(out, sz, a[i].value); return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------- container.xml ---------------- */
|
||||
static void cont_open(const char *n, const rdxml_attr *a, int na, void *ctx)
|
||||
{
|
||||
char *opf = (char *)ctx;
|
||||
if (opf[0]) return;
|
||||
if (strcmp(n, "rootfile") == 0) attr(a, na, "full-path", opf, 320);
|
||||
}
|
||||
static const rdxml_handler cont_h = { cont_open, NULL, NULL };
|
||||
|
||||
/* ---------------- OPF ---------------- */
|
||||
typedef struct { char *id, *href, *mime; } opf_item;
|
||||
typedef struct {
|
||||
int n_manifest; opf_item items[128];
|
||||
int n_spine; char *spine_idref[512];
|
||||
char ncx_src[320], nav_id[64], title[160], author[160];
|
||||
} opf_t;
|
||||
static void opf_free(opf_t *o)
|
||||
{
|
||||
for (int i = 0; i < o->n_manifest; i++) { free(o->items[i].id); free(o->items[i].href); free(o->items[i].mime); }
|
||||
for (int i = 0; i < o->n_spine; i++) free(o->spine_idref[i]);
|
||||
memset(o, 0, sizeof *o);
|
||||
}
|
||||
typedef struct { opf_t *o; int in_manifest, in_spine, cur_label; char buf[256]; int nbuf; } opfctx;
|
||||
static void opf_open(const char *n, const rdxml_attr *a, int na, void *ctx)
|
||||
{
|
||||
opfctx *c = (opfctx *)ctx; opf_t *o = c->o;
|
||||
if (strcmp(n, "manifest") == 0) { c->in_manifest = 1; return; }
|
||||
if (strcmp(n, "spine") == 0) { c->in_spine = 1; attr(a, na, "toc", o->ncx_src, sizeof o->ncx_src); return; }
|
||||
if (strcmp(n, "metadata") == 0) return;
|
||||
if (c->in_manifest && strcmp(n, "item") == 0) {
|
||||
char id[128]={0}, href[320]={0}, mime[64]={0};
|
||||
attr(a, na, "id", id, sizeof id); attr(a, na, "href", href, sizeof href); attr(a, na, "media-type", mime, sizeof mime);
|
||||
if (id[0] && href[0] && o->n_manifest < 128) {
|
||||
opf_item *it = &o->items[o->n_manifest++];
|
||||
it->id = strdup(id); it->href = strdup(href); it->mime = strdup(mime);
|
||||
}
|
||||
if (id[0] && (strcmp(id, "nav") == 0)) stl(o->nav_id, sizeof o->nav_id, id);
|
||||
return;
|
||||
}
|
||||
if (c->in_spine && strcmp(n, "itemref") == 0) {
|
||||
char idref[128]={0}; attr(a, na, "idref", idref, sizeof idref);
|
||||
if (idref[0] && o->n_spine < 512) o->spine_idref[o->n_spine++] = strdup(idref);
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "title") == 0) { c->cur_label = 1; c->nbuf = 0; return; }
|
||||
if (strcmp(n, "creator") == 0) { c->cur_label = 2; c->nbuf = 0; return; }
|
||||
}
|
||||
static void opf_text(const char *t, size_t len, void *ctx)
|
||||
{
|
||||
opfctx *c = (opfctx *)ctx;
|
||||
if (!c->cur_label) return;
|
||||
for (size_t i = 0; i < len && c->nbuf < (int)sizeof c->buf - 1; i++) {
|
||||
uint8_t b = (uint8_t)t[i];
|
||||
if (b == '\r' || b == '\n' || b == '\t') continue;
|
||||
c->buf[c->nbuf++] = (char)b;
|
||||
}
|
||||
}
|
||||
static void opf_close(const char *n, void *ctx)
|
||||
{
|
||||
opfctx *c = (opfctx *)ctx; opf_t *o = c->o;
|
||||
if (strcmp(n, "manifest") == 0) { c->in_manifest = 0; return; }
|
||||
if (strcmp(n, "spine") == 0) { c->in_spine = 0; return; }
|
||||
if (strcmp(n, "title") == 0) { c->buf[c->nbuf] = 0; stl(o->title, sizeof o->title, c->buf); c->cur_label = 0; }
|
||||
if (strcmp(n, "creator") == 0) { c->buf[c->nbuf] = 0; stl(o->author, sizeof o->author, c->buf); c->cur_label = 0; }
|
||||
}
|
||||
|
||||
/* ---------------- NCX ---------------- */
|
||||
typedef struct { char label[160]; char src[320]; } ncxent;
|
||||
typedef struct { ncxent *ents; int n, cap; ncxent stk[64]; int sp; int want_label; } ncxctx;
|
||||
static void ncx_add(ncxctx *c, const char *label, const char *src)
|
||||
{
|
||||
if (c->n >= c->cap) {
|
||||
int nc = c->cap ? c->cap * 2 : 32;
|
||||
ncxent *ne = (ncxent *)realloc(c->ents, (size_t)nc * sizeof(ncxent));
|
||||
if (!ne) return;
|
||||
c->ents = ne; c->cap = nc;
|
||||
}
|
||||
ncxent *e = &c->ents[c->n++];
|
||||
stl(e->label, sizeof e->label, label);
|
||||
stl(e->src, sizeof e->src, src);
|
||||
}
|
||||
static void ncx_open(const char *n, const rdxml_attr *a, int na, void *ctx)
|
||||
{
|
||||
ncxctx *c = (ncxctx *)ctx;
|
||||
if (strcmp(n, "navPoint") == 0) { if (c->sp < 64) { memset(&c->stk[c->sp], 0, sizeof(ncxent)); c->sp++; } return; }
|
||||
if (strcmp(n, "text") == 0) { c->want_label = 1; return; }
|
||||
if (strcmp(n, "content") == 0 && c->sp > 0) attr(a, na, "src", c->stk[c->sp - 1].src, sizeof(c->stk[c->sp - 1].src));
|
||||
}
|
||||
static void ncx_text(const char *t, size_t len, void *ctx)
|
||||
{
|
||||
ncxctx *c = (ncxctx *)ctx;
|
||||
if (!c->want_label || c->sp <= 0) return;
|
||||
ncxent *e = &c->stk[c->sp - 1];
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t b = (uint8_t)t[i];
|
||||
if (b == '\r' || b == '\n') continue;
|
||||
size_t n = strlen(e->label);
|
||||
if (n + 1 < sizeof e->label) { e->label[n] = (char)b; e->label[n + 1] = 0; }
|
||||
}
|
||||
}
|
||||
static void ncx_close(const char *n, void *ctx)
|
||||
{
|
||||
ncxctx *c = (ncxctx *)ctx;
|
||||
if (strcmp(n, "text") == 0) { c->want_label = 0; return; }
|
||||
if (strcmp(n, "navPoint") == 0 && c->sp > 0) {
|
||||
ncxent *e = &c->stk[c->sp - 1];
|
||||
if (e->src[0]) ncx_add(c, e->label[0] ? e->label : "?", e->src);
|
||||
c->sp--;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- impl ---------------- */
|
||||
#define MAXSPINE 512
|
||||
typedef struct {
|
||||
rdzip *zip;
|
||||
char opf_dir[320];
|
||||
char spine_path[MAXSPINE][320];
|
||||
int n_spine;
|
||||
int *spine_first_seg; /* 每 spine 第一个全局段下标 */
|
||||
/* 段表(next 期间 lazy 填充) */
|
||||
int *seg_spine; size_t *seg_off, *seg_len; int n_seg, cap_seg;
|
||||
/* next 状态机 */
|
||||
int next_spine;
|
||||
rdsb sb;
|
||||
size_t emit_off;
|
||||
int n_toc; ncxent *toc;
|
||||
char **img_refs; int n_img, cap_img;
|
||||
char cover[320];
|
||||
} epub_impl;
|
||||
|
||||
static void epub_free(rd_src *s)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)s->impl;
|
||||
if (!e) return;
|
||||
if (e->zip) rdzip_close(e->zip);
|
||||
free(e->spine_first_seg);
|
||||
free(e->seg_spine); free(e->seg_off); free(e->seg_len);
|
||||
free(e->toc); /* label/src 为内联数组,只释放整块 */
|
||||
for (int i = 0; i < e->n_img; i++) free(e->img_refs[i]);
|
||||
free(e->img_refs);
|
||||
rdsb_free(&e->sb);
|
||||
free(e);
|
||||
s->impl = NULL;
|
||||
}
|
||||
|
||||
static void nav_extract(const uint8_t *nd, size_t nsz, epub_impl *e);
|
||||
|
||||
typedef struct { epub_impl *e; rdsb *sb; char dir[320]; } spinectx;
|
||||
static void spine_text(const uint8_t *u, size_t len, void *ctx) { rdsb_put(((spinectx *)ctx)->sb, u, len); }
|
||||
static void spine_nl(void *ctx) { rdsb_nl(((spinectx *)ctx)->sb); }
|
||||
static void spine_img(const char *src, void *ctx)
|
||||
{
|
||||
spinectx *c = (spinectx *)ctx;
|
||||
char path[320], frag[16], abs[320];
|
||||
path_frag(src, path, sizeof path, frag, sizeof frag);
|
||||
path_join(c->dir, path, abs, sizeof abs);
|
||||
if (!abs[0]) return;
|
||||
epub_impl *e = c->e;
|
||||
if (!e->cover[0] && strstr(abs, "cover")) stl(e->cover, sizeof e->cover, abs);
|
||||
for (int i = 0; i < e->n_img; i++) if (strcmp(e->img_refs[i], abs) == 0) { rdsb_put(c->sb, (const uint8_t *)"\n[图]\n", 7); return; }
|
||||
if (e->n_img >= e->cap_img) { int nc = e->cap_img ? e->cap_img * 2 : 32; char **ni = (char **)realloc(e->img_refs, (size_t)nc * sizeof(char*)); if (ni) { e->img_refs = ni; e->cap_img = nc; } }
|
||||
if (e->n_img < e->cap_img) {
|
||||
e->img_refs[e->n_img++] = strdup(abs);
|
||||
rdsb_put(c->sb, (const uint8_t *)"\n[图]\n", 7);
|
||||
}
|
||||
}
|
||||
|
||||
static void spine_parse(epub_impl *e, int idx, rdsb *sb)
|
||||
{
|
||||
rdsb_init(sb);
|
||||
if (idx < 0 || idx >= e->n_spine) return;
|
||||
uint8_t *data = NULL; size_t sz = 0;
|
||||
if (rdzip_read_named(e->zip, e->spine_path[idx], &data, &sz) != 0) { rdsb_nl(sb); return; }
|
||||
char dir[320]; path_dir(e->spine_path[idx], dir, sizeof dir);
|
||||
spinectx c; c.e = e; c.sb = sb; memcpy(c.dir, dir, sizeof c.dir);
|
||||
rdhtml_out out = { spine_text, spine_nl, spine_img, NULL };
|
||||
if (rdhtml_parse(data, sz, &out, &c, 0) == 0) { /* 无 body → 照收全部 */
|
||||
rdsb_free(sb); rdsb_init(sb);
|
||||
rdhtml_parse(data, sz, &out, &c, 1);
|
||||
}
|
||||
while (sb->len && (sb->buf[sb->len - 1] == '\n' || sb->buf[sb->len - 1] == ' ')) sb->len--;
|
||||
free(data);
|
||||
}
|
||||
|
||||
static int add_seg(epub_impl *e, int sp, size_t off, size_t len)
|
||||
{
|
||||
if (e->n_seg >= e->cap_seg) {
|
||||
int nc = e->cap_seg ? e->cap_seg * 2 : 256;
|
||||
int *ns = (int *)realloc(e->seg_spine, (size_t)nc * sizeof(int));
|
||||
size_t *no = (size_t *)realloc(e->seg_off, (size_t)nc * sizeof(size_t));
|
||||
size_t *nl = (size_t *)realloc(e->seg_len, (size_t)nc * sizeof(size_t));
|
||||
if (!ns || !no || !nl) return -1;
|
||||
e->seg_spine = ns; e->seg_off = no; e->seg_len = nl; e->cap_seg = nc;
|
||||
}
|
||||
e->seg_spine[e->n_seg] = sp;
|
||||
e->seg_off[e->n_seg] = off;
|
||||
e->seg_len[e->n_seg] = len;
|
||||
e->n_seg++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 产生下一个「段元数据」并追加到段表;返回 0=ok(填 sp/off/len),1=eof。 */
|
||||
static int yield_segmeta(epub_impl *e, int *sp, size_t *off, size_t *len)
|
||||
{
|
||||
while (1) {
|
||||
if (e->next_spine >= e->n_spine) return 1;
|
||||
if (e->emit_off == 0) {
|
||||
spine_parse(e, e->next_spine, &e->sb);
|
||||
if (e->sb.len == 0) { e->next_spine++; continue; }
|
||||
}
|
||||
if (e->emit_off >= e->sb.len) {
|
||||
rdsb_free(&e->sb); rdsb_init(&e->sb); e->emit_off = 0;
|
||||
e->next_spine++;
|
||||
continue;
|
||||
}
|
||||
size_t co = e->emit_off, nlen = 0, nxt = 0;
|
||||
int rc = rdsb_chunk(&e->sb, &co, NULL, 0, &nlen, &nxt);
|
||||
if (rc != 0) { rdsb_free(&e->sb); rdsb_init(&e->sb); e->emit_off = 0; e->next_spine++; continue; }
|
||||
if (co == 0) e->spine_first_seg[e->next_spine] = e->n_seg; /* 该 spine 首段 */
|
||||
*sp = e->next_spine; *off = co; *len = nlen;
|
||||
add_seg(e, e->next_spine, co, nlen);
|
||||
e->emit_off = nxt;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
static void epub_build_all(epub_impl *e)
|
||||
{
|
||||
int sp; size_t off, n;
|
||||
while (yield_segmeta(e, &sp, &off, &n) == 0) { }
|
||||
}
|
||||
static int copy_segtext(epub_impl *e, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
if (idx < 0 || idx >= e->n_seg) return 1;
|
||||
int sp = e->seg_spine[idx];
|
||||
rdsb sb; spine_parse(e, sp, &sb);
|
||||
size_t co = e->seg_off[idx], n = e->seg_len[idx];
|
||||
if (co >= sb.len) { rdsb_free(&sb); return 1; }
|
||||
if (co + n > sb.len) n = sb.len - co;
|
||||
if (n > cap) n = cap;
|
||||
memcpy(buf, sb.buf + co, n); *len = n;
|
||||
rdsb_free(&sb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int epub_reset(rd_src *s)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)s->impl;
|
||||
e->next_spine = 0; e->emit_off = 0;
|
||||
rdsb_free(&e->sb); rdsb_init(&e->sb);
|
||||
return 0;
|
||||
}
|
||||
static int epub_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)s->impl;
|
||||
int sp = -1; size_t off = 0, n = 0;
|
||||
(void)sp;
|
||||
int rc = yield_segmeta(e, &sp, &off, &n);
|
||||
if (rc != 0) return rc == 1 ? 1 : -1;
|
||||
/* 直接从当前 spine 缓冲(保留在 e->sb)切片 */
|
||||
if (off >= e->sb.len || n == 0) return epub_next(s, buf, cap, len);
|
||||
size_t cl = n; if (off + cl > e->sb.len) cl = e->sb.len - off;
|
||||
if (cl > cap) cl = cap;
|
||||
memcpy(buf, e->sb.buf + off, cl); *len = cl;
|
||||
return 0;
|
||||
}
|
||||
static int epub_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)s->impl;
|
||||
while (e->n_seg <= idx) { /* 惰性建表到 idx */
|
||||
int sp; size_t off, n;
|
||||
if (yield_segmeta(e, &sp, &off, &n) != 0) break;
|
||||
}
|
||||
if (idx < 0 || idx >= e->n_seg) return 1;
|
||||
return copy_segtext(e, idx, buf, cap, len);
|
||||
}
|
||||
static int epub_toc_n(rd_src *s) { const epub_impl *e = (const epub_impl *)s->impl; return e->n_toc; }
|
||||
static int epub_toc(rd_src *s, int i, rd_toc_entry *t)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)s->impl;
|
||||
if (i < 0 || i >= e->n_toc) return -1;
|
||||
epub_build_all(e); /* 确保段表已建,spine_first_seg 有效 */
|
||||
char path[320], frag[16], abs[320];
|
||||
path_frag(e->toc[i].src, path, sizeof path, frag, sizeof frag);
|
||||
path_join(e->opf_dir, path, abs, sizeof abs);
|
||||
int sp = -1;
|
||||
for (int k = 0; k < e->n_spine; k++) if (strcmp(e->spine_path[k], abs) == 0) { sp = k; break; }
|
||||
if (sp < 0) return -1;
|
||||
stl(t->title, sizeof t->title, e->toc[i].label);
|
||||
t->seg = e->spine_first_seg[sp];
|
||||
t->ch = 0;
|
||||
return 0;
|
||||
}
|
||||
static int epub_img(rd_src *s, const char *ref, uint8_t **data, size_t *size)
|
||||
{
|
||||
const epub_impl *e = (const epub_impl *)s->impl;
|
||||
if (!ref || !data || !size || !e->zip) return -1;
|
||||
return rdzip_read_named(e->zip, ref, data, size);
|
||||
}
|
||||
static int epub_pages(rd_src *s) { (void)s; return 0; }
|
||||
static int epub_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
|
||||
|
||||
static int epub_open(rd_src *s)
|
||||
{
|
||||
epub_impl *e = (epub_impl *)calloc(1, sizeof *e);
|
||||
if (!e) return -6;
|
||||
s->impl = e;
|
||||
e->spine_first_seg = (int *)calloc(MAXSPINE, sizeof(int));
|
||||
if (!e->spine_first_seg) { epub_free(s); return -6; }
|
||||
for (int i = 1; i < MAXSPINE; i++) e->spine_first_seg[i] = -1;
|
||||
|
||||
if (rdzip_open(&e->zip, s->path) != 0) { epub_free(s); return -2; }
|
||||
uint8_t *data = NULL; size_t sz = 0;
|
||||
char opf[320] = {0};
|
||||
if (rdzip_read_named(e->zip, "META-INF/container.xml", &data, &sz) == 0 && sz > 0) {
|
||||
rdxml_parse(data, sz, &cont_h, opf);
|
||||
free(data);
|
||||
}
|
||||
if (!opf[0]) { epub_free(s); return -3; }
|
||||
path_dir(opf, e->opf_dir, sizeof e->opf_dir);
|
||||
|
||||
data = NULL; sz = 0;
|
||||
if (rdzip_read_named(e->zip, opf, &data, &sz) != 0 || sz == 0) { epub_free(s); return -3; }
|
||||
opf_t o; memset(&o, 0, sizeof o);
|
||||
opfctx c; memset(&c, 0, sizeof c); c.o = &o;
|
||||
rdxml_handler oh = { opf_open, opf_close, opf_text };
|
||||
rdxml_parse(data, sz, &oh, &c);
|
||||
free(data);
|
||||
stl(s->title, sizeof s->title, o.title);
|
||||
stl(s->author, sizeof s->author, o.author);
|
||||
|
||||
/* spine path */
|
||||
e->n_spine = 0;
|
||||
for (int i = 0; i < o.n_spine && e->n_spine < MAXSPINE; i++) {
|
||||
const char *href = NULL;
|
||||
for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.spine_idref[i]) == 0) { href = o.items[k].href; break; }
|
||||
if (!href) continue;
|
||||
char abs[320]; path_join(e->opf_dir, href, abs, sizeof abs);
|
||||
stl(e->spine_path[e->n_spine], sizeof(e->spine_path[0]), abs);
|
||||
e->n_spine++;
|
||||
}
|
||||
e->spine_first_seg[0] = 0; /* 首 spine 首段=0;其余在建 seg 时填充 */
|
||||
for (int i = 1; i < e->n_spine; i++) e->spine_first_seg[i] = 0;
|
||||
|
||||
/* NCX */
|
||||
{
|
||||
char ncx_href[320] = {0};
|
||||
if (o.ncx_src[0]) {
|
||||
/* spine 的 toc 属性通常是 manifest id → 解析 href;若本身就是路径则直接用 */
|
||||
for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.ncx_src) == 0) { stl(ncx_href, sizeof ncx_href, o.items[k].href); break; }
|
||||
if (!ncx_href[0]) stl(ncx_href, sizeof ncx_href, o.ncx_src);
|
||||
}
|
||||
if (!ncx_href[0]) for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].mime, "application/x-dtbncx+xml") == 0) { stl(ncx_href, sizeof ncx_href, o.items[k].href); break; }
|
||||
if (ncx_href[0]) {
|
||||
char abs[320]; path_join(e->opf_dir, ncx_href, abs, sizeof abs);
|
||||
uint8_t *nd = NULL; size_t nsz = 0;
|
||||
if (rdzip_read_named(e->zip, abs, &nd, &nsz) == 0 && nsz) {
|
||||
ncxctx nc; memset(&nc, 0, sizeof nc);
|
||||
rdxml_handler nh = { ncx_open, ncx_close, ncx_text };
|
||||
rdxml_parse(nd, nsz, &nh, &nc);
|
||||
e->toc = nc.ents; e->n_toc = nc.n;
|
||||
free(nd);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* nav(EPUB3,仅当无 NCX):从 nav.xhtml 抽 <a href>text</a> */
|
||||
if (e->n_toc == 0 && o.nav_id[0]) {
|
||||
char abs[320];
|
||||
for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.nav_id) == 0) {
|
||||
path_join(e->opf_dir, o.items[k].href, abs, sizeof abs);
|
||||
uint8_t *nd = NULL; size_t nsz = 0;
|
||||
if (rdzip_read_named(e->zip, abs, &nd, &nsz) == 0 && nsz) {
|
||||
nav_extract(nd, nsz, e);
|
||||
free(nd);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
opf_free(&o);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* nav 抽取:<a href=..>…text…</a> 一个扁平目录 */
|
||||
static void nav_extract(const uint8_t *nd, size_t nsz, epub_impl *e)
|
||||
{
|
||||
int in_a = 0; char href[320] = {0}; char label[160] = {0}; size_t llen = 0;
|
||||
size_t i = 0;
|
||||
while (i < nsz) {
|
||||
if (nd[i] == '<') {
|
||||
if (i + 1 < nsz && (nd[i+1] == '/' ) && i + 2 < nsz && tolower(nd[i+2]) == 'a') {
|
||||
in_a = 0; i++; while (i < nsz && nd[i] != '>') i++; i++; continue;
|
||||
}
|
||||
/* a opening */
|
||||
size_t k = i + 1; size_t name_s = k;
|
||||
while (k < nsz && !strchr(" \t>\r\n/", nd[k])) k++;
|
||||
if (k - name_s == 1 && tolower(nd[name_s]) == 'a') {
|
||||
/* parse href */
|
||||
in_a = 1; href[0] = 0; label[0] = 0; llen = 0;
|
||||
while (k < nsz && nd[k] != '>') {
|
||||
/* skip ws */
|
||||
while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
|
||||
size_t an = k; while (k < nsz && nd[k] != '=' && nd[k] != '>' && nd[k] != ' ' && nd[k] != '\t') k++;
|
||||
size_t ae = k;
|
||||
while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
|
||||
if (k < nsz && nd[k] == '=') {
|
||||
k++;
|
||||
while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
|
||||
uint8_t q = 0; if (k < nsz && (nd[k] == '"' || nd[k] == '\'')) { q = nd[k]; k++; }
|
||||
size_t vs = k; while (k < nsz && (q ? nd[k] != q : nd[k] != '>' && nd[k] != ' ' && nd[k] != '\t')) k++;
|
||||
size_t ve = k; if (k < nsz && q && nd[k] == q) k++;
|
||||
if (ae - an == 4 && strncasecmp((const char*)nd + an, "href", 4) == 0) {
|
||||
size_t L = ve - vs; if (L >= sizeof href) L = sizeof href - 1;
|
||||
memcpy(href, nd + vs, L); href[L] = 0;
|
||||
}
|
||||
} else break;
|
||||
}
|
||||
i = k + 1; continue;
|
||||
}
|
||||
i = k; continue;
|
||||
}
|
||||
if (in_a) {
|
||||
uint8_t b = nd[i];
|
||||
if (b == '\r' || b == '\n' || b == '\t') { i++; continue; }
|
||||
if (b == ' ') { /* 折叠 */ if (llen && label[llen-1] != ' ') { label[llen++] = ' '; } i++; continue; }
|
||||
if (llen < sizeof label - 1) label[llen++] = (char)b;
|
||||
i++; continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (href[0] && label[0]) {
|
||||
ncxent *ne = (ncxent *)realloc(e->toc, (size_t)(e->n_toc + 1) * sizeof(ncxent));
|
||||
if (ne) { e->toc = ne; ncxent *dst = &e->toc[e->n_toc++]; stl(dst->label, sizeof dst->label, label); stl(dst->src, sizeof dst->src, href); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 注册 ---------------- */
|
||||
static int epub_probe(const uint8_t *head, size_t n)
|
||||
{
|
||||
return (n >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 3 && head[3] == 4);
|
||||
}
|
||||
static const rd_src_vt epub_vt = {
|
||||
epub_free, epub_reset, epub_next, epub_seg,
|
||||
epub_toc_n, epub_toc, epub_img, epub_pages, epub_page,
|
||||
};
|
||||
const rd_src_loader rd_epub_loader = { RD_FMT_EPUB, ".epub", epub_probe, epub_open, &epub_vt };
|
||||
@@ -0,0 +1,280 @@
|
||||
/* rdfb2.c — FB2 loader:严格 XML,用自写 rdxml 回调解析器抽段落。
|
||||
* 结构:<FictionBook> → <description>(元数据跳过) → <body>(主) → <section>… → <p>;
|
||||
* <title> 段题(目录);<binary id>BASE64</binary> 内嵌图(FBReader 式 base64 解码存 list);
|
||||
* <image l:href="#id"> 在文内落 "[图:ref]" 占位。
|
||||
*
|
||||
* 内存纪律:全文读入解析一次后即释;段文本存段表(可读正文),内嵌图解码一次存 image 列表。
|
||||
*/
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdxml.h"
|
||||
#include "rdsb.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void stl(char *d, int sz, const char *s)
|
||||
{
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
if (sz > 0) { if (n >= (size_t)sz) n = (size_t)sz - 1; memcpy(d, s, n); d[n] = 0; }
|
||||
}
|
||||
typedef struct fb2_img_t { char id[64]; uint8_t *data; size_t size; } fb2_img_t;
|
||||
typedef struct fb2_seg_t { char *text; size_t len; char title[160]; } fb2_seg_t;
|
||||
typedef struct fb2_toc_t { char title[160]; int seg; } fb2_toc_t;
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
fb2_seg_t *segs; int n_seg, cap_seg;
|
||||
fb2_toc_t *toc; int n_toc, cap_toc;
|
||||
fb2_img_t *imgs; int n_img, cap_img;
|
||||
int cur;
|
||||
} fb2_impl;
|
||||
|
||||
/* 解析 ctx:section 栈(text/title 各一个 rdsb),加文本模式标志 */
|
||||
typedef struct {
|
||||
fb2_impl *m;
|
||||
rdsb text[64], title[64]; int sp;
|
||||
int in_main, seen_body;
|
||||
int cur_title, in_binary, in_desc;
|
||||
char bin_id[64]; rdsb b64;
|
||||
} fb2c;
|
||||
|
||||
static void fb2_free(rd_src *s)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)s->impl;
|
||||
if (!m) return;
|
||||
for (int i = 0; i < m->n_seg; i++) free(m->segs[i].text);
|
||||
free(m->segs);
|
||||
for (int i = 0; i < m->n_img; i++) free(m->imgs[i].data);
|
||||
free(m->imgs);
|
||||
free(m->toc);
|
||||
free(m);
|
||||
s->impl = NULL;
|
||||
}
|
||||
static int seg_push(fb2_impl *m, rdsb *text, rdsb *title)
|
||||
{
|
||||
if (m->n_seg >= m->cap_seg) {
|
||||
int nc = m->cap_seg ? m->cap_seg * 2 : 64;
|
||||
fb2_seg_t *ns = (fb2_seg_t *)realloc(m->segs, (size_t)nc * sizeof(fb2_seg_t));
|
||||
if (!ns) return -1;
|
||||
m->segs = ns; m->cap_seg = nc;
|
||||
}
|
||||
fb2_seg_t *sg = &m->segs[m->n_seg];
|
||||
sg->text = (char *)malloc(text->len ? text->len : 1);
|
||||
if (!sg->text) return -1;
|
||||
if (text->len) memcpy(sg->text, text->buf, text->len);
|
||||
sg->len = text->len;
|
||||
size_t tl = title->len, k = 0;
|
||||
while (k < tl && (title->buf[k] == '\n' || title->buf[k] == ' ' || title->buf[k] == '\t')) k++;
|
||||
size_t s0 = k; while (k < tl && title->buf[k] != '\n') k++;
|
||||
if (k - s0 >= sizeof sg->title) k = s0 + sizeof sg->title - 1;
|
||||
memcpy(sg->title, title->buf + s0, k - s0); sg->title[k - s0] = 0;
|
||||
m->n_seg++;
|
||||
return 0;
|
||||
}
|
||||
static int toc_push(fb2_impl *m, const char *title, int seg)
|
||||
{
|
||||
if (m->n_toc >= m->cap_toc) {
|
||||
int nc = m->cap_toc ? m->cap_toc * 2 : 32;
|
||||
fb2_toc_t *nt = (fb2_toc_t *)realloc(m->toc, (size_t)nc * sizeof(fb2_toc_t));
|
||||
if (!nt) return -1;
|
||||
m->toc = nt; m->cap_toc = nc;
|
||||
}
|
||||
fb2_toc_t *t = &m->toc[m->n_toc++];
|
||||
stl(t->title, sizeof t->title, title);
|
||||
t->seg = seg;
|
||||
return 0;
|
||||
}
|
||||
static int img_add(fb2_impl *m, const char *id, uint8_t *data, size_t size)
|
||||
{
|
||||
if (m->n_img >= m->cap_img) {
|
||||
int nc = m->cap_img ? m->cap_img * 2 : 8;
|
||||
fb2_img_t *ni = (fb2_img_t *)realloc(m->imgs, (size_t)nc * sizeof(fb2_img_t));
|
||||
if (!ni) return -1;
|
||||
m->imgs = ni; m->cap_img = nc;
|
||||
}
|
||||
fb2_img_t *im = &m->imgs[m->n_img++];
|
||||
stl(im->id, sizeof im->id, id);
|
||||
im->data = data; im->size = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fb2_xml_open(const char *n, const rdxml_attr *a, int na, void *ctx)
|
||||
{
|
||||
fb2c *c = (fb2c *)ctx;
|
||||
if (strcmp(n, "body") == 0) { if (!c->seen_body) { c->seen_body = 1; c->in_main = 1; } else c->in_main = 0; return; }
|
||||
if (strcmp(n, "description") == 0) { c->in_desc = 1; return; }
|
||||
if (strcmp(n, "section") == 0 && c->in_main) {
|
||||
if (c->sp < 64) { rdsb_init(&c->text[c->sp]); rdsb_init(&c->title[c->sp]); c->sp++; }
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "title") == 0 && c->in_main && c->sp > 0) { c->cur_title = 1; return; }
|
||||
if (strcmp(n, "binary") == 0) {
|
||||
c->in_binary = 1; c->bin_id[0] = 0;
|
||||
for (int i = 0; i < na; i++) if (strcmp(a[i].name, "id") == 0) { stl(c->bin_id, sizeof c->bin_id, a[i].value); }
|
||||
rdsb_init(&c->b64);
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "image") == 0 && c->in_main && !c->in_binary && c->sp > 0) {
|
||||
char ref[96] = {0};
|
||||
for (int i = 0; i < na; i++)
|
||||
if (strcmp(a[i].name, "href") == 0 || strcmp(a[i].name, "xlink:href") == 0 || strcmp(a[i].name, "l:href") == 0)
|
||||
stl(ref, sizeof ref, a[i].value);
|
||||
if (ref[0] == '#') memmove(ref, ref + 1, strlen(ref));
|
||||
if (ref[0]) {
|
||||
rdsb *tb = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
|
||||
rdsb_put(tb, (const uint8_t *)"\n[图:", 4);
|
||||
rdsb_put(tb, (const uint8_t *)ref, strlen(ref));
|
||||
rdsb_put(tb, (const uint8_t *)"]\n", 2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
static void fb2_xml_text(const char *t, size_t len, void *ctx)
|
||||
{
|
||||
fb2c *c = (fb2c *)ctx;
|
||||
if (c->in_binary) { rdsb_put(&c->b64, (const uint8_t *)t, len); return; }
|
||||
if (c->in_desc || !c->in_main || c->sp <= 0) return;
|
||||
rdsb *dst = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
|
||||
uint8_t dec[512];
|
||||
size_t done = 0;
|
||||
while (done < len) {
|
||||
size_t want = len - done; if (want > sizeof dec - 2) want = sizeof dec - 2;
|
||||
size_t n = rd_ent_decode_to((const uint8_t *)t + done, want, dec, sizeof dec);
|
||||
rdsb_put(dst, dec, n);
|
||||
done += want;
|
||||
}
|
||||
}
|
||||
static void fb2_xml_close(const char *n, void *ctx)
|
||||
{
|
||||
fb2c *c = (fb2c *)ctx; fb2_impl *m = c->m;
|
||||
if (strcmp(n, "body") == 0) { c->in_main = 0; return; }
|
||||
if (strcmp(n, "description") == 0) { c->in_desc = 0; return; }
|
||||
if (strcmp(n, "title") == 0) {
|
||||
c->cur_title = 0;
|
||||
/* 段题也进入正文(标题作段首),保证「第一章」出现在渲染文本中 */
|
||||
if (c->in_main && c->sp > 0 && c->title[c->sp - 1].len > 0) {
|
||||
rdsb_put(&c->text[c->sp - 1], c->title[c->sp - 1].buf, c->title[c->sp - 1].len);
|
||||
rdsb_nl(&c->text[c->sp - 1]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "p") == 0 && c->in_main && c->sp > 0) {
|
||||
rdsb *dst = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
|
||||
rdsb_nl(dst);
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "binary") == 0 && c->in_binary) {
|
||||
c->in_binary = 0;
|
||||
size_t dlen = 0; uint8_t *dec = NULL;
|
||||
if (c->b64.len > 0) {
|
||||
size_t cap = c->b64.len / 4 * 3 + 4;
|
||||
dec = (uint8_t *)malloc(cap);
|
||||
if (dec && rd_b64_decode(c->b64.buf, c->b64.len, dec, cap, &dlen) != 0) { free(dec); dec = NULL; }
|
||||
}
|
||||
if (c->bin_id[0]) { if (!dec) { dec = (uint8_t *)malloc(1); dlen = 0; } img_add(m, c->bin_id, dec, dlen); }
|
||||
else free(dec);
|
||||
rdsb_free(&c->b64);
|
||||
return;
|
||||
}
|
||||
if (strcmp(n, "section") == 0 && c->in_main && c->sp > 0) {
|
||||
c->sp--;
|
||||
if (c->text[c->sp].len > 0 || c->title[c->sp].len > 1) {
|
||||
int idx = m->n_seg;
|
||||
if (seg_push(m, &c->text[c->sp], &c->title[c->sp]) == 0) {
|
||||
if (m->segs[idx].title[0]) toc_push(m, m->segs[idx].title, idx);
|
||||
}
|
||||
}
|
||||
rdsb_free(&c->text[c->sp]); rdsb_free(&c->title[c->sp]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- vtable ---- */
|
||||
static int fb2_reset(rd_src *s) { ((fb2_impl *)s->impl)->cur = 0; return 0; }
|
||||
static int fb2_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)s->impl;
|
||||
if (m->cur >= m->n_seg) return 1;
|
||||
fb2_seg_t *sg = &m->segs[m->cur++];
|
||||
size_t n = sg->len;
|
||||
if (n > cap) n = cap;
|
||||
if (n) memcpy(buf, sg->text, n);
|
||||
*len = n;
|
||||
return 0;
|
||||
}
|
||||
static int fb2_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)s->impl;
|
||||
if (idx < 0 || idx >= m->n_seg) return 1;
|
||||
fb2_seg_t *sg = &m->segs[idx];
|
||||
size_t n = sg->len; if (n > cap) n = cap;
|
||||
if (n) memcpy(buf, sg->text, n);
|
||||
*len = n;
|
||||
return 0;
|
||||
}
|
||||
static int fb2_toc_n(rd_src *s) { return ((fb2_impl *)s->impl)->n_toc; }
|
||||
static int fb2_toc(rd_src *s, int i, rd_toc_entry *t)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)s->impl;
|
||||
if (i < 0 || i >= m->n_toc) return -1;
|
||||
stl(t->title, sizeof t->title, m->toc[i].title);
|
||||
t->seg = m->toc[i].seg; t->ch = 0;
|
||||
return 0;
|
||||
}
|
||||
static int fb2_img(rd_src *s, const char *ref, uint8_t **data, size_t *size)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)s->impl;
|
||||
if (!data || !size || !ref) return -1;
|
||||
for (int i = 0; i < m->n_img; i++) if (strcmp(m->imgs[i].id, ref) == 0) {
|
||||
*data = (uint8_t *)malloc(m->imgs[i].size ? m->imgs[i].size : 1);
|
||||
if (!*data) return -6;
|
||||
if (m->imgs[i].size) memcpy(*data, m->imgs[i].data, m->imgs[i].size);
|
||||
*size = m->imgs[i].size;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
static int fb2_pages(rd_src *s) { (void)s; return 0; }
|
||||
static int fb2_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
|
||||
|
||||
static int fb2_open(rd_src *s)
|
||||
{
|
||||
fb2_impl *m = (fb2_impl *)calloc(1, sizeof *m);
|
||||
if (!m) return -6;
|
||||
s->impl = m;
|
||||
m->path = strdup(s->path);
|
||||
FILE *f = fopen(s->path, "rb");
|
||||
if (!f) { fb2_free(s); return -1; }
|
||||
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
if (sz <= 0) { fclose(f); fb2_free(s); return -2; }
|
||||
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
|
||||
if (!buf) { fclose(f); fb2_free(s); return -6; }
|
||||
size_t got = fread(buf, 1, (size_t)sz, f);
|
||||
fclose(f);
|
||||
|
||||
fb2c c; memset(&c, 0, sizeof c); c.m = m;
|
||||
rdxml_handler h = { fb2_xml_open, fb2_xml_close, fb2_xml_text };
|
||||
rdxml_parse(buf, got, &h, &c);
|
||||
free(buf);
|
||||
stl(s->title, sizeof s->title, "FB2");
|
||||
/* 书名用第一个 section title?默认文件名 */
|
||||
if (m->n_toc > 0) stl(s->title, sizeof s->title, m->toc[0].title);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fb2_probe(const uint8_t *head, size_t n)
|
||||
{
|
||||
/* '<FictionBook' 开头 */
|
||||
if (n < 13) return 0;
|
||||
if (memcmp(head, "<?xml", 5) == 0) {
|
||||
for (size_t i = 0; i < n - 10; i++)
|
||||
if (memcmp(head + i, "<FictionBook", 12) == 0) return 1;
|
||||
}
|
||||
if (memcmp(head, "<FictionBook", 12) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
static const rd_src_vt fb2_vt = {
|
||||
fb2_free, fb2_reset, fb2_next, fb2_seg,
|
||||
fb2_toc_n, fb2_toc, fb2_img, fb2_pages, fb2_page,
|
||||
};
|
||||
const rd_src_loader rd_fb2_loader = { RD_FMT_FB2, ".fb2", fb2_probe, fb2_open, &fb2_vt };
|
||||
@@ -0,0 +1,286 @@
|
||||
/* rdhtml.c — XHTML / raw HTML 粗解析(实现见 rdhtml.h)。 */
|
||||
#include "rdhtml.h"
|
||||
#include "rdxml.h" /* 实体解码 rd_ent_decode / rd_ent_decode_to */
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
typedef struct {
|
||||
const rdhtml_out *out; void *ctx;
|
||||
uint8_t run[1024]; int run_len;
|
||||
int pending_space;
|
||||
int inbody; int seen_body;
|
||||
int in_heading; /* 当前在 h1..h6 内 */
|
||||
uint8_t hbuf[256]; int hlen; /* 标题文本缓冲 */
|
||||
} hx;
|
||||
static int is_heading_name(const uint8_t *nm, size_t nl)
|
||||
{
|
||||
if (nl < 2 || nl > 3 || nm[0] != 'h') return 0;
|
||||
uint8_t c = nm[1];
|
||||
if (c < '1' || c > '6') return 0;
|
||||
if (nl == 3 && nm[2] != '\0') return 0;
|
||||
return 1;
|
||||
}
|
||||
static void hx_heading_flush(hx *x)
|
||||
{
|
||||
/* 去掉首尾空白后回调 */
|
||||
if (x->in_heading && x->out && x->out->heading) {
|
||||
int s = 0, e = x->hlen;
|
||||
while (s < e && (x->hbuf[s] == ' ' || x->hbuf[s] == '\n' || x->hbuf[s] == '\t')) s++;
|
||||
while (e > s && (x->hbuf[e-1] == ' ' || x->hbuf[e-1] == '\n' || x->hbuf[e-1] == '\t')) e--;
|
||||
if (e > s) x->out->heading(x->hbuf + s, (size_t)(e - s), x->ctx);
|
||||
}
|
||||
x->hlen = 0;
|
||||
}
|
||||
|
||||
static void hx_flush(hx *x)
|
||||
{
|
||||
if (x->run_len > 0) {
|
||||
if (x->out && x->out->text) x->out->text(x->run, (size_t)x->run_len, x->ctx);
|
||||
x->run_len = 0;
|
||||
}
|
||||
}
|
||||
static void hx_space(hx *x)
|
||||
{
|
||||
hx_flush(x);
|
||||
if (x->pending_space) return;
|
||||
if (x->out && x->out->text) {
|
||||
static const uint8_t sp = ' ';
|
||||
x->out->text(&sp, 1, x->ctx);
|
||||
}
|
||||
if (x->in_heading && x->hlen < (int)sizeof x->hbuf) x->hbuf[x->hlen] = ' ', x->hlen++;
|
||||
x->pending_space = 1;
|
||||
}
|
||||
static void hx_byte(hx *x, uint8_t b)
|
||||
{
|
||||
if (x->pending_space) x->pending_space = 0;
|
||||
if (x->run_len >= (int)sizeof x->run) hx_flush(x);
|
||||
x->run[x->run_len++] = b;
|
||||
if (x->in_heading && x->hlen < (int)sizeof x->hbuf) x->hbuf[x->hlen++] = b;
|
||||
}
|
||||
static void hx_newline(hx *x)
|
||||
{
|
||||
hx_flush(x);
|
||||
x->pending_space = 0;
|
||||
if (x->out && x->out->newline) x->out->newline(x->ctx);
|
||||
}
|
||||
|
||||
/* 在 tag 子串(ta[0..tlen),不含首 '<')里查属性 attr(局部名),写解码值到 val(≤vsz)。
|
||||
* 返回 1=找到。宽松:把 namespace 前缀剥离(':' 或 ':' 取后段)。 */
|
||||
static int attr_lookup(const uint8_t *ta, size_t tlen, const char *attr, char *val, size_t vsz)
|
||||
{
|
||||
size_t i = 0;
|
||||
size_t alen = strlen(attr);
|
||||
while (i < tlen) {
|
||||
/* 跳空白与 '/' '>' */
|
||||
while (i < tlen && (ta[i] == ' ' || ta[i] == '\t' || ta[i] == '\r' || ta[i] == '\n')) i++;
|
||||
if (i >= tlen || ta[i] == '>' || ta[i] == '/') break;
|
||||
size_t ns = i;
|
||||
while (i < tlen && ta[i] != ' ' && ta[i] != '\t' && ta[i] != '=' && ta[i] != '>' && ta[i] != '/') i++;
|
||||
size_t ne = i;
|
||||
/* 局部名:剥 ':' 前缀 */
|
||||
size_t lstart = ns, lend = ne;
|
||||
for (size_t k = ns; k < ne; k++) if (ta[k] == ':') { lstart = k + 1; }
|
||||
size_t ln = lend - lstart;
|
||||
while (i < tlen && (ta[i] == ' ' || ta[i] == '\t')) i++;
|
||||
if (i < tlen && ta[i] == '=') {
|
||||
i++;
|
||||
while (i < tlen && (ta[i] == ' ' || ta[i] == '\t')) i++;
|
||||
if (i < tlen && (ta[i] == '"' || ta[i] == '\'')) {
|
||||
uint8_t q = ta[i]; i++;
|
||||
size_t vs = i;
|
||||
while (i < tlen && ta[i] != q) i++;
|
||||
size_t ve = i;
|
||||
if (i < tlen && ta[i] == q) i++;
|
||||
if (ln == alen && memcmp(ta + lstart, attr, alen) == 0) {
|
||||
size_t dec = rd_ent_decode_to(ta + vs, ve - vs, (uint8_t *)val, vsz);
|
||||
if (dec >= vsz) dec = vsz - 1;
|
||||
val[dec] = 0;
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
size_t vs = i;
|
||||
while (i < tlen && ta[i] != ' ' && ta[i] != '\t' && ta[i] != '>') i++;
|
||||
if (ln == alen && memcmp(ta + lstart, attr, alen) == 0) {
|
||||
size_t vlen = i - vs; if (vlen >= vsz) vlen = vsz - 1;
|
||||
memcpy(val, ta + vs, vlen); val[vlen] = 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int name_eq_cs(const uint8_t *nm, size_t nl, const char *s)
|
||||
{
|
||||
size_t sl = strlen(s);
|
||||
if (nl != sl) return 0;
|
||||
for (size_t k = 0; k < nl; k++) {
|
||||
uint8_t a = nm[k]; if (a >= 'A' && a <= 'Z') a += 32;
|
||||
if (a != (uint8_t)s[k]) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static int name_eq_css(const uint8_t *nm, size_t nl, const char *s) /* 已小写 s */
|
||||
{
|
||||
return name_eq_cs(nm, nl, s);
|
||||
}
|
||||
|
||||
/* 判定是否块级断行 tag(局部名小写)。 */
|
||||
static int is_block_tag(const uint8_t *nm, size_t nl, int is_close)
|
||||
{
|
||||
/* 开/闭都断行:p div li h1..h6 blockquote section article table tr td ul ol dl dt dd
|
||||
* center hr br(br 行断) */
|
||||
static const char *blocks[] = { "p","div","li","ul","ol","dl","dt","dd","h1","h2","h3",
|
||||
"h4","h5","h6","blockquote","section","article","tr","td",
|
||||
"th","table","center","hr","br","address","pre" };
|
||||
(void)is_close;
|
||||
for (size_t k = 0; k < sizeof blocks / sizeof blocks[0]; k++)
|
||||
if (name_eq_css(nm, nl, blocks[k])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rdhtml_parse(const uint8_t *html, size_t len, const rdhtml_out *out, void *ctx, int emit_all)
|
||||
{
|
||||
if (!html) return 0;
|
||||
hx x; memset(&x, 0, sizeof x);
|
||||
x.out = out; x.ctx = ctx;
|
||||
x.inbody = emit_all; /* emit_all: 无 body 门控 */
|
||||
int raw_until = 0; /* 0=无;>0=处于 raw 区(1=script,2=style) */
|
||||
size_t i = 0;
|
||||
|
||||
while (i < len) {
|
||||
uint8_t c = html[i];
|
||||
if (raw_until) {
|
||||
/* 在 <script>/<style> raw 区:找匹配闭合标签 */
|
||||
size_t j = i;
|
||||
const char *endtag = (raw_until == 1) ? "</script" : "</style>";
|
||||
size_t etl = (raw_until == 1) ? 8 : 7;
|
||||
while (j + etl <= len && !(tolower(html[j]) == '<' && tolower(html[j + 1]) == '/' &&
|
||||
memcmp(html + j + 2, endtag + 2, etl - 2) == 0)) j++;
|
||||
if (j + etl <= len) { i = j; raw_until = 0; }
|
||||
else i = len;
|
||||
continue;
|
||||
}
|
||||
if (c == '<') {
|
||||
/* 处理 PI/注释/doctype(粗略跳过) */
|
||||
if (i + 1 < len && html[i + 1] == '?') { while (i < len && !(html[i] == '?' && i + 1 < len && html[i + 1] == '>')) i++; i += (i < len) ? 2 : 0; continue; }
|
||||
if (i + 3 < len && memcmp(html + i, "<!--", 4) == 0) { while (i + 2 < len && !(html[i] == '-' && html[i + 1] == '-' && html[i + 2] == '>')) i++; i += (i + 2 < len) ? 3 : 0; continue; }
|
||||
if (i + 8 < len && memcmp(html + i, "<![CDATA[", 9) == 0) {
|
||||
size_t s = i + 9; size_t j = s;
|
||||
while (j + 2 < len && !(html[j] == ']' && html[j + 1] == ']' && html[j + 2] == '>')) j++;
|
||||
if (x.inbody) { if (!x.pending_space && x.run_len == 0) hx_flush(&x); for (size_t kk = s; kk < j; kk++) hx_byte(&x, html[kk]); }
|
||||
i = (j + 2 < len) ? j + 3 : len; continue;
|
||||
}
|
||||
if (i + 1 < len && html[i + 1] == '!') { while (i < len && html[i] != '>') i++; i += (i < len) ? 1 : 0; continue; }
|
||||
|
||||
/* 正常标签:读到 '>'(尊重引号),截断到 MAX_TAG */
|
||||
size_t j = i + 1;
|
||||
uint8_t q = 0;
|
||||
int selfclose = 0;
|
||||
size_t tagend = (size_t)-1;
|
||||
while (j < len) {
|
||||
uint8_t tc = html[j];
|
||||
if (q) { if (tc == q) q = 0; }
|
||||
else if (tc == '"' || tc == '\'') q = tc;
|
||||
else if (tc == '>') { tagend = j; break; }
|
||||
j++;
|
||||
if (j - i > 1000) { /* 超长 tag:吞到下一个 '>'(防溢出) */
|
||||
while (j < len && html[j] != '>') j++;
|
||||
if (j < len) { tagend = j; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tagend == (size_t)-1) break;
|
||||
|
||||
uint8_t tagbuf[1000]; size_t tl = tagend - (i + 1);
|
||||
if (tl > sizeof tagbuf) tl = sizeof tagbuf;
|
||||
memcpy(tagbuf, html + i + 1, tl);
|
||||
|
||||
size_t k = 0;
|
||||
while (k < tl && (tagbuf[k] == ' ' || tagbuf[k] == '\t' || tagbuf[k] == '\r' || tagbuf[k] == '\n')) k++;
|
||||
int is_close = (k < tl && tagbuf[k] == '/');
|
||||
if (is_close) k++;
|
||||
while (k < tl && (tagbuf[k] == ' ' || tagbuf[k] == '\t' || tagbuf[k] == '\r' || tagbuf[k] == '\n')) k++;
|
||||
size_t ns = k;
|
||||
while (k < tl && tagbuf[k] != ' ' && tagbuf[k] != '>' && tagbuf[k] != '/' && tagbuf[k] != '\t' &&
|
||||
tagbuf[k] != '\r' && tagbuf[k] != '\n') k++;
|
||||
size_t ne = k;
|
||||
|
||||
/* 自闭合判断:末尾 '/>' */
|
||||
{
|
||||
size_t e = tl; while (e > 0 && (tagbuf[e - 1] == ' ' || tagbuf[e - 1] == '\t')) e--;
|
||||
if (e >= 1 && tagbuf[e - 1] == '/') selfclose = 1;
|
||||
}
|
||||
|
||||
/* script/style */
|
||||
if (name_eq_css(tagbuf + ns, ne - ns, "script") && !is_close) { raw_until = 1; i = tagend + 1; continue; }
|
||||
if (name_eq_css(tagbuf + ns, ne - ns, "style") && !is_close) { raw_until = 2; i = tagend + 1; continue; }
|
||||
if ((name_eq_css(tagbuf + ns, ne - ns, "script") || name_eq_css(tagbuf + ns, ne - ns, "style")) && is_close) { raw_until = 0; i = tagend + 1; continue; }
|
||||
|
||||
/* <body> 门控 */
|
||||
if (!selfclose && name_eq_css(tagbuf + ns, ne - ns, "body") && !is_close) { x.seen_body = 1; x.inbody = 1; i = tagend + 1; continue; }
|
||||
if (name_eq_css(tagbuf + ns, ne - ns, "body") && is_close) { x.inbody = 0; i = tagend + 1; continue; }
|
||||
|
||||
if (!x.inbody && !emit_all) { i = tagend + 1; continue; }
|
||||
|
||||
/* 图片 */
|
||||
if (!selfclose && !is_close && name_eq_css(tagbuf + ns, ne - ns, "img")) {
|
||||
char src[512]; src[0] = 0;
|
||||
if (attr_lookup(tagbuf, tl, "src", src, sizeof src) && src[0]) {
|
||||
hx_flush(&x); x.pending_space = 0;
|
||||
if (x.out && x.out->image) x.out->image(src, x.ctx);
|
||||
}
|
||||
i = tagend + 1; continue;
|
||||
}
|
||||
|
||||
/* 标题(h1..h6):闭合时回调标题文本;仍作块级断行 */
|
||||
if (is_heading_name(tagbuf + ns, ne - ns)) {
|
||||
if (is_close) { hx_heading_flush(&x); x.in_heading = 0; hx_newline(&x); }
|
||||
else { x.in_heading = 1; x.hlen = 0; x.pending_space = 0; hx_newline(&x); }
|
||||
i = tagend + 1; continue;
|
||||
}
|
||||
/* 块级断行 */
|
||||
if (name_eq_css(tagbuf + ns, ne - ns, "rt")) { i = tagend + 1; continue; } /* v1 忽略 ruby rt */
|
||||
if (name_eq_css(tagbuf + ns, ne - ns, "ruby")) { i = tagend + 1; continue; }
|
||||
if (is_block_tag(tagbuf + ns, ne - ns, is_close)) {
|
||||
if (!is_close || !selfclose) hx_newline(&x);
|
||||
i = tagend + 1; continue;
|
||||
}
|
||||
|
||||
i = tagend + 1;
|
||||
continue;
|
||||
}
|
||||
if (c == '&') {
|
||||
/* 实体:找到 ';' */
|
||||
size_t es = i + 1; size_t e = es;
|
||||
while (e < len && html[e] != ';' && e - es < 16) e++;
|
||||
uint8_t tmp[8]; int dn;
|
||||
if (e < len && html[e] == ';') {
|
||||
dn = rd_ent_decode(html + i, (e + 1) - i, tmp, sizeof tmp);
|
||||
} else dn = -1;
|
||||
if (dn > 0) {
|
||||
if (x.inbody) for (int bi = 0; bi < dn; bi++) {
|
||||
uint8_t b = tmp[bi];
|
||||
if (b == ' ') hx_space(&x);
|
||||
else hx_byte(&x, b);
|
||||
}
|
||||
i = (e < len) ? e + 1 : len;
|
||||
continue;
|
||||
}
|
||||
/* 未识别实体:吐 '&' 继续 */
|
||||
if (x.inbody) hx_space(&x); /* & 罕见作实义字符,给个空格(宽松) */
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
/* 文本 */
|
||||
if (!x.inbody && !emit_all) { i++; continue; }
|
||||
if (c == '\r') { i++; continue; }
|
||||
if (c == '\t' || c == '\n') { hx_space(&x); i++; continue; }
|
||||
if (c == ' ') { hx_space(&x); i++; continue; }
|
||||
hx_byte(&x, c);
|
||||
i++;
|
||||
}
|
||||
hx_flush(&x);
|
||||
return x.seen_body;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* rdhtml.h — XHTML / raw HTML 粗解析(机身正文;容错,不建 DOM,不喂 pugixml)。
|
||||
*
|
||||
* 对齐 READER_ENGINE_NOTES §4.2-4.4(epub2txt2 式三态机):未知/坏标签吞掉、块级 tag 断行、
|
||||
* 实体内省、<script>/<style> 内容当 raw text 跳过、<body> 前内容丢弃(无 body 时收全部)、
|
||||
* `\r` 跳过/`\t`→空格/连续空格折叠、`\n` 折叠为空格(块断由回调给)。
|
||||
*
|
||||
* 输出经回调(非直接产出缓冲):text(utf8,len) / newline() / image(src)。
|
||||
* 调用方(rdsrc 各 loader)用这些事件组装「UTF-8 段 + {块断} + 图片引用」。
|
||||
* 纯 C,宿主可测,无 SDK 依赖。
|
||||
*/
|
||||
#ifndef RDHTML_H
|
||||
#define RDHTML_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct rdhtml_out {
|
||||
void (*text)(const uint8_t *utf8, size_t len, void *ctx);
|
||||
void (*newline)(void *ctx); /* 段/行断(块级 tag / <br>) */
|
||||
void (*image)(const char *src, void *ctx); /* <img src>,src 原样 */
|
||||
void (*heading)(const uint8_t *utf8, size_t len, void *ctx); /* 可选:闭合 h1..h6 时回调其文本 */
|
||||
} rdhtml_out;
|
||||
|
||||
/* 解析 [html, html+len)。emit_all=1 → 忽略 <body> 门控(无 body 的篇章照收,但 script/style
|
||||
* 仍跳过)。返回 1=遇到 <body> 进入正文;0=全程在 <body> 外(可据此再用 emit_all 重扫)。 */
|
||||
int rdhtml_parse(const uint8_t *html, size_t len, const rdhtml_out *out, void *ctx, int emit_all);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* RDHTML_H */
|
||||
@@ -0,0 +1,237 @@
|
||||
/* rdmobi.c — MOBI/AZW3 loader:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml
|
||||
* → flow(html 章节) + resources(图片)),HTML 走 rdhtml 同款粗解析 → UTF-8 段契约。
|
||||
*
|
||||
* 合规:libmobi 为 LGPL-3.0;NOTICE 文件(版权+LGPL 文本+锁定版本)见 patch_v2/reader_app/NOTICE。
|
||||
* libmobi 编译用系统 zlib(USE_MINIZ 未定义→m_uncompress=uncompress),不与其内置 miniz 冲突。
|
||||
*
|
||||
* 内存纪律:open 时遍历 flow 部件抽文本(可读文字)→ 段表;复制 resources 图片到 list;然后析构
|
||||
* rawml+mobi(释掉整书 HTML/标记),稳态 = 可读正文 + 图片。
|
||||
*/
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdhtml.h"
|
||||
#include "rdsb.h"
|
||||
#include "mobi.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct m_info { char uid_s[24]; uint8_t *data; size_t size; } m_info;
|
||||
typedef struct m_seg { char *text; size_t len; } m_seg;
|
||||
typedef struct m_toc { char title[160]; int seg; } m_toc;
|
||||
|
||||
typedef struct {
|
||||
m_seg *segs; int n_seg, cap_seg;
|
||||
m_toc *toc; int n_toc, cap_toc;
|
||||
m_info *imgs; int n_img, cap_img;
|
||||
int cur;
|
||||
int *flow_first_seg; int n_flow;
|
||||
} mobi_impl;
|
||||
|
||||
/* 解析某一 flow 部件 → rdsb 文本;同时捕获首个标题(回调) */
|
||||
typedef struct { rdsb *sb; char title[160]; int have_title; } motrice;
|
||||
static void mo_text(const uint8_t *u, size_t len, void *ctx) { rdsb_put(((motrice *)ctx)->sb, u, len); }
|
||||
static void mo_nl(void *ctx) { rdsb_nl(((motrice *)ctx)->sb); }
|
||||
static void mo_img(const char *src, void *ctx) { (void)src; motrice *m = (motrice *)ctx; rdsb_put(m->sb, (const uint8_t *)"\n[图]\n", 7); }
|
||||
static void mo_heading(const uint8_t *u, size_t len, void *ctx)
|
||||
{
|
||||
motrice *m = (motrice *)ctx;
|
||||
if (m->have_title || len == 0) return;
|
||||
size_t n = len < sizeof m->title - 1 ? len : sizeof m->title - 1;
|
||||
memcpy(m->title, u, n); m->title[n] = 0; m->have_title = 1;
|
||||
}
|
||||
|
||||
static void mobi_free_src(rd_src *s)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)s->impl;
|
||||
if (!m) return;
|
||||
for (int i = 0; i < m->n_seg; i++) free(m->segs[i].text);
|
||||
free(m->segs);
|
||||
free(m->toc);
|
||||
for (int i = 0; i < m->n_img; i++) free(m->imgs[i].data);
|
||||
free(m->imgs);
|
||||
free(m->flow_first_seg);
|
||||
free(m);
|
||||
s->impl = NULL;
|
||||
}
|
||||
static int push_seg(mobi_impl *m, const char *text, size_t len)
|
||||
{
|
||||
if (m->n_seg >= m->cap_seg) {
|
||||
int nc = m->cap_seg ? m->cap_seg * 2 : 64;
|
||||
m_seg *ns = (m_seg *)realloc(m->segs, (size_t)nc * sizeof(m_seg));
|
||||
if (!ns) return -1;
|
||||
m->segs = ns; m->cap_seg = nc;
|
||||
}
|
||||
m_seg *sg = &m->segs[m->n_seg++];
|
||||
sg->text = (char *)malloc(len ? len : 1);
|
||||
if (!sg->text) return -1;
|
||||
if (len) memcpy(sg->text, text, len);
|
||||
sg->len = len;
|
||||
return 0;
|
||||
}
|
||||
static int push_toc(mobi_impl *m, const char *title, int seg)
|
||||
{
|
||||
if (m->n_toc >= m->cap_toc) {
|
||||
int nc = m->cap_toc ? m->cap_toc * 2 : 32;
|
||||
m_toc *nt = (m_toc *)realloc(m->toc, (size_t)nc * sizeof(m_toc));
|
||||
if (!nt) return -1;
|
||||
m->toc = nt; m->cap_toc = nc;
|
||||
}
|
||||
m_toc *t = &m->toc[m->n_toc++];
|
||||
strncpy(t->title, title, sizeof t->title - 1); t->title[sizeof t->title - 1] = 0;
|
||||
t->seg = seg;
|
||||
return 0;
|
||||
}
|
||||
static int push_img(mobi_impl *m, const char *uid_s, uint8_t *data, size_t size)
|
||||
{
|
||||
if (m->n_img >= m->cap_img) {
|
||||
int nc = m->cap_img ? m->cap_img * 2 : 8;
|
||||
m_info *ni = (m_info *)realloc(m->imgs, (size_t)nc * sizeof(m_info));
|
||||
if (!ni) return -1;
|
||||
m->imgs = ni; m->cap_img = nc;
|
||||
}
|
||||
m_info *im = &m->imgs[m->n_img++];
|
||||
strncpy(im->uid_s, uid_s, sizeof im->uid_s - 1); im->uid_s[sizeof im->uid_s - 1] = 0;
|
||||
im->data = data; im->size = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mobi_reset(rd_src *s) { ((mobi_impl *)s->impl)->cur = 0; return 0; }
|
||||
static int mobi_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)s->impl;
|
||||
if (m->cur >= m->n_seg) return 1;
|
||||
m_seg *sg = &m->segs[m->cur++];
|
||||
size_t n = sg->len; if (n > cap) n = cap;
|
||||
if (n) memcpy(buf, sg->text, n);
|
||||
*len = n;
|
||||
return 0;
|
||||
}
|
||||
static int mobi_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)s->impl;
|
||||
if (idx < 0 || idx >= m->n_seg) return 1;
|
||||
m_seg *sg = &m->segs[idx];
|
||||
size_t n = sg->len; if (n > cap) n = cap;
|
||||
if (n) memcpy(buf, sg->text, n);
|
||||
*len = n;
|
||||
return 0;
|
||||
}
|
||||
static int mobi_toc_n(rd_src *s) { return ((mobi_impl *)s->impl)->n_toc; }
|
||||
static int mobi_toc(rd_src *s, int i, rd_toc_entry *t)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)s->impl;
|
||||
if (i < 0 || i >= m->n_toc) return -1;
|
||||
strncpy(t->title, m->toc[i].title, sizeof t->title - 1); t->title[sizeof t->title - 1] = 0;
|
||||
t->seg = m->toc[i].seg; t->ch = 0;
|
||||
return 0;
|
||||
}
|
||||
static int mobi_img(rd_src *s, const char *ref, uint8_t **data, size_t *size)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)s->impl;
|
||||
if (!ref || !data || !size) return -1;
|
||||
for (int i = 0; i < m->n_img; i++) if (strcmp(m->imgs[i].uid_s, ref) == 0) {
|
||||
*data = (uint8_t *)malloc(m->imgs[i].size ? m->imgs[i].size : 1);
|
||||
if (!*data) return -6;
|
||||
if (m->imgs[i].size) memcpy(*data, m->imgs[i].data, m->imgs[i].size);
|
||||
*size = m->imgs[i].size;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
static int mobi_pages(rd_src *s) { (void)s; return 0; }
|
||||
static int mobi_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
|
||||
|
||||
static void parse_flow_to_segs(mobi_impl *m, const unsigned char *data, size_t size,
|
||||
int flow_idx)
|
||||
{
|
||||
(void)flow_idx;
|
||||
rdsb sb; rdsb_init(&sb);
|
||||
motrice c; memset(&c, 0, sizeof c); c.sb = &sb;
|
||||
rdhtml_out out = { mo_text, mo_nl, mo_img, mo_heading };
|
||||
if (rdhtml_parse(data, size, &out, &c, 0) == 0) {
|
||||
rdsb_free(&sb); rdsb_init(&sb);
|
||||
rdhtml_parse(data, size, &out, &c, 1);
|
||||
}
|
||||
while (sb.len && (sb.buf[sb.len-1] == '\n' || sb.buf[sb.len-1] == ' ')) sb.len--;
|
||||
if (c.have_title) { push_toc(m, c.title, m->n_seg); }
|
||||
else { push_toc(m, "无标题", m->n_seg); }
|
||||
/* 按 RDSRC_SEG_MAX 切段存 */
|
||||
size_t off = 0;
|
||||
while (off < sb.len) {
|
||||
uint8_t tmp[RDSRC_SEG_MAX + 8]; size_t nlen = 0, nxt = 0;
|
||||
if (rdsb_chunk(&sb, &off, tmp, sizeof tmp, &nlen, &nxt) != 0) break;
|
||||
push_seg(m, (const char *)tmp, nlen);
|
||||
off = nxt;
|
||||
if (off == 0) break; /* 防死循环 */
|
||||
}
|
||||
rdsb_free(&sb);
|
||||
}
|
||||
|
||||
static int mobi_open(rd_src *s)
|
||||
{
|
||||
mobi_impl *m = (mobi_impl *)calloc(1, sizeof *m);
|
||||
if (!m) return -6;
|
||||
s->impl = m;
|
||||
|
||||
MOBIData *md = mobi_init();
|
||||
if (!md) { mobi_free_src(s); return -6; }
|
||||
if (mobi_load_filename(md, s->path) != MOBI_SUCCESS) { mobi_free(md); mobi_free_src(s); return -2; }
|
||||
size_t ver = mobi_get_fileversion(md);
|
||||
MOBI_RET pr = MOBI_SUCCESS;
|
||||
if (mobi_is_hybrid(md) || ver >= 8) pr = mobi_parse_kf8(md);
|
||||
if (pr != MOBI_SUCCESS) pr = mobi_parse_kf7(md);
|
||||
if (pr != MOBI_SUCCESS) { mobi_free(md); mobi_free_src(s); return -3; }
|
||||
|
||||
MOBIRawml *rawml = mobi_init_rawml(md);
|
||||
if (!rawml) { mobi_free(md); mobi_free_src(s); return -6; }
|
||||
/* 只取 flow(章节 HTML)+resources(图片):不开 TOC/字典/FDST 重建(否则对无 INDX 的最小样本报错) */
|
||||
if (mobi_parse_rawml_opt(rawml, md, false, false, false) != MOBI_SUCCESS) {
|
||||
if (mobi_parse_rawml(rawml, md) != MOBI_SUCCESS) { mobi_free_rawml(rawml); mobi_free(md); mobi_free_src(s); return -4; }
|
||||
}
|
||||
|
||||
char *title = mobi_meta_get_title(md); if (title) { strncpy(s->title, title, sizeof s->title - 1); s->title[sizeof s->title - 1] = 0; free(title); }
|
||||
char *author = mobi_meta_get_author(md); if (author) { strncpy(s->author, author, sizeof s->author - 1); s->author[sizeof s->author - 1] = 0; free(author); }
|
||||
if (!s->title[0]) {
|
||||
char fn[160]; mobi_get_fullname(md, fn, sizeof fn);
|
||||
if (fn[0]) { strncpy(s->title, fn, sizeof s->title - 1); s->title[sizeof s->title - 1] = 0; }
|
||||
}
|
||||
|
||||
/* flow 部件 → 段 */
|
||||
int nflow = 0;
|
||||
for (MOBIPart *p = rawml->flow; p; p = p->next) nflow++;
|
||||
m->flow_first_seg = (int *)calloc((size_t)(nflow ? nflow : 1), sizeof(int));
|
||||
int fi = 0;
|
||||
for (MOBIPart *p = rawml->flow; p; p = p->next, fi++) {
|
||||
if (fi > 0) m->flow_first_seg[fi] = m->n_seg;
|
||||
if (p->data && p->size > 0) parse_flow_to_segs(m, p->data, p->size, fi);
|
||||
}
|
||||
|
||||
/* resources → 图片列表(uid 字符串 key) */
|
||||
for (MOBIPart *p = rawml->resources; p; p = p->next) {
|
||||
if (p->type == T_JPG || p->type == T_GIF || p->type == T_PNG || p->type == T_BMP) {
|
||||
if (p->size == 0 || !p->data) continue;
|
||||
uint8_t *cp = (uint8_t *)malloc(p->size);
|
||||
if (!cp) continue;
|
||||
memcpy(cp, p->data, p->size);
|
||||
char us[24]; snprintf(us, sizeof us, "%zu", p->uid);
|
||||
push_img(m, us, cp, p->size);
|
||||
if (!s->cover[0]) strncpy(s->cover, us, sizeof s->cover - 1);
|
||||
}
|
||||
}
|
||||
|
||||
mobi_free_rawml(rawml);
|
||||
mobi_free(md);
|
||||
if (m->n_seg == 0) { mobi_free_src(s); return -5; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mobi_probe(const uint8_t *head, size_t n)
|
||||
{
|
||||
/* PDB magic */
|
||||
return (n >= 60 && memcmp(head + 60, "BOOKMOBI", 8) == 0);
|
||||
}
|
||||
static const rd_src_vt mobi_vt = {
|
||||
mobi_free_src, mobi_reset, mobi_next, mobi_seg,
|
||||
mobi_toc_n, mobi_toc, mobi_img, mobi_pages, mobi_page,
|
||||
};
|
||||
const rd_src_loader rd_mobi_loader = { RD_FMT_MOBI, ".mobi", mobi_probe, mobi_open, &mobi_vt };
|
||||
@@ -0,0 +1,70 @@
|
||||
/* rdsb.c — 段组装器(实现见 rdsb.h)。 */
|
||||
#include "rdsb.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void rdsb_init(rdsb *b)
|
||||
{
|
||||
if (!b) return;
|
||||
b->buf = NULL; b->len = b->cap = 0;
|
||||
}
|
||||
void rdsb_free(rdsb *b)
|
||||
{
|
||||
if (!b) return;
|
||||
free(b->buf);
|
||||
b->buf = NULL; b->len = b->cap = 0;
|
||||
}
|
||||
static int rdsb_reserve(rdsb *b, size_t extra)
|
||||
{
|
||||
if (b->len + extra <= b->cap) return 0;
|
||||
size_t ncap = b->cap ? b->cap * 2 : 4096;
|
||||
while (ncap < b->len + extra) ncap *= 2;
|
||||
uint8_t *nb = (uint8_t *)realloc(b->buf, ncap);
|
||||
if (!nb) return -1;
|
||||
b->buf = nb; b->cap = ncap;
|
||||
return 0;
|
||||
}
|
||||
int rdsb_put(rdsb *b, const uint8_t *s, size_t n)
|
||||
{
|
||||
if (!b || (!s && n)) return -1;
|
||||
if (rdsb_reserve(b, n) != 0) return -1;
|
||||
memcpy(b->buf + b->len, s, n);
|
||||
b->len += n;
|
||||
return 0;
|
||||
}
|
||||
void rdsb_nl(rdsb *b)
|
||||
{
|
||||
if (!b) return;
|
||||
if (rdsb_reserve(b, 1) != 0) return;
|
||||
b->buf[b->len++] = '\n';
|
||||
}
|
||||
|
||||
int rdsb_chunk(const rdsb *b, size_t *off, uint8_t *out, size_t cap, size_t *outlen, size_t *next_off)
|
||||
{
|
||||
if (!b || !off || !outlen || !next_off) return -1; /* 允许 out==NULL(只取元数据) */
|
||||
if (*off >= b->len) return 1;
|
||||
size_t start = *off;
|
||||
size_t want = start + RDSRC_SEG_MAX < b->len ? start + RDSRC_SEG_MAX : b->len;
|
||||
/* 尽量在 '\n' 处断(段落界);若 want 处即 '\n' 或 start 到 want 无 '\n',则强切 */
|
||||
size_t cut = want;
|
||||
size_t nl = (size_t)-1;
|
||||
for (size_t k = start + 1; k < want; k++) if (b->buf[k] == '\n') nl = k;
|
||||
if (nl != (size_t)-1 && nl > start) cut = nl + 1; /* 落在段后 */
|
||||
else {
|
||||
/* 无 '\n':按最多 want 切,保证 UTF-8 边界 */
|
||||
cut = want;
|
||||
if (cut < b->len) {
|
||||
size_t back = 0;
|
||||
while (back < 3 && (b->buf[cut - 1 - back] & 0xC0) == 0x80) back++;
|
||||
if (back) cut -= back;
|
||||
}
|
||||
}
|
||||
size_t n = cut - start; /* 真实分块长(不随 cap 截断) */
|
||||
if (out && n) {
|
||||
size_t c = n > cap ? cap : n;
|
||||
memcpy(out, b->buf + start, c);
|
||||
}
|
||||
*outlen = n;
|
||||
*next_off = cut;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* rdsb.h — 段组装器:把 rdhtml 事件段组装成「UTF-8 段落流」(\n 分段),并按 RDSRC_SEG_MAX
|
||||
* 在 \n 边界切成 sub-seg 供 rd_src 产出。纯 C,宿主可测。 */
|
||||
#ifndef RDSB_H
|
||||
#define RDSB_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 段(sub-seg)UTF-8 上限,与 R1 rdtxt 的 RDTXT_SEG_UTF8_MAX 对齐(96KB)。 */
|
||||
#define RDSRC_SEG_MAX (96 * 1024)
|
||||
|
||||
typedef struct rdsb {
|
||||
uint8_t *buf;
|
||||
size_t len, cap;
|
||||
} rdsb;
|
||||
|
||||
void rdsb_init(rdsb *b);
|
||||
void rdsb_free(rdsb *b);
|
||||
int rdsb_put(rdsb *b, const uint8_t *s, size_t n); /* 0 ok; 负=oom */
|
||||
void rdsb_nl(rdsb *b); /* 追加 '\n' */
|
||||
|
||||
/* 把 b 的内容从 off 起切一段 ≤RDSRC_SEG_MAX 到 out(cap≥RDSRC_SEG_MAX+8)。
|
||||
* 切分优先落在 '\n'(段落界);无 '\n' 则按最多 RDSRC_SEG_MAX 强切(UTF-8 边界保持)。
|
||||
* 返回 0=正常(*next_off 前进);1=b 已切完;负=错。 */
|
||||
int rdsb_chunk(const rdsb *b, size_t *off, uint8_t *out, size_t cap, size_t *outlen, size_t *next_off);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* RDSB_H */
|
||||
@@ -0,0 +1,176 @@
|
||||
/* rdsrc.c — 统一文档源调度:按扩展名/magic 选格式 loader,开放 rd_src。含 TXT 包装(复用 rdtxt)。
|
||||
* 对外只暴露 rd_src_* 接口(见 rdsrc.h)。
|
||||
*/
|
||||
#include "rdsrc.h"
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdtxt.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
static void stl(char *d, int sz, const char *s)
|
||||
{
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
if (sz > 0) { if (n >= (size_t)sz) n = (size_t)sz - 1; memcpy(d, s, n); d[n] = 0; }
|
||||
}
|
||||
|
||||
/* ---------------- 各格式 loader(由 rdsrc.c 引用,rdsrc_int.h 头) ---------------- */
|
||||
extern const rd_src_loader rd_fb2_loader;
|
||||
extern const rd_src_loader rd_epub_loader;
|
||||
extern const rd_src_loader rd_mobi_loader;
|
||||
extern const rd_src_loader rd_cbz_loader;
|
||||
|
||||
/* ---------------- TXT 包装(复用 rdtxt) ---------------- */
|
||||
typedef struct {
|
||||
rdtxt_info info;
|
||||
size_t *off, *len; int nsegs, cap; /* 段 (file_off,file_len) 表 */
|
||||
int cur;
|
||||
} txt_impl;
|
||||
|
||||
static void txt_free(rd_src *s)
|
||||
{
|
||||
txt_impl *m = (txt_impl *)s->impl;
|
||||
if (!m) return;
|
||||
free(m->off); free(m->len);
|
||||
free(m);
|
||||
s->impl = NULL;
|
||||
}
|
||||
static int txt_open(rd_src *s)
|
||||
{
|
||||
txt_impl *m = (txt_impl *)calloc(1, sizeof *m);
|
||||
if (!m) return -6;
|
||||
s->impl = m;
|
||||
if (rdtxt_probe(s->path, &m->info) != 0) { txt_free(s); return -1; }
|
||||
/* 建段表:一次扫描(off,len) */
|
||||
rdtxt_reader *r = NULL;
|
||||
if (rdtxt_reader_open(s->path, &m->info, &r) != 0) { txt_free(s); return -2; }
|
||||
uint8_t *scratch = (uint8_t *)malloc(RDTXT_SEG_UTF8_MAX + 8);
|
||||
if (!scratch) { rdtxt_reader_free(r); txt_free(s); return -6; }
|
||||
size_t ul, off, flen;
|
||||
while (rdtxt_reader_next(r, scratch, RDTXT_SEG_UTF8_MAX + 8, &ul, &off, &flen) == 0) {
|
||||
if (m->nsegs >= m->cap) {
|
||||
int nc = m->cap ? m->cap * 2 : 64;
|
||||
size_t *no = (size_t *)realloc(m->off, (size_t)nc * sizeof(size_t));
|
||||
size_t *nl = (size_t *)realloc(m->len, (size_t)nc * sizeof(size_t));
|
||||
if (!no || !nl) break;
|
||||
m->off = no; m->len = nl; m->cap = nc;
|
||||
}
|
||||
m->off[m->nsegs] = off; m->len[m->nsegs] = flen; m->nsegs++;
|
||||
}
|
||||
free(scratch);
|
||||
rdtxt_reader_free(r);
|
||||
/* 标题 = 文件名 */
|
||||
const char *sl = strrchr(s->path, '/');
|
||||
stl(s->title, sizeof s->title, sl ? sl + 1 : s->path);
|
||||
char *dot = strrchr(s->title, '.'); if (dot) *dot = 0;
|
||||
return 0;
|
||||
}
|
||||
static int txt_reset(rd_src *s) { ((txt_impl *)s->impl)->cur = 0; return 0; }
|
||||
static int txt_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
txt_impl *m = (txt_impl *)s->impl;
|
||||
if (m->cur >= m->nsegs) return 1;
|
||||
size_t off = m->off[m->cur], flen = m->len[m->cur]; m->cur++;
|
||||
int rc = rdtxt_convert_span(s->path, m->info.enc, off, flen, buf, cap, len);
|
||||
return (rc == RD_ENC_OK || rc == RD_ENC_ERR_BUFSZ) ? 0 : -3;
|
||||
}
|
||||
static int txt_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
txt_impl *m = (txt_impl *)s->impl;
|
||||
if (idx < 0 || idx >= m->nsegs) return 1;
|
||||
int rc = rdtxt_convert_span(s->path, m->info.enc, m->off[idx], m->len[idx], buf, cap, len);
|
||||
return (rc == RD_ENC_OK || rc == RD_ENC_ERR_BUFSZ) ? 0 : -3;
|
||||
}
|
||||
static int txt_toc_n(rd_src *s) { (void)s; return 0; }
|
||||
static int txt_toc(rd_src *s, int i, rd_toc_entry *t) { (void)s;(void)i;(void)t; return -1; }
|
||||
static int txt_img(rd_src *s, const char *ref, uint8_t **data, size_t *size) { (void)s;(void)ref;(void)data;(void)size; return -1; }
|
||||
static int txt_pages(rd_src *s) { (void)s; return 0; }
|
||||
static int txt_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
|
||||
static const rd_src_vt txt_vt = {
|
||||
txt_free, txt_reset, txt_next, txt_seg, txt_toc_n, txt_toc, txt_img, txt_pages, txt_page,
|
||||
};
|
||||
static int txt_probe(const uint8_t *head, size_t n) { (void)head; (void)n; return 0; }
|
||||
static const rd_src_loader rd_txt_loader = { RD_FMT_TXT, ".txt", txt_probe, txt_open, &txt_vt };
|
||||
|
||||
|
||||
/* ---------------- 调度 ---------------- */
|
||||
static const rd_src_loader *g_loaders[] = {
|
||||
&rd_txt_loader, &rd_fb2_loader, &rd_epub_loader, &rd_mobi_loader, &rd_cbz_loader,
|
||||
};
|
||||
#define N_LOADERS ((int)(sizeof g_loaders / sizeof g_loaders[0]))
|
||||
|
||||
static int is_ext(const char *path, const char *ext)
|
||||
{
|
||||
size_t pl = strlen(path), el = strlen(ext);
|
||||
if (el == 0 || pl < el) return 0;
|
||||
return strcasecmp(path + pl - el, ext) == 0; /* ext 含点(如 ".cbz") */
|
||||
}
|
||||
|
||||
int rd_src_open(rd_src **out, const char *path)
|
||||
{
|
||||
if (!out || !path) return -1;
|
||||
/* 读前 64 字节做 magic 探测 */
|
||||
uint8_t head[64]; size_t hn = 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f) { hn = fread(head, 1, sizeof head, f); fclose(f); }
|
||||
|
||||
const rd_src_loader *pick = NULL;
|
||||
for (int i = 0; i < N_LOADERS; i++)
|
||||
if (g_loaders[i]->ext && is_ext(path, g_loaders[i]->ext)) { pick = g_loaders[i]; break; }
|
||||
if (!pick) {
|
||||
for (int i = 0; i < N_LOADERS; i++)
|
||||
if (g_loaders[i]->probe && g_loaders[i]->probe(head, hn)) { pick = g_loaders[i]; break; }
|
||||
}
|
||||
if (!pick) return -9;
|
||||
|
||||
rd_src *s = (rd_src *)calloc(1, sizeof *s);
|
||||
if (!s) return -6;
|
||||
if (strlen(path) >= sizeof s->path) { free(s); return -2; }
|
||||
strcpy(s->path, path);
|
||||
s->fmt = pick->fmt;
|
||||
int rc = pick->open(s);
|
||||
if (rc != 0) { s->vt = NULL; s->impl = NULL; rd_src_free(s); return rc; }
|
||||
s->vt = pick->vt;
|
||||
*out = s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void rd_src_free(rd_src *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (s->vt && s->vt->vt_free) s->vt->vt_free(s);
|
||||
free(s);
|
||||
}
|
||||
rd_fmt_t rd_src_fmt(const rd_src *s) { return s ? s->fmt : RD_FMT_TXT; }
|
||||
const char *rd_src_title(const rd_src *s) { return (s && s->title[0]) ? s->title : (s ? s->path : ""); }
|
||||
const char *rd_src_author(const rd_src *s) { return (s && s->author[0]) ? s->author : "佚名"; }
|
||||
const char *rd_src_cover(const rd_src *s) { return (s && s->cover[0]) ? s->cover : NULL; }
|
||||
|
||||
int rd_src_reset(rd_src *s) { return (s && s->vt && s->vt->vt_reset) ? s->vt->vt_reset(s) : 0; }
|
||||
int rd_src_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_next) ? s->vt->vt_next(s, buf, cap, len) : 1;
|
||||
}
|
||||
int rd_src_seg(const rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_seg) ? s->vt->vt_seg((rd_src *)s, idx, buf, cap, len) : 1;
|
||||
}
|
||||
int rd_src_toc_n(const rd_src *s)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_toc_n) ? s->vt->vt_toc_n((rd_src *)s) : 0;
|
||||
}
|
||||
int rd_src_toc(const rd_src *s, int i, rd_toc_entry *t)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_toc) ? s->vt->vt_toc((rd_src *)s, i, t) : -1;
|
||||
}
|
||||
int rd_src_img(const rd_src *s, const char *ref, uint8_t **data, size_t *size)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_img) ? s->vt->vt_img((rd_src *)s, ref, data, size) : -1;
|
||||
}
|
||||
int rd_src_pages(const rd_src *s)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_pages) ? s->vt->vt_pages((rd_src *)s) : 0;
|
||||
}
|
||||
int rd_src_page(const rd_src *s, int i, uint8_t **data, size_t *size)
|
||||
{
|
||||
return (s && s->vt && s->vt->vt_page) ? s->vt->vt_page((rd_src *)s, i, data, size) : -1;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/* rdsrc.h — 统一「文档源」抽象:把 TXT/FB2/EPUB/MOBI/CBZ 都变成可喂排版器的「UTF-8 段序列 +
|
||||
* 目录 + 元数据 + 图片访问」。契约对齐 R1 的 rdtxt(UTF-8 段 + {file_off,file_len}),但段重显
|
||||
* 按「段下标」(跨格式统一),锚定 (段,字符偏移) 不变。
|
||||
*
|
||||
* 各格式 loader 产出:
|
||||
* - 段序列:rd_src_next/rd_src_seg 确定性产出同一 UTF-8 段(段 = 章节/章节分片);
|
||||
* - 目录:EPUB NCX/nav、FB2 section title、MOBI toc → (title, seg, ch);
|
||||
* - 图片:EPUB/FB2/MOBI 内嵌/封面 raw 数据;CBZ 页面序列;
|
||||
* - 元数据:title/author/cover。
|
||||
* 内存纪律:loader 只保留「目录/段表/图片索引」这类轻量结构 + ZIP/PDB 句柄;正文文本按需
|
||||
* 重解析重显,不驻留全书文本 → EPUB 大书不建全 DOM。
|
||||
*/
|
||||
#ifndef RDSRC_H
|
||||
#define RDSRC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
RD_FMT_TXT = 0,
|
||||
RD_FMT_FB2 = 1,
|
||||
RD_FMT_EPUB = 2,
|
||||
RD_FMT_MOBI = 3,
|
||||
RD_FMT_CBZ = 4,
|
||||
} rd_fmt_t;
|
||||
|
||||
typedef struct rd_src rd_src;
|
||||
|
||||
typedef struct rd_toc_entry {
|
||||
char title[160];
|
||||
int seg; /* 目标段下标 */
|
||||
int32_t ch; /* 段内字符偏移 */
|
||||
} rd_toc_entry;
|
||||
|
||||
/* 打开(按扩展名/magic 探测格式)。0=成功;负值=不支持/打不开。 */
|
||||
int rd_src_open(rd_src **out, const char *path);
|
||||
void rd_src_free(rd_src *s);
|
||||
|
||||
rd_fmt_t rd_src_fmt(const rd_src *s);
|
||||
const char *rd_src_title(const rd_src *s);
|
||||
const char *rd_src_author(const rd_src *s);
|
||||
const char *rd_src_cover(const rd_src *s); /* 封面 ref;无则 NULL */
|
||||
|
||||
/* 段迭代(建索引用):reset 后反复 next。0=成功(len=该段 UTF-8 字节);1=EOF;负值=错。 */
|
||||
int rd_src_reset(rd_src *s);
|
||||
int rd_src_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len);
|
||||
|
||||
/* 按段下标重显(渲染用;与迭代产出逐字节一致)。0=ok;1=越界/空。 */
|
||||
int rd_src_seg(const rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len);
|
||||
|
||||
/* 目录。0=无目录。 */
|
||||
int rd_src_toc_n(const rd_src *s);
|
||||
int rd_src_toc(const rd_src *s, int i, rd_toc_entry *t);
|
||||
|
||||
/* 图片:按 ref(href/src/id)取原始数据(EPUB/FB2/MOBI)。0=ok;*data 为 malloc 缓冲,调用方
|
||||
* free(原子字节;解码交给调用方,如 stb_image)。 */
|
||||
int rd_src_img(const rd_src *s, const char *ref, uint8_t **data, size_t *size);
|
||||
|
||||
/* CBZ:页数(非 CBZ=0)+ 第 i 页原始图片数据(malloc,调用方 free)。 */
|
||||
int rd_src_pages(const rd_src *s);
|
||||
int rd_src_page(const rd_src *s, int i, uint8_t **data, size_t *size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* RDSRC_H */
|
||||
@@ -0,0 +1,45 @@
|
||||
/* rdsrc_int.h — rd_src 内部结构 + 格式 loader 注册表(仅供 core/rdsrc.c 及 core/rd*.c 引用)。
|
||||
* 不对外。 */
|
||||
#ifndef RDSRC_INT_H
|
||||
#define RDSRC_INT_H
|
||||
|
||||
#include "rdsrc.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct rd_src rd_src;
|
||||
|
||||
typedef struct rd_src_vt {
|
||||
void (*vt_free)(rd_src *s);
|
||||
int (*vt_reset)(rd_src *s);
|
||||
int (*vt_next)(rd_src *s, uint8_t *buf, size_t cap, size_t *len);
|
||||
int (*vt_seg)(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len);
|
||||
int (*vt_toc_n)(rd_src *s);
|
||||
int (*vt_toc)(rd_src *s, int i, rd_toc_entry *t);
|
||||
int (*vt_img)(rd_src *s, const char *ref, uint8_t **data, size_t *size);
|
||||
int (*vt_pages)(rd_src *s);
|
||||
int (*vt_page)(rd_src *s, int i, uint8_t **data, size_t *size);
|
||||
} rd_src_vt;
|
||||
|
||||
struct rd_src {
|
||||
const rd_src_vt *vt;
|
||||
rd_fmt_t fmt;
|
||||
char path[400];
|
||||
char title[160];
|
||||
char author[160];
|
||||
char cover[320];
|
||||
void *impl;
|
||||
};
|
||||
|
||||
/* 注册一个格式(供 rdsrc.c 调度)。 */
|
||||
typedef struct rd_src_loader {
|
||||
rd_fmt_t fmt;
|
||||
const char *ext; /* 点扩展名(小写);NULL 表示不靠扩展名 */
|
||||
/* 探测 magic(前若干字节返回 1=命中);可为 NULL。 */
|
||||
int (*probe)(const uint8_t *head, size_t n);
|
||||
int (*open)(rd_src *s); /* s->path 已填;fails: 返回负 */
|
||||
const rd_src_vt *vt; /* 成功打开后由 rdsrc.c 赋给 s->vt */
|
||||
} rd_src_loader;
|
||||
|
||||
|
||||
#endif /* RDSRC_INT_H */
|
||||
@@ -0,0 +1,309 @@
|
||||
/* rdxml.c — 极简纯 C XML 回调解析器(实现见 rdxml.h)。 */
|
||||
#include "rdxml.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ---------------- 实体解码 ---------------- */
|
||||
typedef struct { const char *name; uint16_t cp; } ent_t;
|
||||
static const ent_t ent_table[] = {
|
||||
{ "amp", 0x26 }, { "lt", 0x3C }, { "gt", 0x3E }, { "quot", 0x22 },
|
||||
{ "apos", 0x27 }, { "nbsp", 0xA0 }, { "copy", 0xA9 }, { "reg", 0xAE },
|
||||
{ "trade", 0x2122 }, { "hellip", 0x2026 }, { "mdash", 0x2014 },
|
||||
{ "ndash", 0x2013 }, { "laquo", 0xAB }, { "raquo", 0xBB },
|
||||
{ "lsquo", 0x2018 }, { "rsquo", 0x2019 }, { "ldquo", 0x201C },
|
||||
{ "rdquo", 0x201D }, { "times", 0xD7 }, { "divide", 0xF7 },
|
||||
{ "bull", 0x2022 }, { "middot", 0xB7 }, { "deg", 0xB0 },
|
||||
};
|
||||
#define ENT_N ((int)(sizeof ent_table / sizeof ent_table[0]))
|
||||
|
||||
/* 码点 → UTF-8,返回字节数。 */
|
||||
static int ent_put_utf8(uint32_t cp, uint8_t *o)
|
||||
{
|
||||
if (cp <= 0x7F) { o[0] = (uint8_t)cp; return 1; }
|
||||
if (cp <= 0x7FF) {
|
||||
o[0] = (uint8_t)(0xC0 | (cp >> 6));
|
||||
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||
return 2;
|
||||
}
|
||||
o[0] = (uint8_t)(0xE0 | (cp >> 12));
|
||||
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||
return 3;
|
||||
}
|
||||
|
||||
int rd_ent_decode(const uint8_t *s, size_t len, uint8_t *out, size_t cap)
|
||||
{
|
||||
if (!s || !out || len < 2 || s[0] != '&') return -1;
|
||||
/* 找 ';'(限 len 内) */
|
||||
size_t semi = (size_t)-1;
|
||||
for (size_t i = 1; i < len; i++) if (s[i] == ';') { semi = i; break; }
|
||||
if (semi == (size_t)-1 || semi - 1 == 0) return -1;
|
||||
|
||||
uint32_t cp;
|
||||
if (s[1] == '#') {
|
||||
if (semi - 1 < 2) return -1;
|
||||
if (s[2] == 'x' || s[2] == 'X') {
|
||||
cp = 0;
|
||||
for (size_t i = 3; i < semi; i++) {
|
||||
uint8_t c = s[i];
|
||||
int d = (c >= '0' && c <= '9') ? c - '0'
|
||||
: (c >= 'a' && c <= 'f') ? c - 'a' + 10
|
||||
: (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1;
|
||||
if (d < 0) return -1;
|
||||
cp = cp * 16 + (uint32_t)d;
|
||||
}
|
||||
} else {
|
||||
cp = 0;
|
||||
for (size_t i = 2; i < semi; i++) {
|
||||
uint8_t c = s[i];
|
||||
if (c < '0' || c > '9') return -1;
|
||||
cp = cp * 10 + (uint32_t)(c - '0');
|
||||
}
|
||||
}
|
||||
if (cp == 0 || cp > 0x10FFFF) return -1;
|
||||
} else {
|
||||
size_t name_len = semi - 1;
|
||||
int found = -1;
|
||||
for (int k = 0; k < ENT_N; k++) {
|
||||
if (strlen(ent_table[k].name) == name_len &&
|
||||
memcmp(ent_table[k].name, s + 1, name_len) == 0) { found = k; break; }
|
||||
}
|
||||
if (found < 0) return -1;
|
||||
cp = ent_table[found].cp;
|
||||
}
|
||||
uint8_t tmp[4];
|
||||
int n = ent_put_utf8(cp, tmp);
|
||||
if (n < 0 || (size_t)n > cap) return -1;
|
||||
memcpy(out, tmp, (size_t)n);
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t rd_ent_decode_to(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap)
|
||||
{
|
||||
if (!in || !out) return 0;
|
||||
size_t o = 0, i = 0;
|
||||
while (i < inlen) {
|
||||
if (in[i] == '&') {
|
||||
uint8_t tmp[4];
|
||||
int n = rd_ent_decode(in + i, inlen - i, tmp, sizeof tmp);
|
||||
if (n > 0) {
|
||||
if (o + (size_t)n > cap) break;
|
||||
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
|
||||
i++;
|
||||
while (i < inlen && in[i] != ';') i++;
|
||||
i++; /* 跳过 ';' */
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (o + 1 > cap) break;
|
||||
out[o++] = in[i++];
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
int rd_b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap, size_t *outlen)
|
||||
{
|
||||
if (!in || !out) return -1;
|
||||
static const int8_t tbl[256] = {
|
||||
/* 0-31 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
/* 32-47 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
|
||||
/* 48-63 */ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
|
||||
/* 64-79 */ -1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
|
||||
/* 80-95 */ 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
|
||||
/* 96-111 */ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
|
||||
/* 112-127 */ 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
|
||||
/* 128-255 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
|
||||
};
|
||||
size_t o = 0;
|
||||
uint32_t acc = 0; int nbits = 0;
|
||||
for (size_t i = 0; i < inlen; i++) {
|
||||
uint8_t c = in[i];
|
||||
if (c == '=') break;
|
||||
if (c == '\r' || c == '\n' || c == ' ' || c == '\t') continue;
|
||||
if (c >= 128 || tbl[c] < 0) continue; /* 非法字符跳过(宽容) */
|
||||
acc = (acc << 6) | (uint32_t)tbl[c];
|
||||
nbits += 6;
|
||||
if (nbits >= 8) {
|
||||
nbits -= 8;
|
||||
if (o + 1 > cap) { *outlen = o; return 0; }
|
||||
out[o++] = (uint8_t)((acc >> nbits) & 0xFF);
|
||||
}
|
||||
}
|
||||
if (outlen) *outlen = o;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------- 解析器 ---------------- */
|
||||
typedef struct {
|
||||
const uint8_t *p; size_t len; size_t i;
|
||||
const rdxml_handler *h; void *ctx;
|
||||
int depth;
|
||||
} xmlctx;
|
||||
|
||||
/* 名字:取 ':' 后局部名,拷贝到 out(≤64)。 */
|
||||
static void local_name(const uint8_t *s, size_t n, char *out, size_t outsz)
|
||||
{
|
||||
const uint8_t *col = NULL;
|
||||
for (size_t k = 0; k < n && s[k] != ' ' && s[k] != '\t' && s[k] != '\r' &&
|
||||
s[k] != '\n' && s[k] != '/' && s[k] != '>'; k++)
|
||||
if (s[k] == ':') { col = s + k; break; }
|
||||
size_t start = col ? (size_t)(col - s) + 1 : 0;
|
||||
size_t end = start;
|
||||
while (end < n && s[end] != ' ' && s[end] != '\t' && s[end] != '\r' &&
|
||||
s[end] != '\n' && s[end] != '/' && s[end] != '>' && end < outsz - 1)
|
||||
end++;
|
||||
size_t L = end - start;
|
||||
if (L > outsz - 1) L = outsz - 1;
|
||||
memcpy(out, s + start, L); out[L] = 0;
|
||||
}
|
||||
|
||||
/* 解析一个开放标签('<' 已读过,i 指向名字首)。处理至 '>'。 */
|
||||
static void parse_open(xmlctx *x)
|
||||
{
|
||||
size_t name_start = x->i;
|
||||
size_t j = x->i;
|
||||
while (j < x->len && x->p[j] != ' ' && x->p[j] != '\t' && x->p[j] != '\r' &&
|
||||
x->p[j] != '\n' && x->p[j] != '/' && x->p[j] != '>') j++;
|
||||
char nm[64]; local_name(x->p + name_start, j - name_start, nm, sizeof nm);
|
||||
|
||||
/* 属性循环 */
|
||||
rdxml_attr attrs[32]; int nattrs = 0;
|
||||
size_t k = j;
|
||||
int selfclose = 0;
|
||||
while (k < x->len) {
|
||||
while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t' || x->p[k] == '\r' || x->p[k] == '\n')) k++;
|
||||
if (k >= x->len) break;
|
||||
if (x->p[k] == '/') { if (k + 1 < x->len && x->p[k + 1] == '>') selfclose = 1; break; }
|
||||
if (x->p[k] == '>') break;
|
||||
/* 属性名 */
|
||||
size_t an = k;
|
||||
while (k < x->len && x->p[k] != ' ' && x->p[k] != '\t' && x->p[k] != '=' && x->p[k] != '>') k++;
|
||||
char anm[64]; local_name(x->p + an, k - an, anm, sizeof anm);
|
||||
while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t')) k++;
|
||||
if (k < x->len && x->p[k] == '=') {
|
||||
k++;
|
||||
while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t')) k++;
|
||||
if (k < x->len && (x->p[k] == '"' || x->p[k] == '\'')) {
|
||||
uint8_t q = x->p[k]; k++;
|
||||
size_t vs = k;
|
||||
while (k < x->len && x->p[k] != q) k++;
|
||||
if (nattrs < 32 && anm[0]) {
|
||||
rdxml_attr *a = &attrs[nattrs];
|
||||
size_t vlen = k - vs;
|
||||
size_t dec = rd_ent_decode_to(x->p + vs, vlen, (uint8_t *)a->value, sizeof a->value);
|
||||
if (dec >= sizeof a->value) dec = sizeof a->value - 1;
|
||||
a->value[dec] = 0;
|
||||
{ size_t L = strlen(anm); if (L >= sizeof a->name) L = sizeof a->name - 1; memcpy(a->name, anm, L); a->name[L] = 0; }
|
||||
nattrs++;
|
||||
}
|
||||
if (k < x->len && x->p[k] == q) k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 找 '>' 结束 */
|
||||
while (k < x->len && x->p[k] != '>') k++;
|
||||
if (k < x->len) k++; /* 跳过 '>' */
|
||||
x->i = k;
|
||||
|
||||
if (nm[0]) {
|
||||
if (x->h && x->h->on_open) x->h->on_open(nm, nattrs ? attrs : NULL, nattrs, x->ctx);
|
||||
x->depth++;
|
||||
if (selfclose && x->h && x->h->on_close) x->h->on_close(nm, x->ctx);
|
||||
if (selfclose) x->depth--;
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_close(xmlctx *x)
|
||||
{
|
||||
size_t j = x->i;
|
||||
while (j < x->len && x->p[j] != '>') j++;
|
||||
char nm[64];
|
||||
size_t name_start = x->i;
|
||||
size_t name_end = j;
|
||||
while (name_end > name_start && (x->p[name_end - 1] == ' ' || x->p[name_end - 1] == '\t')) name_end--;
|
||||
local_name(x->p + name_start, name_end - name_start, nm, sizeof nm);
|
||||
x->i = (j < x->len) ? j + 1 : j;
|
||||
if (nm[0]) {
|
||||
if (x->depth > 0) x->depth--;
|
||||
if (x->h && x->h->on_close) x->h->on_close(nm, x->ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static void emit_text(xmlctx *x, size_t start, size_t end)
|
||||
{
|
||||
if (end <= start) return;
|
||||
/* 纯空白 → 仍回调(调用方按需忽略);但跳过 CDATA/PI 场景不在此 */
|
||||
if (x->h && x->h->on_text)
|
||||
x->h->on_text((const char *)x->p + start, end - start, x->ctx);
|
||||
}
|
||||
|
||||
int rdxml_parse(const uint8_t *xml, size_t len, const rdxml_handler *h, void *ctx)
|
||||
{
|
||||
if (!xml || !h) return -1;
|
||||
xmlctx x; memset(&x, 0, sizeof x);
|
||||
x.p = xml; x.len = len; x.h = h; x.ctx = ctx;
|
||||
|
||||
while (x.i < x.len) {
|
||||
uint8_t c = x.p[x.i];
|
||||
if (c == '<') {
|
||||
/* PI */
|
||||
if (x.i + 1 < x.len && x.p[x.i + 1] == '?') {
|
||||
size_t j = x.i + 2;
|
||||
while (j + 1 < x.len && !(x.p[j] == '?' && x.p[j + 1] == '>')) j++;
|
||||
x.i = (j + 1 < x.len) ? j + 2 : x.len;
|
||||
continue;
|
||||
}
|
||||
/* 注释 */
|
||||
if (x.i + 3 < x.len && memcmp(x.p + x.i, "<!--", 4) == 0) {
|
||||
size_t j = x.i + 4;
|
||||
while (j + 2 < x.len && !(x.p[j] == '-' && x.p[j + 1] == '-' && x.p[j + 2] == '>')) j++;
|
||||
x.i = (j + 2 < x.len) ? j + 3 : x.len;
|
||||
continue;
|
||||
}
|
||||
/* CDATA */
|
||||
if (x.i + 8 < x.len && memcmp(x.p + x.i, "<![CDATA[", 9) == 0) {
|
||||
size_t start = x.i + 9;
|
||||
size_t j = start;
|
||||
while (j + 2 < x.len && !(x.p[j] == ']' && x.p[j + 1] == ']' && x.p[j + 2] == '>')) j++;
|
||||
emit_text(&x, start, j);
|
||||
x.i = (j + 2 < x.len) ? j + 3 : x.len;
|
||||
continue;
|
||||
}
|
||||
/* DOCTYPE */
|
||||
if (x.i + 1 < x.len && x.p[x.i + 1] == '!') {
|
||||
/* 跳过到 '>',内部子集用方括号配平 */
|
||||
size_t j = x.i + 2; int bracket = 0;
|
||||
while (j < x.len) {
|
||||
if (x.p[j] == '[') bracket++;
|
||||
else if (x.p[j] == ']') bracket--;
|
||||
else if (x.p[j] == '>' && bracket <= 0) break;
|
||||
j++;
|
||||
}
|
||||
x.i = (j < x.len) ? j + 1 : x.len;
|
||||
continue;
|
||||
}
|
||||
/* 关闭标签 */
|
||||
if (x.i + 1 < x.len && x.p[x.i + 1] == '/') {
|
||||
x.i += 2;
|
||||
parse_close(&x);
|
||||
continue;
|
||||
}
|
||||
/* 开放标签 */
|
||||
x.i += 1;
|
||||
parse_open(&x);
|
||||
continue;
|
||||
}
|
||||
/* 文本:累积到下一个 '<' */
|
||||
size_t start = x.i;
|
||||
while (x.i < x.len && x.p[x.i] != '<') x.i++;
|
||||
emit_text(&x, start, x.i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* rdxml.h — 极简纯 C XML 回调解析器(OPF/NCX/FB2 用;替代 pugixml)。
|
||||
*
|
||||
* 为什么自写(见 READER_ENGINE_NOTES 的偏离):
|
||||
* 工具链是单一 C 编译器(zig-cc / gcc),pugixml 是 C++(需 C++ TU),
|
||||
* 而 FB2/OPF/NCX 都是严格 XML,只需「抽结构」不必建 DOM —— SAX 式回调足够且更省内存。
|
||||
*
|
||||
* 设计:一次扫描,回调 on_open/on_close/on_text,命名空间前缀剥离(比 ':' 取局部名),
|
||||
* 属性值/文本里的实体解码。容错:坏 XML 尽量跳过而非 abort(EPUB 的 OPF 偶有污点)。
|
||||
* 不校验 DTD/嵌套合法(调用方按需)。
|
||||
*
|
||||
* 另导出实体解码 + base64 解码(rdhtml.c 也复用)。纯 C,宿主可测,无 SDK 依赖。
|
||||
*/
|
||||
#ifndef RDXML_H
|
||||
#define RDXML_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct rdxml_attr {
|
||||
char name[64]; /* 局部名(已剥命名空间前缀,小写不变) */
|
||||
char value[320]; /* 已解码值 */
|
||||
} rdxml_attr;
|
||||
|
||||
typedef struct rdxml_handler {
|
||||
/* name 为局部名(已剥前缀)。attrs 可为 NULL(nattrs==0)。 */
|
||||
void (*on_open)(const char *name, const rdxml_attr *attrs, int nattrs, void *ctx);
|
||||
void (*on_close)(const char *name, void *ctx);
|
||||
void (*on_text)(const char *text, size_t len, void *ctx); /* len 字节,非 NUL 结尾 */
|
||||
} rdxml_handler;
|
||||
|
||||
/* 解析 XML 片段 [xml, xml+len)。调用回调。返回 0=正常结束;-1=严重畸形(尽量恢复)。 */
|
||||
int rdxml_parse(const uint8_t *xml, size_t len, const rdxml_handler *h, void *ctx);
|
||||
|
||||
/* ---- 共享工具(rdhtml.c 复用)---- */
|
||||
|
||||
/* 解码一个实体:输入 s[0..len) 以 '&' 开头。写出到 out(cap),返回写入字节数;未识别实体返回 -1。
|
||||
* 支持:& < > " ' &#NNN; &#xHH; 及其它常见 HTML 实体按名映射。 */
|
||||
int rd_ent_decode(const uint8_t *s, size_t len, uint8_t *out, size_t cap);
|
||||
|
||||
/* 把一段文本里的实体替换为 UTF-8 输出到 out(cap)。返回输出字节数;out 不足的字节被截断。 */
|
||||
size_t rd_ent_decode_to(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap);
|
||||
|
||||
/* base64 解码(FB2 内嵌图)。返回解码字节数写 out(cap);*outlen 不为 NULL 时填实际解码长。
|
||||
* -1=非法过短。支持空白/换行跳过。 */
|
||||
int rd_b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap, size_t *outlen);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* RDXML_H */
|
||||
@@ -0,0 +1,82 @@
|
||||
/* rdzip.c — ZIP 容器读取(miniz 封装,实现见 rdzip.h)。 */
|
||||
#include "rdzip.h"
|
||||
#include "miniz.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct rdzip {
|
||||
mz_zip_archive za;
|
||||
int num;
|
||||
};
|
||||
|
||||
static int stat_of(const rdzip *z, int idx, mz_zip_archive_file_stat *st)
|
||||
{
|
||||
if (idx < 0 || idx >= z->num) return 0;
|
||||
return mz_zip_reader_file_stat((mz_zip_archive *)&z->za, (mz_uint)idx, st);
|
||||
}
|
||||
|
||||
int rdzip_open(rdzip **z, const char *path)
|
||||
{
|
||||
if (!z || !path) return -1;
|
||||
rdzip *x = (rdzip *)calloc(1, sizeof *x);
|
||||
if (!x) return -6;
|
||||
memset(&x->za, 0, sizeof x->za);
|
||||
if (!mz_zip_reader_init_file(&x->za, path, 0)) { free(x); return -2; }
|
||||
x->num = (int)mz_zip_reader_get_num_files(&x->za);
|
||||
*z = x;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void rdzip_close(rdzip *z)
|
||||
{
|
||||
if (!z) return;
|
||||
mz_zip_reader_end(&z->za);
|
||||
free(z);
|
||||
}
|
||||
|
||||
int rdzip_count(const rdzip *z) { return z ? z->num : 0; }
|
||||
|
||||
int rdzip_name(const rdzip *z, int idx, char *out, int cap)
|
||||
{
|
||||
if (!z || !out || cap <= 0) return -1;
|
||||
mz_zip_archive_file_stat st; memset(&st, 0, sizeof st);
|
||||
if (!stat_of(z, idx, &st)) return -2;
|
||||
size_t n = strnlen(st.m_filename, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE);
|
||||
if (n >= (size_t)cap) n = (size_t)cap - 1;
|
||||
memcpy(out, st.m_filename, n); out[n] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rdzip_find(const rdzip *z, const char *name)
|
||||
{
|
||||
if (!z || !name) return -1;
|
||||
for (int i = 0; i < z->num; i++) {
|
||||
mz_zip_archive_file_stat st; memset(&st, 0, sizeof st);
|
||||
if (!stat_of(z, i, &st)) continue;
|
||||
size_t n = strnlen(st.m_filename, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE);
|
||||
if (n == strlen(name) && memcmp(st.m_filename, name, n) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rdzip_read(const rdzip *z, int idx, uint8_t **data, size_t *size)
|
||||
{
|
||||
if (!z || !data || !size) return -1;
|
||||
*data = NULL; *size = 0;
|
||||
if (idx < 0 || idx >= z->num) return -1;
|
||||
mz_zip_archive_file_stat st; memset(&st, 0, sizeof st);
|
||||
if (!stat_of(z, idx, &st)) return -2;
|
||||
if (st.m_uncomp_size == 0) return 1;
|
||||
size_t sz = 0;
|
||||
void *p = mz_zip_reader_extract_to_heap((mz_zip_archive *)&z->za, (mz_uint)idx, &sz, 0);
|
||||
if (!p) return -3;
|
||||
*data = (uint8_t *)p; *size = sz;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rdzip_read_named(const rdzip *z, const char *name, uint8_t **data, size_t *size)
|
||||
{
|
||||
int idx = rdzip_find(z, name);
|
||||
if (idx < 0) return -1;
|
||||
return rdzip_read(z, idx, data, size);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* rdzip.h — ZIP 容器读取(EPUB/CBZ 用;miniz 封装,纯 C)。
|
||||
*
|
||||
* 内存纪律:从**文件**打开 ZIP(mz_zip_reader_init_file),按需抽单条目到堆(用完即释),
|
||||
* 不一整本读进 RAM(对齐 EPUB 大书不建全 DOM;ZIP 磁盘驻留)。条目名按规范原始大小写匹配。
|
||||
*/
|
||||
#ifndef RDZIP_H
|
||||
#define RDZIP_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct rdzip rdzip;
|
||||
|
||||
/* 打开 ZIP(path)。0=成功;负值=错误(非 zip / 打不开)。 */
|
||||
int rdzip_open(rdzip **z, const char *path);
|
||||
void rdzip_close(rdzip *z);
|
||||
|
||||
/* 条目数。 */
|
||||
int rdzip_count(const rdzip *z);
|
||||
|
||||
/* 第 idx 个条目名(写 out,≤cap)。0=成功;负值=越界/过长。 */
|
||||
int rdzip_name(const rdzip *z, int idx, char *out, int cap);
|
||||
|
||||
/* 按完整条目名精确查找(大小写敏感)。返回下标;-1=未找到。 */
|
||||
int rdzip_find(const rdzip *z, const char *name);
|
||||
|
||||
/* 读取第 idx 条目解压后数据到堆(*data 由调用方 free)。0=成功;1=条目为空;负值=错。 */
|
||||
int rdzip_read(const rdzip *z, int idx, uint8_t **data, size_t *size);
|
||||
|
||||
/* 便捷:按名字读。0=成功;-1=未找到;负值=读错。 */
|
||||
int rdzip_read_named(const rdzip *z, const char *name, uint8_t **data, size_t *size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* RDZIP_H */
|
||||
Reference in new issue
Block a user