code wiki / _hdl_build / nx_cbx_pages.nx
nx_cbx_pages.nx source
↩ module page · 800 lines · 37892 B
1// nx_cbx_pages.nx -- SOVEREIGN CBX page-image extraction (Reader-arc RUNG 3, FORMAT-FIX rev "cfix":2).
2//
3// The operator headline ask: "convert pdf into cbx type zoom" so books read easily on mobile; then
4// "the formats are broken like for the comics" -- this rev EATS that debt. CBX = comic-archive style:
5// ordered page IMAGES the browser renders + pinch/pan/zooms natively. KEY INSIGHT: the browser is the
6// last-mile image renderer/zoomer (allowed). Nishi only EXTRACTS page image BYTES sovereignly, serves them.
7//
8// CONTAINERS (routed by MAGIC BYTES, never extension -- wild comics LIE: zip renamed .cbr, rar renamed .cbz):
9// (A) ZIP (cbz) -- central-directory walker (proven, from nx_epub_read.nx), store+deflate.
10// (B) RAR4 (cbr) -- sovereign block walker, STORED (method 0x30) entries extracted. Comics keep
11// JPEG pages STORED (compressing JPEG gains ~0%), so store-only covers the real corpus; entries that
12// ARE compressed are COUNTED and reported CBX-RAR-COMPRESSED-UNSUPPORTED -- honest, never fake pages.
13// (C) RAR5 (cbr) -- vint block walker, method bits 7..9 == 0 (store) extracted; same honesty.
14// (D) 7z (cb7) -- CBX-7Z-UNSUPPORTED honest skip (LZMA decode not built yet).
15// (E) Scanned/image PDF -- /DCTDecode image XObjects = raw JPEG between stream/endstream. Anchor on
16// /DCTDecode (NEVER bare "stream"), confirm SOI ff d8, scan to endstream. Vector/text PDF -> honest skip.
17//
18// FORMAT-FIX rev (the broken-comics debt this rev eats):
19// 1. NATURAL page sort -- ascii sort put "page10" before "page2" in any non-zero-padded comic = pages
20// OUT OF ORDER. name_cmp_nat compares digit runs numerically (case-insensitive elsewhere).
21// 2. PER-PAGE magic gate -- __MACOSX/._foo.jpg AppleDouble junk passed the extension filter and was
22// WRITTEN as a broken page. Now every page's BYTES must pass img_sig_ok or the entry is skipped+logged.
23// 3. CBR support (B)(C) + magic sniffing (misnamed containers) + honest skip codes.
24// 4. "cfix":2 schema tag in cbx.json -- the server treats a cbx.json WITHOUT the current tag as a cache
25// MISS, so comics extracted by the OLD organ self-heal on next open (no remote rm; additive).
26//
27// SOVEREIGN: pure Nishi organs + raw syscalls. NO python/unzip/unrar/pdfium/mupdf/ghostscript/poppler/gcc/sh.
28// Image DISPLAY+zoom is the browser (last-mile); the viewer JS/CSS is hand-written (no 3rd-party libs).
29// NO-FAKE-GREEN: every written page's first bytes are a REAL image magic (JPEG/PNG/GIF/BMP/WEBP); skips are
30// COUNTED and printed, never silent. ADDITIVE (rule 13): output dir per slug; reruns overwrite atomically.
31// license_tier: ORIGINAL
32import "nx_syscalls.nx"
33import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
34import "nx_deflate.nx"
35const PAGE_MAGIC_1024: i64 = 1024
36const PAGE_MAGIC_1048576: i64 = 1048576
37
38// ---- io + string helpers (reused verbatim from nx_epub_read.nx) ----
39func er_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
40func er_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, er_slen(s)); return 0 }
41// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
42// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
43// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
44// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
45func er_n(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
46func er_p(s: *u8) -> i64 { er_w(1, s); return 0 }
47func er_pn(v: i64) -> i64 { er_n(1, v); return 0 }
48
49func le16(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o+1] as i64) << 8) }
50func le32(b: *u8, o: i64) -> i64 { return (b[o] as i64) | ((b[o+1] as i64) << 8) | ((b[o+2] as i64) << 16) | ((b[o+3] as i64) << 24) }
51
52func er_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[off+i]=s[i];i=i+1} return off+i }
53func er_catn(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; if m==0{t[0]=48 as u8;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{dst[off+i]=t[k-1-i];i=i+1} return off+k }
54
55// substring search; return index in hay[0..hl] or -1 (verbatim from nx_epub_read.nx)
56func find_sub(hay: *u8, hl: i64, needle: *u8) -> i64 {
57 let nl: i64 = er_slen(needle)
58 if nl == 0 { return 0-1 }
59 var i: i64 = 0
60 while i + nl <= hl {
61 var k: i64 = 0
62 var hit: i64 = 1
63 while k < nl { if hay[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
64 if hit == 1 { return i }
65 i = i + 1
66 }
67 return 0-1
68}
69
70// substring search starting at >= from; return index or -1
71func find_sub_from(hay: *u8, hl: i64, needle: *u8, from: i64) -> i64 {
72 if from < 0 { return 0-1 }
73 let r: i64 = find_sub((hay as i64 + from) as *u8, hl - from, needle)
74 if r < 0 { return 0-1 }
75 return from + r
76}
77
78// ---- ZIP central-directory walker (find_eocd / zip_enum reused from prior rev) ----
79func find_eocd(z: *u8, zl: i64) -> i64 {
80 var i: i64 = zl - 22
81 if i < 0 { return 0-1 }
82 while i >= 0 {
83 if z[i] == (0x50 as u8) { if z[i+1] == (0x4b as u8) { if z[i+2] == (0x05 as u8) { if z[i+3] == (0x06 as u8) { return i } } } }
84 i = i - 1
85 }
86 return 0-1
87}
88
89// enumerate ALL zip entries: filenames into a NUL-separated arena (starts in noffs[]), parallel per-entry
90// {method,comp,dataoff} arrays (dataoff resolved from each entry's own LOCAL header). Returns entry count.
91func zip_enum(z: *u8, zl: i64, arena: *u8, acap: i64, noffs: *i64, meths: *i64, comps: *i64, doffs: *i64, ecap: i64) -> i64 {
92 let eocd: i64 = find_eocd(z, zl)
93 if eocd < 0 { return 0 }
94 let count: i64 = le16(z, eocd + 10)
95 var cd: i64 = le32(z, eocd + 16)
96 var k: i64 = 0
97 var n: i64 = 0
98 var aoff: i64 = 0
99 while k < count {
100 if n >= ecap { return n }
101 if cd + 46 > zl { return n }
102 if z[cd] != (0x50 as u8) { return n }
103 let method: i64 = le16(z, cd + 10)
104 let comp: i64 = le32(z, cd + 20)
105 let fnlen: i64 = le16(z, cd + 28)
106 let extralen: i64 = le16(z, cd + 30)
107 let commentlen: i64 = le16(z, cd + 32)
108 let lho: i64 = le32(z, cd + 42)
109 if aoff + fnlen + 1 < acap {
110 noffs[n] = aoff
111 var i: i64 = 0
112 while i < fnlen { arena[aoff] = z[cd + 46 + i]; aoff = aoff + 1; i = i + 1 }
113 arena[aoff] = 0 as u8; aoff = aoff + 1
114 let lfn: i64 = le16(z, lho + 26)
115 let lex: i64 = le16(z, lho + 28)
116 meths[n] = method
117 comps[n] = comp
118 doffs[n] = lho + 30 + lfn + lex
119 n = n + 1
120 }
121 cd = cd + 46 + fnlen + extralen + commentlen
122 k = k + 1
123 }
124 return n
125}
126
127// ASCII byte-compare two NUL-terminated names: <0, 0, >0 (kept: extension matching in is_image_name)
128func name_cmp(a: *u8, b: *u8) -> i64 {
129 var i: i64 = 0
130 while a[i] != (0 as u8) {
131 if b[i] == (0 as u8) { return 1 }
132 let ca: i64 = a[i] as i64
133 let cb: i64 = b[i] as i64
134 if ca < cb { return 0-1 }
135 if ca > cb { return 1 }
136 i = i + 1
137 }
138 if b[i] != (0 as u8) { return 0-1 }
139 return 0
140}
141
142// lowercase one ascii byte
143func lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
144
145// count of consecutive ascii digits in s starting at o
146func nat_digits(s: *u8, o: i64) -> i64 {
147 var k: i64 = 0
148 var go: i64 = 1
149 while go == 1 {
150 let c: i64 = s[o+k] as i64
151 var d: i64 = 0
152 if c >= 48 { if c <= 57 { d = 1 } }
153 if d == 1 { k = k + 1 } else { go = 0 }
154 }
155 return k
156}
157
158// NATURAL name compare (numeric-aware, case-insensitive): "page2" < "page10". The FORMAT-FIX for wild
159// comics whose page names are NOT zero-padded ("Page 2.jpg" vs "Page 10.jpg") -- ascii sort broke order.
160func name_cmp_nat(a: *u8, b: *u8) -> i64 {
161 var i: i64 = 0
162 var j: i64 = 0
163 var res: i64 = 2
164 while res == 2 {
165 let ca: i64 = a[i] as i64
166 let cb: i64 = b[j] as i64
167 if ca == 0 {
168 if cb == 0 { res = 0 } else { res = 0-1 }
169 } else {
170 if cb == 0 { res = 1 } else {
171 var da: i64 = 0
172 if ca >= 48 { if ca <= 57 { da = 1 } }
173 var db: i64 = 0
174 if cb >= 48 { if cb <= 57 { db = 1 } }
175 var both: i64 = 0
176 if da == 1 { if db == 1 { both = 1 } }
177 if both == 1 {
178 var zi: i64 = i
179 var zj: i64 = j
180 while a[zi] == (48 as u8) { zi = zi + 1 }
181 while b[zj] == (48 as u8) { zj = zj + 1 }
182 let li: i64 = nat_digits(a, zi)
183 let lj: i64 = nat_digits(b, zj)
184 if li < lj { res = 0-1 } else {
185 if li > lj { res = 1 } else {
186 var k: i64 = 0
187 while k < li {
188 if res == 2 {
189 let xa: i64 = a[zi+k] as i64
190 let xb: i64 = b[zj+k] as i64
191 if xa < xb { res = 0-1 }
192 if xa > xb { res = 1 }
193 }
194 k = k + 1
195 }
196 if res == 2 { i = zi + li; j = zj + lj }
197 }
198 }
199 } else {
200 let la: i64 = lc(ca)
201 let lb2: i64 = lc(cb)
202 if la < lb2 { res = 0-1 }
203 if la > lb2 { res = 1 }
204 if res == 2 { i = i + 1; j = j + 1 }
205 }
206 }
207 }
208 }
209 return res
210}
211
212// 1 if name ends with one of the image extensions .jpg/.jpeg/.png/.webp/.gif/.bmp (case-insensitive)
213func is_image_name(name: *u8) -> i64 {
214 let n: i64 = er_slen(name)
215 if n < 4 { return 0 }
216 var dot: i64 = 0-1
217 var i: i64 = 0
218 while i < n { if name[i] == (0x2e as u8) { dot = i } i = i + 1 }
219 if dot < 0 { return 0 }
220 let extlen: i64 = n - dot - 1
221 let e: *u8 = sys_mmap(16)
222 var k: i64 = 0
223 while k < extlen { if k < 12 { e[k] = lc(name[dot+1+k] as i64) as u8 } k = k + 1 }
224 e[extlen] = 0 as u8
225 if name_cmp(e, "jpg" as *u8) == 0 { return 1 }
226 if name_cmp(e, "jpeg" as *u8) == 0 { return 1 }
227 if name_cmp(e, "png" as *u8) == 0 { return 1 }
228 if name_cmp(e, "webp" as *u8) == 0 { return 1 }
229 if name_cmp(e, "gif" as *u8) == 0 { return 1 }
230 if name_cmp(e, "bmp" as *u8) == 0 { return 1 }
231 return 0
232}
233
234// 1 if the entry is archive JUNK, not a page: __MACOSX resource-fork dirs + AppleDouble "._" basenames.
235// These carry image EXTENSIONS (._0001.jpg) but their bytes are AppleDouble metadata -- they rendered as
236// BROKEN pages in the viewer before this gate. Bytes are ALSO magic-gated per page (belt + suspenders).
237func is_junk_name(name: *u8) -> i64 {
238 let n: i64 = er_slen(name)
239 if find_sub(name, n, "__MACOSX" as *u8) >= 0 { return 1 }
240 var slash: i64 = 0-1
241 var i: i64 = 0
242 while i < n { if name[i] == (0x2f as u8) { slash = i } i = i + 1 }
243 let b0: i64 = slash + 1
244 if b0 + 1 < n { if name[b0] == (0x2e as u8) { if name[b0+1] == (0x5f as u8) { return 1 } } }
245 return 0
246}
247
248// extension string (with leading dot) for a name, lowercased, into out; default ".jpg"
249func ext_of(name: *u8, out: *u8) -> i64 {
250 let n: i64 = er_slen(name)
251 var dot: i64 = 0-1
252 var i: i64 = 0
253 while i < n { if name[i] == (0x2e as u8) { dot = i } i = i + 1 }
254 if dot < 0 { return er_cat(out, 0, ".jpg" as *u8) }
255 var o: i64 = 0
256 out[o] = 0x2e as u8; o = o + 1
257 var k: i64 = dot + 1
258 while k < n { out[o] = lc(name[k] as i64) as u8; o = o + 1; k = k + 1 }
259 out[o] = 0 as u8
260 return o
261}
262
263// mkdirat AT_FDCWD path 0755 (idempotent; -EEXIST harmless). x86_64 mkdirat=258, AT_FDCWD=-100.
264func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 }
265
266const PAGE_CAP: i64 = 67108864 // 64 MiB per page buffer (largest cbz page << this)
267
268// build knowledge/staging/media/reader/<slug>/page<NNN><ext>, write n bytes; ret 0/-1
269func write_page(dir: *u8, idx: i64, ext: *u8, bytes: *u8, n: i64) -> i64 {
270 let path: *u8 = sys_mmap(PAGE_MAGIC_1024)
271 var o: i64 = er_cat(path, 0, dir as *u8)
272 o = er_cat(path, o, "/page" as *u8)
273 if idx < 100 { path[o] = 48 as u8; o = o + 1 }
274 if idx < 10 { path[o] = 48 as u8; o = o + 1 }
275 o = er_catn(path, o, idx)
276 o = er_cat(path, o, ext as *u8); path[o] = 0 as u8
277 let fd: i64 = sys_openat_wr(path, 0x1a4)
278 if fd < 0 { return 0-1 }
279 sys_write(fd, bytes, n)
280 sys_close(fd)
281 return 0
282}
283
284// write the page filename (page<NNN><ext>) into out (NUL-term); ret length
285func page_fname(out: *u8, idx: i64, ext: *u8) -> i64 {
286 var o: i64 = er_cat(out, 0, "page" as *u8)
287 if idx < 100 { out[o] = 48 as u8; o = o + 1 }
288 if idx < 10 { out[o] = 48 as u8; o = o + 1 }
289 o = er_catn(out, o, idx)
290 o = er_cat(out, o, ext as *u8); out[o] = 0 as u8
291 return o
292}
293
294// no-fake-green image signature check: 1 if the buffer begins with a REAL image magic among the formats
295// browsers render (JPEG ff d8 / PNG 89504e47 / GIF "GIF8" / BMP "BM" / WEBP "RIFF"...."WEBP").
296func img_sig_ok(b: *u8, n: i64) -> i64 {
297 if n >= 2 { if (b[0] as i64) == 0xff { if (b[1] as i64) == 0xd8 { return 1 } } } // JPEG
298 if n >= 4 { if (b[0] as i64) == 0x89 { if (b[1] as i64) == 0x50 { if (b[2] as i64) == 0x4e { if (b[3] as i64) == 0x47 { return 1 } } } } } // PNG
299 if n >= 4 { if (b[0] as i64) == 0x47 { if (b[1] as i64) == 0x49 { if (b[2] as i64) == 0x46 { if (b[3] as i64) == 0x38 { return 1 } } } } } // GIF8
300 if n >= 2 { if (b[0] as i64) == 0x42 { if (b[1] as i64) == 0x4d { return 1 } } } // BMP "BM"
301 if n >= 12 { if (b[0] as i64) == 0x52 { if (b[1] as i64) == 0x49 { if (b[2] as i64) == 0x46 { if (b[3] as i64) == 0x46 { if (b[8] as i64) == 0x57 { if (b[9] as i64) == 0x45 { if (b[10] as i64) == 0x42 { if (b[11] as i64) == 0x50 { return 1 } } } } } } } } } // WEBP
302 return 0
303}
304
305const ECAP: i64 = 4096 // max archive entries enumerated
306
307// ================= SHARED page emitter (zip + rar feed the same arrays) =================
308// meths[i]: 0 = raw copy at doffs[i] (zip stored / rar stored), 8 = zip deflate stream at doffs[i].
309// Filters junk names, NATURAL-sorts, extracts, PER-PAGE magic-gates, writes pages + cbx.json (with
310// "cfix":2 -- the server-side cache-heal tag; older cbx.json without it re-extracts on next open).
311// cover_only=1: extract ONLY the natural-first image to <dir>/cover.jpg (grid thumbnails must not pay
312// for a full 100MB+ unpack per cover request) -- no cbx.json, no page files.
313func emit_pages(z: *u8, slug: *u8, fmt: *u8, arena: *u8, noffs: *i64, meths: *i64, comps: *i64, doffs: *i64, nent: i64, cover_only: i64) -> i64 {
314 let order: *i64 = sys_mmap(8 * ECAP) as *i64
315 var nimg: i64 = 0
316 var i: i64 = 0
317 while i < nent {
318 let nm: *u8 = (arena as i64 + noffs[i]) as *u8
319 if is_junk_name(nm) == 0 { if is_image_name(nm) == 1 { order[nimg] = i; nimg = nimg + 1 } }
320 i = i + 1
321 }
322 if nimg <= 0 { er_p("CBX-FAIL no-image-entries slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
323
324 // insertion sort order[] by NATURAL name order = true page order
325 var a: i64 = 1
326 while a < nimg {
327 let cur: i64 = order[a]
328 let curnm: *u8 = (arena as i64 + noffs[cur]) as *u8
329 var b: i64 = a - 1
330 var stop: i64 = 0
331 while stop == 0 {
332 if b < 0 { stop = 1 }
333 else {
334 let bnm: *u8 = (arena as i64 + noffs[order[b]]) as *u8
335 if name_cmp_nat(bnm, curnm) > 0 { order[b+1] = order[b]; b = b - 1 }
336 else { stop = 1 }
337 }
338 }
339 order[b+1] = cur
340 a = a + 1
341 }
342
343 // per-slug output dir
344 let dir: *u8 = sys_mmap(512)
345 var dlen: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8)
346 dlen = er_cat(dir, dlen, slug); dir[dlen] = 0 as u8
347 ensure_dir(dir)
348
349 // COVER-ONLY: write the natural-first REAL image as <dir>/cover.jpg and stop -- O(one page), not O(book)
350 if cover_only == 1 {
351 let cbuf: *u8 = sys_mmap(PAGE_CAP)
352 var cw: i64 = 0
353 var cp: i64 = 0
354 while cp < nimg {
355 if cw == 0 {
356 let cei: i64 = order[cp]
357 let cme: i64 = meths[cei]
358 let cco: i64 = comps[cei]
359 let cdo: i64 = doffs[cei]
360 var cn: i64 = 0-1
361 if cco <= PAGE_CAP {
362 if cme == 0 { var cc: i64 = 0; while cc < cco { cbuf[cc] = z[cdo + cc]; cc = cc + 1 } cn = cco }
363 if cme == 8 {
364 let cr: *NxDeflateResult = nx_deflate_inflate((z as i64 + cdo) as *u8, cco, PAGE_CAP)
365 if (cr as i64) != 0 { let crn: i64 = cr.output_size; var c3: i64 = 0; while c3 < crn { if c3 < PAGE_CAP { cbuf[c3] = cr.output_data[c3] } c3 = c3 + 1 } cn = crn }
366 }
367 }
368 if cn > 0 { if img_sig_ok(cbuf, cn) == 1 {
369 let cvp: *u8 = sys_mmap(600)
370 var cvo: i64 = er_cat(cvp, 0, dir as *u8); cvo = er_cat(cvp, cvo, "/cover.jpg" as *u8); cvp[cvo] = 0 as u8
371 let cvfd: i64 = sys_openat_wr(cvp, 0x1a4)
372 if cvfd >= 0 { sys_write(cvfd, cbuf, cn); sys_close(cvfd); cw = 1 }
373 } }
374 }
375 cp = cp + 1
376 }
377 er_p("CBX-COVER slug=" as *u8); er_w(1, slug); er_p(" ok=" as *u8); er_pn(cw); er_p("\n" as *u8)
378 if cw == 1 { return 0 }
379 return 0-1
380 }
381
382 // cbx.json (pages array first; npages LAST = the post-gate truth, may be < nimg)
383 let jp: *u8 = sys_mmap(512)
384 var jo: i64 = er_cat(jp, 0, dir as *u8); jo = er_cat(jp, jo, "/cbx.json" as *u8); jp[jo] = 0 as u8
385 let jfd: i64 = sys_openat_wr(jp, 0x1a4)
386 if jfd < 0 { er_p("CBX-FAIL open-json slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
387 er_w(jfd, "{\"title\":\"" as *u8); er_w(jfd, slug)
388 er_w(jfd, "\",\"format\":\"" as *u8); er_w(jfd, fmt)
389 er_w(jfd, "\",\"cfix\":2,\"pages\":[" as *u8)
390
391 let pbuf: *u8 = sys_mmap(PAGE_CAP)
392 let ext: *u8 = sys_mmap(16)
393 let fname: *u8 = sys_mmap(64)
394 var total_bytes: i64 = 0
395 var written: i64 = 0
396 var skip_magic: i64 = 0
397 var skip_extract: i64 = 0
398 var p: i64 = 0
399 while p < nimg {
400 let ei: i64 = order[p]
401 let nm: *u8 = (arena as i64 + noffs[ei]) as *u8
402 let method: i64 = meths[ei]
403 let comp: i64 = comps[ei]
404 let dataoff: i64 = doffs[ei]
405 var pn: i64 = 0-1
406 if comp > PAGE_CAP { pn = 0-1 } else {
407 if method == 0 {
408 var c: i64 = 0
409 while c < comp { pbuf[c] = z[dataoff + c]; c = c + 1 }
410 pn = comp
411 }
412 if method == 8 {
413 let r: *NxDeflateResult = nx_deflate_inflate((z as i64 + dataoff) as *u8, comp, PAGE_CAP)
414 if (r as i64) != 0 {
415 let rn: i64 = r.output_size
416 var c2: i64 = 0
417 while c2 < rn { if c2 < PAGE_CAP { pbuf[c2] = r.output_data[c2] } c2 = c2 + 1 }
418 pn = rn
419 }
420 }
421 }
422 if pn > 0 {
423 // PER-PAGE no-fake-green gate: bytes must BE an image, or the entry is junk (AppleDouble,
424 // stray text) -- skipped + counted, never written as a broken page.
425 if img_sig_ok(pbuf, pn) == 1 {
426 ext_of(nm, ext)
427 write_page(dir, written, ext, pbuf, pn)
428 page_fname(fname, written, ext)
429 if written > 0 { er_w(jfd, "," as *u8) }
430 er_w(jfd, "{\"idx\":" as *u8); er_n(jfd, written)
431 er_w(jfd, ",\"file\":\"" as *u8); er_w(jfd, fname)
432 er_w(jfd, "\",\"bytes\":" as *u8); er_n(jfd, pn)
433 er_w(jfd, "}" as *u8)
434 total_bytes = total_bytes + pn
435 written = written + 1
436 } else {
437 skip_magic = skip_magic + 1
438 er_p("CBX-SKIP nonimage-bytes entry=" as *u8); er_w(1, nm); er_p("\n" as *u8)
439 }
440 } else {
441 skip_extract = skip_extract + 1
442 er_p("CBX-SKIP extract-failed entry=" as *u8); er_w(1, nm); er_p("\n" as *u8)
443 }
444 p = p + 1
445 }
446 er_w(jfd, "],\"npages\":" as *u8); er_n(jfd, written)
447 er_w(jfd, "}" as *u8)
448 sys_close(jfd)
449
450 er_p("CBX slug=" as *u8); er_w(1, slug)
451 er_p(" source=" as *u8); er_w(1, fmt)
452 er_p(" npages=" as *u8); er_pn(written)
453 er_p(" skipped_nonimage=" as *u8); er_pn(skip_magic)
454 er_p(" skipped_extract=" as *u8); er_pn(skip_extract)
455 er_p(" total_bytes=" as *u8); er_pn(total_bytes)
456 er_p("\n" as *u8)
457
458 if written <= 0 { return 0-1 }
459 return 0
460}
461
462// ================= RAR4 (Rar! 1a 07 00) STORE walker =================
463// Block: u16 crc, u8 type, u16 flags, u16 hsize. File header (type 0x74): packsize@7 unpsize@11
464// method@25 (0x30=stored) namesize@26 name@32 (@40 + high sizes if flags&0x100). Data at block+hsize.
465// Dirs (flags&0xe0==0xe0), encrypted (flags&4) and compressed (method!=0x30) entries are skip-counted.
466func rar4_enum(z: *u8, zl: i64, arena: *u8, acap: i64, noffs: *i64, meths: *i64, comps: *i64, doffs: *i64, ecap: i64, skipc: *i64) -> i64 {
467 var pos: i64 = 7
468 var n: i64 = 0
469 var aoff: i64 = 0
470 var go: i64 = 1
471 while go == 1 {
472 if pos + 7 > zl { go = 0 } else {
473 let btype: i64 = z[pos+2] as i64
474 let bflags: i64 = le16(z, pos+3)
475 let hsize: i64 = le16(z, pos+5)
476 if hsize < 7 { go = 0 } else {
477 var datasz: i64 = 0
478 if btype == 0x74 {
479 if pos + 32 > zl { go = 0 } else {
480 datasz = le32(z, pos+7)
481 let method: i64 = z[pos+25] as i64
482 let nmsz: i64 = le16(z, pos+26)
483 var nmoff: i64 = pos + 32
484 if (bflags & 0x100) != 0 { nmoff = pos + 40; datasz = datasz | (le32(z, pos+32) << 32) }
485 var isdir: i64 = 0
486 if (bflags & 0xe0) == 0xe0 { isdir = 1 }
487 var admit: i64 = 1
488 if isdir == 1 { admit = 0 }
489 if admit == 1 { if (bflags & 4) != 0 { admit = 0; skipc[0] = skipc[0] + 1 } }
490 if admit == 1 { if method != 0x30 { admit = 0; skipc[0] = skipc[0] + 1 } }
491 if n >= ecap { admit = 0 }
492 if nmoff + nmsz > zl { admit = 0 }
493 if datasz <= 0 { admit = 0 }
494 if admit == 1 { if aoff + nmsz + 1 < acap {
495 noffs[n] = aoff
496 var i: i64 = 0
497 var stopn: i64 = 0
498 while i < nmsz {
499 if stopn == 0 {
500 let c: u8 = z[nmoff+i]
501 if c == (0 as u8) { stopn = 1 } else { arena[aoff] = c; aoff = aoff + 1 }
502 }
503 i = i + 1
504 }
505 arena[aoff] = 0 as u8; aoff = aoff + 1
506 meths[n] = 0
507 comps[n] = datasz
508 doffs[n] = pos + hsize
509 n = n + 1
510 } }
511 }
512 } else {
513 if btype == 0x7b { go = 0 }
514 if (bflags & 0x8000) != 0 { if pos + 11 <= zl { datasz = le32(z, pos+7) } }
515 }
516 if go == 1 {
517 let nxt: i64 = pos + hsize + datasz
518 if nxt <= pos { go = 0 } else { pos = nxt }
519 }
520 }
521 }
522 }
523 return n
524}
525
526// RAR5 vint: 7-bit little-endian groups, bit7 = continuation. Advances pos[0]; bounded.
527func r5v(z: *u8, pos: *i64, zl: i64) -> i64 {
528 var v: i64 = 0
529 var sh: i64 = 0
530 var go: i64 = 1
531 while go == 1 {
532 if pos[0] >= zl { go = 0 } else {
533 if sh > 63 { go = 0 } else {
534 let c: i64 = z[pos[0]] as i64
535 pos[0] = pos[0] + 1
536 v = v | ((c & 0x7f) << sh)
537 sh = sh + 7
538 if (c & 0x80) == 0 { go = 0 }
539 }
540 }
541 }
542 return v
543}
544
545// ================= RAR5 (Rar! 1a 07 01 00) STORE walker =================
546// Block: u32 crc, vint hsize, then header: vint type(2=file), vint flags(1=extra,2=data),
547// [vint extra_size], [vint data_size]; file: vint ff(1=dir,2=mtime,4=crc), vint unp, vint attr,
548// [u32 mtime], [u32 crc], vint cinfo (bits 7..9 = method, 0=store), vint host_os, vint namelen, name.
549// Data area follows the header (hstart+hsize); next block = data end.
550func rar5_enum(z: *u8, zl: i64, arena: *u8, acap: i64, noffs: *i64, meths: *i64, comps: *i64, doffs: *i64, ecap: i64, skipc: *i64) -> i64 {
551 let pp: *i64 = sys_mmap(16) as *i64
552 var pos: i64 = 8
553 var n: i64 = 0
554 var aoff: i64 = 0
555 var go: i64 = 1
556 while go == 1 {
557 if pos + 8 > zl { go = 0 } else {
558 pp[0] = pos + 4
559 let hsz: i64 = r5v(z, pp, zl)
560 let hstart: i64 = pp[0]
561 if hsz <= 0 { go = 0 } else {
562 if hstart + hsz > zl { go = 0 } else {
563 let htype: i64 = r5v(z, pp, zl)
564 let hflags: i64 = r5v(z, pp, zl)
565 var xsz: i64 = 0
566 if (hflags & 1) != 0 { xsz = r5v(z, pp, zl) }
567 var dsz: i64 = 0
568 if (hflags & 2) != 0 { dsz = r5v(z, pp, zl) }
569 if htype == 5 { go = 0 } else {
570 if htype == 2 {
571 let ff: i64 = r5v(z, pp, zl)
572 let unp: i64 = r5v(z, pp, zl)
573 let attr: i64 = r5v(z, pp, zl)
574 if (ff & 2) != 0 { pp[0] = pp[0] + 4 }
575 if (ff & 4) != 0 { pp[0] = pp[0] + 4 }
576 let cinfo: i64 = r5v(z, pp, zl)
577 let hos: i64 = r5v(z, pp, zl)
578 let nml: i64 = r5v(z, pp, zl)
579 let nmoff: i64 = pp[0]
580 let method: i64 = (cinfo >> 7) & 7
581 var admit: i64 = 1
582 if (ff & 1) != 0 { admit = 0 }
583 if admit == 1 { if method != 0 { admit = 0; skipc[0] = skipc[0] + 1 } }
584 if n >= ecap { admit = 0 }
585 if nmoff + nml > zl { admit = 0 }
586 if dsz <= 0 { admit = 0 }
587 if admit == 1 { if aoff + nml + 1 < acap {
588 noffs[n] = aoff
589 var i: i64 = 0
590 while i < nml { arena[aoff] = z[nmoff+i]; aoff = aoff + 1; i = i + 1 }
591 arena[aoff] = 0 as u8; aoff = aoff + 1
592 meths[n] = 0
593 comps[n] = dsz
594 doffs[n] = hstart + hsz
595 n = n + 1
596 } }
597 }
598 let nxt: i64 = hstart + hsz + dsz
599 if nxt <= pos { go = 0 } else { pos = nxt }
600 }
601 }
602 }
603 }
604 }
605 return n
606}
607
608// ========================= PDF (DCTDecode) ===================
609func do_pdf(path: *u8, slug: *u8) -> i64 {
610 let lb: *i64 = sys_mmap(16) as *i64
611 let d: *u8 = sys_read_file(path, lb)
612 if (d as i64) == 0 { er_p("CBX-FAIL pdf read slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
613 let dl: i64 = lb[0]
614
615 // count /DCTDecode -> if zero, HONEST unsupported (vector/text or FlateDecode-image)
616 if find_sub(d, dl, "/DCTDecode" as *u8) < 0 {
617 er_p("CBX-PDF-UNSUPPORTED slug=" as *u8); er_w(1, slug)
618 er_p(" reason=no-DCTDecode-image-xobjects(vector/text-or-flate-image-pdf)\n" as *u8)
619 return 0-2 // distinct from FAIL: honest skip
620 }
621
622 let dir: *u8 = sys_mmap(512)
623 var dlen: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8)
624 dlen = er_cat(dir, dlen, slug); dir[dlen] = 0 as u8
625 ensure_dir(dir)
626
627 let jp: *u8 = sys_mmap(512)
628 var jo: i64 = er_cat(jp, 0, dir as *u8); jo = er_cat(jp, jo, "/cbx.json" as *u8); jp[jo] = 0 as u8
629 let jfd: i64 = sys_openat_wr(jp, 0x1a4)
630 if jfd < 0 { er_p("CBX-FAIL pdf open-json slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
631 er_w(jfd, "{\"title\":\"" as *u8); er_w(jfd, slug)
632 er_w(jfd, "\",\"format\":\"pdf-cbx\",\"cfix\":2,\"pages\":[" as *u8)
633
634 let fname: *u8 = sys_mmap(64)
635 var total_bytes: i64 = 0
636 var any_ok: i64 = 0
637 var written: i64 = 0
638 var pos: i64 = 0
639 var go: i64 = 1
640 while go == 1 {
641 let dct: i64 = find_sub_from(d, dl, "/DCTDecode" as *u8, pos)
642 if dct < 0 { go = 0 }
643 else {
644 let st: i64 = find_sub_from(d, dl, "stream" as *u8, dct)
645 if st < 0 { go = 0 }
646 else {
647 var b: i64 = st + 6
648 if b < dl { if (d[b] as i64) == 0x0d { b = b + 1 } }
649 if b < dl { if (d[b] as i64) == 0x0a { b = b + 1 } }
650 var is_jpeg: i64 = 0
651 if b + 1 < dl { if (d[b] as i64) == 0xff { if (d[b+1] as i64) == 0xd8 { is_jpeg = 1 } } }
652 let es: i64 = find_sub_from(d, dl, "endstream" as *u8, b)
653 if is_jpeg == 1 { if es > b {
654 var e: i64 = es
655 if e > b { if (d[e-1] as i64) == 0x0a { e = e - 1 } }
656 if e > b { if (d[e-1] as i64) == 0x0d { e = e - 1 } }
657 let n: i64 = e - b
658 if n > 0 {
659 write_page(dir, written, ".jpg" as *u8, (d as i64 + b) as *u8, n)
660 page_fname(fname, written, ".jpg" as *u8)
661 if written > 0 { er_w(jfd, "," as *u8) }
662 er_w(jfd, "{\"idx\":" as *u8); er_n(jfd, written)
663 er_w(jfd, ",\"file\":\"" as *u8); er_w(jfd, fname)
664 er_w(jfd, "\",\"bytes\":" as *u8); er_n(jfd, n)
665 er_w(jfd, "}" as *u8)
666 total_bytes = total_bytes + n
667 if written == 0 { if (d[b] as i64) == 0xff { if (d[b+1] as i64) == 0xd8 { any_ok = 1 } } }
668 written = written + 1
669 }
670 } }
671 if es > 0 { pos = es + 9 } else { pos = b + 1 }
672 }
673 }
674 }
675 er_w(jfd, "],\"npages\":" as *u8); er_n(jfd, written)
676 er_w(jfd, "}" as *u8)
677 sys_close(jfd)
678
679 er_p("CBX slug=" as *u8); er_w(1, slug)
680 er_p(" source=pdf npages=" as *u8); er_pn(written)
681 er_p(" total_bytes=" as *u8); er_pn(total_bytes)
682 er_p(" first_magic_ok=" as *u8); er_pn(any_ok)
683 er_p("\n" as *u8)
684
685 if written <= 0 { return 0-1 }
686 if any_ok != 1 { return 0-1 }
687 return 0
688}
689
690// ========================= comic dispatcher (kept name do_cbz; now sniffs) ===============
691// Wild "cbz"/"cbr" files LIE about their container (zip renamed .cbr, rar renamed .cbz, pdf renamed
692// either) -- so route by MAGIC BYTES, never extension: PK->zip, Rar!->rar4/rar5 store, 7z->honest
693// unsupported, %PDF->the DCTDecode path. The extension only decided "is a comic candidate" upstream.
694func do_cbz(path: *u8, slug: *u8, cover_only: i64) -> i64 {
695 let lb: *i64 = sys_mmap(16) as *i64
696 let z: *u8 = sys_read_file(path, lb)
697 if (z as i64) == 0 { er_p("CBX-FAIL comic read slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
698 let zl: i64 = lb[0]
699 if zl < 8 { er_p("CBX-FAIL comic tiny slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
700
701 // %PDF -- a "comic" that is really a pdf
702 if z[0] == (0x25 as u8) { if z[1] == (0x50 as u8) { if z[2] == (0x44 as u8) { if z[3] == (0x46 as u8) {
703 return do_pdf(path, slug)
704 } } } }
705
706 // PK -- zip container (true cbz, or a renamed one)
707 if z[0] == (0x50 as u8) { if z[1] == (0x4b as u8) {
708 let arena: *u8 = sys_mmap(PAGE_MAGIC_1048576)
709 let noffs: *i64 = sys_mmap(8 * ECAP) as *i64
710 let meths: *i64 = sys_mmap(8 * ECAP) as *i64
711 let comps: *i64 = sys_mmap(8 * ECAP) as *i64
712 let doffs: *i64 = sys_mmap(8 * ECAP) as *i64
713 let nent: i64 = zip_enum(z, zl, arena, PAGE_MAGIC_1048576, noffs, meths, comps, doffs, ECAP)
714 if nent <= 0 { er_p("CBX-FAIL zip no-entries slug=" as *u8); er_w(1, slug); er_p("\n" as *u8); return 0-1 }
715 return emit_pages(z, slug, "cbz" as *u8, arena, noffs, meths, comps, doffs, nent, cover_only)
716 } }
717
718 // Rar! 1a 07 -- rar4 (00) or rar5 (01 00)
719 if z[0] == (0x52 as u8) { if z[1] == (0x61 as u8) { if z[2] == (0x72 as u8) { if z[3] == (0x21 as u8) {
720 let arena: *u8 = sys_mmap(PAGE_MAGIC_1048576)
721 let noffs: *i64 = sys_mmap(8 * ECAP) as *i64
722 let meths: *i64 = sys_mmap(8 * ECAP) as *i64
723 let comps: *i64 = sys_mmap(8 * ECAP) as *i64
724 let doffs: *i64 = sys_mmap(8 * ECAP) as *i64
725 let skipc: *i64 = sys_mmap(16) as *i64
726 skipc[0] = 0
727 var nent: i64 = 0
728 var fmt: *u8 = "cbr-rar4" as *u8
729 if (z[6] as i64) == 1 { fmt = "cbr-rar5" as *u8; nent = rar5_enum(z, zl, arena, PAGE_MAGIC_1048576, noffs, meths, comps, doffs, ECAP, skipc) }
730 else { nent = rar4_enum(z, zl, arena, PAGE_MAGIC_1048576, noffs, meths, comps, doffs, ECAP, skipc) }
731 if nent <= 0 {
732 if skipc[0] > 0 {
733 er_p("CBX-RAR-COMPRESSED-UNSUPPORTED slug=" as *u8); er_w(1, slug)
734 er_p(" compressed_entries=" as *u8); er_pn(skipc[0])
735 er_p(" (rar decompressor not built; STORED-jpeg rars extract fine)\n" as *u8)
736 return 0-2
737 }
738 er_p("CBX-FAIL rar no-stored-entries slug=" as *u8); er_w(1, slug); er_p("\n" as *u8)
739 return 0-1
740 }
741 if skipc[0] > 0 { er_p("CBX-NOTE rar compressed entries skipped=" as *u8); er_pn(skipc[0]); er_p("\n" as *u8) }
742 return emit_pages(z, slug, fmt, arena, noffs, meths, comps, doffs, nent, cover_only)
743 } } } }
744
745 // 7z bc af 27 1c -- honest unsupported (no sovereign LZMA yet)
746 if z[0] == (0x37 as u8) { if z[1] == (0x7a as u8) { if z[2] == (0xbc as u8) { if z[3] == (0xaf as u8) {
747 er_p("CBX-7Z-UNSUPPORTED slug=" as *u8); er_w(1, slug); er_p(" reason=7z-lzma-not-built\n" as *u8)
748 return 0-2
749 } } } }
750
751 er_p("CBX-FAIL comic unknown-container slug=" as *u8); er_w(1, slug)
752 er_p(" magic0=" as *u8); er_pn(z[0] as i64)
753 er_p(" magic1=" as *u8); er_pn(z[1] as i64)
754 er_p("\n" as *u8)
755 return 0-1
756}
757
758func main(argc: i64, argv: *i64) -> i64 {
759 er_p("CBX: start (sniffed zip/rar4/rar5/pdf extract; natural sort; per-page magic gate; browser = last-mile zoom)\n" as *u8)
760 ensure_dir("knowledge/staging/media/reader" as *u8)
761
762 // argv-driven (additive): `nx_cbx_pages <path> <slug> [--cover]` extracts ANY comic/pdf on demand.
763 // .pdf routes straight to do_pdf; .cbz/.cbr/.cb7 (any cb*) go to the MAGIC-sniffing dispatcher.
764 // --cover extracts ONLY the natural-first image to <slug>/cover.jpg (fast grid thumbnails).
765 if argc >= 3 {
766 let apath: *u8 = argv[1] as *u8
767 let aslug: *u8 = argv[2] as *u8
768 var conly: i64 = 0
769 if argc >= 4 {
770 let a3: *u8 = argv[3] as *u8
771 if a3[0] == (45 as u8) { if a3[1] == (45 as u8) { if a3[2] == (99 as u8) { conly = 1 } } }
772 }
773 var dot: i64 = 0 - 1; var ax: i64 = 0
774 while apath[ax] != (0 as u8) { if apath[ax] == (46 as u8) { dot = ax } ax = ax + 1 }
775 var ar: i64 = 0 - 1
776 if dot >= 0 {
777 if ((apath[dot+1] as i64) | 32) == 112 { if ((apath[dot+2] as i64) | 32) == 100 { if ((apath[dot+3] as i64) | 32) == 102 { ar = do_pdf(apath, aslug) } } }
778 if ((apath[dot+1] as i64) | 32) == 99 { if ((apath[dot+2] as i64) | 32) == 98 { ar = do_cbz(apath, aslug, conly) } }
779 }
780 if ar == 0 { er_p("CBX-OK\n" as *u8); sys_exit(0); return 0 }
781 er_p("CBX-FAIL\n" as *u8); sys_exit(1); return 1
782 }
783
784 var ok: i64 = 0
785 var fail: i64 = 0
786
787 // (A) the guaranteed win: a REAL comic archive
788 let rc: i64 = do_cbz("/mnt/nas_homes/elderwesto/books/Unknown/dragonlancechronicles vol1 drag (1494)/dragonlancechronicles vol1 drag - Unknown.cbz" as *u8, "dragonlance_vol1" as *u8, 0)
789 if rc == 0 { ok = ok + 1 } else { fail = fail + 1 }
790
791 // (B) a REAL scanned/image PDF (honest: if vector/text -> UNSUPPORTED, not a fail)
792 let rp: i64 = do_pdf("/mnt/nas_scans/insurance_home_and_car/20240713_181802_Raven_Scan.pdf" as *u8, "raven_insurance" as *u8)
793 if rp == 0 { ok = ok + 1 }
794 if rp == 0-1 { fail = fail + 1 } // a real failure (UNSUPPORTED = 0-2 = honest skip, not a fail)
795
796 er_p("CBX: produced=" as *u8); er_pn(ok); er_p(" failed=" as *u8); er_pn(fail); er_p("\n" as *u8)
797 // GREEN requires the CBZ path to have rendered real zoomable pages (rc==0).
798 if rc == 0 { if fail == 0 { er_p("CBX-OK\n" as *u8); sys_exit(0); return 0 } }
799 er_p("CBX-FAIL\n" as *u8); sys_exit(1); return 1
800}