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}