code wiki / _hdl_build / nx_kf8_book.nx
nx_kf8_book.nx source
↩ module page · 256 lines · 15225 B
1// nx_kf8_book.nx -- SOVEREIGN KF8 / AZW3 (the newer Kindle format) -> Nishi format. KF8 lives in the SAME PDB
2// container as MOBI6; a dual file has a MOBI6 part (record 0) + a KF8 part whose start record is named by EXTH 121
3// ("KF8 boundary"); a pure-KF8 file has mobiType==8 at record 0. The KF8 part has its OWN PalmDOC/MOBI header +
4// its own text records, compressed exactly like MOBI6 (PalmDOC or HUFF/CDIC) -- so this REUSES the validated
5// nx_palmdoc + nx_huffcdic codecs and only adds the boundary glue. nx_mobi_book (validated) is left untouched.
6// LEGAL LINE: detects encryptionType and DECLINES DRM (exit 2), never circumvents.
7// Rung 1: decompress the KF8 text flows + nx_html_to_text -> readable book.json (the text is all there). Rung 2 =
8// SKEL/FRAG/FDST reassembly for exact structure, + unify the decode glue with nx_mobi_book (DRY). expect_exit: 0
9// Usage: nx_kf8_book <mobi/azw3-path> <slug>. license_tier: ORIGINAL
10import "nx_syscalls.nx"
11import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
12import "nx_palmdoc.nx"
13import "nx_huffcdic.nx"
14import "nx_html_to_text.nx"
15import "nx_mobi_cover.nx"
16import "nx_chapter_split.nx"
17import "nx_charset.nx" // MOJIBAKE FIX: nx_charset_repair_utf8 -- stray Windows-1252 bytes -> clean UTF-8
18const K_MAGIC_1252: i64 = 1252
19const K_MAGIC_1024: i64 = 1024
20const K_MAGIC_8192: i64 = 8192
21const K_MAGIC_262144: i64 = 262144
22const K_MAGIC_17480: i64 = 17480
23
24func kf_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
25func kf_w(fd: i64, s: *u8) -> i64 { sys_write(fd, s, kf_slen(s)); return 0 }
26// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
27// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
28// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
29// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
30func kf_n(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 }
31func kf_p(s: *u8) -> i64 { kf_w(1, s); return 0 }
32func kf_pn(v: i64) -> i64 { kf_n(1, v); return 0 }
33func ensure_dir(path: *u8) -> i64 { __syscall(258, 0-100, path, 0x1ed, 0, 0, 0); return 0 }
34func be16(b: *u8, o: i64) -> i64 { return ((b[o] as i64)<<8)|(b[o+1] as i64) }
35func be32(b: *u8, o: i64) -> i64 { return ((b[o] as i64)<<24)|((b[o+1] as i64)<<16)|((b[o+2] as i64)<<8)|(b[o+3] as i64) }
36func find_sub(hay: *u8, hl: i64, needle: *u8) -> i64 {
37 let nl: i64 = kf_slen(needle); if nl == 0 { return 0-1 }
38 var i: i64 = 0
39 while i + nl <= hl { var k: i64=0; var hit: i64=1; while k<nl { if hay[i+k]!=needle[k]{hit=0;k=nl}else{k=k+1} } if hit==1 {return i} i=i+1 }
40 return 0-1
41}
42func 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 }
43// JSON string emit. MOJIBAKE FIX: repair each metadata string to clean UTF-8 first (idempotent on valid UTF-8).
44func er_wjson(fd: i64, s: *u8) -> i64 {
45 var sl: i64 = 0; while s[sl] != (0 as u8) { sl = sl + 1 }
46 let rcap: i64 = sl * 3 + 16
47 let rep: *u8 = sys_mmap(rcap)
48 let rn: i64 = nx_charset_repair_utf8(s, sl, rep, rcap)
49 var i: i64 = 0
50 while i < rn { let c: i64 = rep[i] as i64; if c == 0x22 { sys_write(fd, "\\\"" as *u8, 2) } else { if c == 0x5c { sys_write(fd, "\\\\" as *u8, 2) } else { if c < 0x20 { sys_write(fd, " " as *u8, 1) } else { sys_write(fd, (rep as i64 + i) as *u8, 1) } } } i = i + 1 }
51 sys_munmap(rep, rcap)
52 return 0
53}
54func utf8_put(out: *u8, o: i64, cp: i64) -> i64 {
55 if cp < 0x80 { out[o] = cp as u8; return o + 1 }
56 if cp < 0x800 { out[o] = (0xC0 | (cp >> 6)) as u8; out[o+1] = (0x80 | (cp & 0x3F)) as u8; return o + 2 }
57 if cp < 0x10000 { out[o] = (0xE0 | (cp >> 12)) as u8; out[o+1] = (0x80 | ((cp >> 6) & 0x3F)) as u8; out[o+2] = (0x80 | (cp & 0x3F)) as u8; return o + 3 }
58 out[o] = (0xF0 | (cp >> 18)) as u8; out[o+1] = (0x80 | ((cp >> 12) & 0x3F)) as u8; out[o+2] = (0x80 | ((cp >> 6) & 0x3F)) as u8; out[o+3] = (0x80 | (cp & 0x3F)) as u8; return o + 4
59}
60func decode_numeric_refs(txt: *u8, n: i64, out: *u8, outcap: i64) -> i64 {
61 var i: i64 = 0
62 var o: i64 = 0
63 while i < n {
64 let c: i64 = txt[i] as i64
65 var done: i64 = 0
66 if c == 0x26 { if i + 1 < n { if (txt[i+1] as i64) == 0x23 {
67 var j: i64 = i + 2
68 var hex: i64 = 0
69 if j < n { let xc: i64 = txt[j] as i64; if xc == 0x78 { hex = 1; j = j + 1 } else { if xc == 0x58 { hex = 1; j = j + 1 } } }
70 var cp: i64 = 0
71 var ndig: i64 = 0
72 var scan: i64 = 1
73 while scan == 1 {
74 if j >= n { scan = 0 } else {
75 let dc: i64 = txt[j] as i64
76 var dv: i64 = 0-1
77 if hex == 1 { if dc >= 48 { if dc <= 57 { dv = dc - 48 } } if dc >= 97 { if dc <= 102 { dv = dc - 87 } } if dc >= 65 { if dc <= 70 { dv = dc - 55 } } }
78 else { if dc >= 48 { if dc <= 57 { dv = dc - 48 } } }
79 if dv < 0 { scan = 0 } else { if hex == 1 { cp = cp * 16 + dv } else { cp = cp * 10 + dv } ndig = ndig + 1; j = j + 1 }
80 }
81 }
82 if ndig > 0 { if j < n { if (txt[j] as i64) == 0x3b { if cp > 0 { if cp <= 0x10FFFF { if o + 4 < outcap { o = utf8_put(out, o, cp) } i = j + 1; done = 1 } } } } }
83 } } }
84 if done == 0 { if o < outcap - 1 { out[o] = txt[i]; o = o + 1 } i = i + 1 }
85 }
86 out[o] = 0 as u8
87 return o
88}
89func exth_copy(z: *u8, p: i64, rlen: i64, out: *u8, cap: i64) -> i64 {
90 var o: i64 = 0; var s: i64 = p + 8; let e: i64 = p + rlen
91 while s < e { if o < cap-1 { out[o] = z[s]; o = o + 1 } s = s + 1 }
92 out[o] = 0 as u8; return o
93}
94
95const KCAP: i64 = 16777216
96
97func do_kf8(path: *u8, slug: *u8) -> i64 {
98 let lb: *i64 = sys_mmap(16) as *i64
99 let z: *u8 = sys_read_file(path, lb)
100 if (z as i64) == 0 { kf_p("KF8-FAIL read " as *u8); kf_p(path); kf_p("\n" as *u8); return 0-1 }
101 let zl: i64 = lb[0]
102 if zl < 80 { kf_p("KF8-FAIL too-small\n" as *u8); return 0-1 }
103 let numRec: i64 = be16(z, 76)
104 let rec0off: i64 = be32(z, 78)
105
106 // ---- EXTH: KF8 boundary (121) + metadata (author 100, title 503) ----
107 let title: *u8 = sys_mmap(K_MAGIC_1024); title[0]=0 as u8
108 let author: *u8 = sys_mmap(K_MAGIC_1024); author[0]=0 as u8
109 let publisher: *u8 = sys_mmap(K_MAGIC_1024); publisher[0]=0 as u8 // EXTH 101
110 let isbn: *u8 = sys_mmap(256); isbn[0]=0 as u8 // EXTH 104
111 let pubdate: *u8 = sys_mmap(256); pubdate[0]=0 as u8 // EXTH 106
112 let descr_raw: *u8 = sys_mmap(K_MAGIC_8192); descr_raw[0]=0 as u8 // EXTH 103 description (HTML)
113 var boundary: i64 = 0-1
114 var coveroff: i64 = 0; var coverfound: i64 = 0; var thumboff: i64 = 0; var thumbfound: i64 = 0 // EMBEDDED cover (shared absolute image records)
115 let rec1off: i64 = be32(z, 78 + 8)
116 var r0ext: i64 = rec1off - rec0off
117 if r0ext <= 0 { r0ext = zl - rec0off }
118 let exrel: i64 = find_sub((z as i64 + rec0off) as *u8, r0ext, "EXTH" as *u8)
119 if exrel >= 0 {
120 let exth: i64 = rec0off + exrel
121 let cnt: i64 = be32(z, exth + 8)
122 var p: i64 = exth + 12; var ci: i64 = 0
123 while ci < cnt {
124 if p + 8 > zl { ci = cnt } else {
125 let rtype: i64 = be32(z, p); let rlen: i64 = be32(z, p+4)
126 if rlen < 8 { ci = cnt } else {
127 if rtype == 121 { boundary = be32(z, p+8) }
128 if rtype == 100 { exth_copy(z, p, rlen, author, K_MAGIC_1024) }
129 if rtype == 503 { exth_copy(z, p, rlen, title, K_MAGIC_1024) }
130 if rtype == 201 { if rlen >= 12 { coveroff = be32(z, p+8); coverfound = 1 } } // EXTH 201 coveroffset
131 if rtype == 202 { if rlen >= 12 { thumboff = be32(z, p+8); thumbfound = 1 } } // EXTH 202 thumboffset
132 if rtype == 101 { exth_copy(z, p, rlen, publisher, K_MAGIC_1024) } // EXTH 101 publisher
133 if rtype == 104 { exth_copy(z, p, rlen, isbn, 256) } // EXTH 104 isbn
134 if rtype == 106 { exth_copy(z, p, rlen, pubdate, 256) } // EXTH 106 publishing date
135 if rtype == 103 { exth_copy(z, p, rlen, descr_raw, K_MAGIC_8192) } // EXTH 103 description (HTML)
136 p = p + rlen; ci = ci + 1
137 }
138 }
139 }
140 }
141 if title[0]==(0 as u8) { var k: i64=0; while k<31 { let c: i64=z[k] as i64; if c==0 {k=31} else {title[k]=z[k] as u8; k=k+1} } title[k]=0 as u8 }
142
143 // ---- locate the KF8 part: dual (EXTH 121) or pure-KF8 (mobiType==8) ----
144 let mobiType: i64 = be32(z, rec0off + 16 + 8)
145 var kfrec0: i64 = 0
146 var B: i64 = 0
147 if boundary >= 0 { if boundary < numRec { B = boundary; kfrec0 = be32(z, 78 + B*8) } else { kf_p("KF8-FAIL boundary-out-of-range\n" as *u8); return 0-1 } }
148 else { if mobiType == 8 { B = 0; kfrec0 = rec0off } else { kf_p("KF8-NOT-KF8 (no EXTH-121 boundary and mobiType=" as *u8); kf_pn(mobiType); kf_p("; this is a MOBI6-only file -- use nx_mobi_book)\n" as *u8); return 0-1 } }
149
150 // ---- KF8 PalmDOC header ----
151 let comp: i64 = be16(z, kfrec0 + 0)
152 let textLen: i64 = be32(z, kfrec0 + 4)
153 let recCount: i64 = be16(z, kfrec0 + 8)
154 let enc: i64 = be16(z, kfrec0 + 12)
155 if enc != 0 { kf_p("KF8-DRM-DECLINED encryptionType=" as *u8); kf_pn(enc); kf_p(" (decode a DRM-free copy; no circumvention)\n" as *u8); sys_exit(2); return 2 }
156
157 // ---- HUFF/CDIC tables (if compressed that way) ----
158 let d1cl: *i64 = sys_mmap(256*8) as *i64
159 let d1tm: *i64 = sys_mmap(256*8) as *i64
160 let d1mx: *i64 = sys_mmap(256*8) as *i64
161 let hmin: *i64 = sys_mmap(64*8) as *i64
162 let hmax: *i64 = sys_mmap(64*8) as *i64
163 let dptr: *i64 = sys_mmap(K_MAGIC_262144*8) as *i64
164 let dlen: *i64 = sys_mmap(K_MAGIC_262144*8) as *i64
165 let dflag: *i64 = sys_mmap(K_MAGIC_262144*8) as *i64
166 let dcnt: *i64 = sys_mmap(8) as *i64; dcnt[0]=0
167 var is_huff: i64 = 0
168 if comp == K_MAGIC_17480 { is_huff = 1 } else { if comp != 1 { if comp != 2 { kf_p("KF8-UNSUPPORTED compression=" as *u8); kf_pn(comp); kf_p("\n" as *u8); return 0-1 } } }
169 if is_huff == 1 {
170 let huffidx: i64 = B + be32(z, kfrec0 + 112) // KF8 part record refs are RELATIVE to the boundary (B+40 = abs 86)
171 let huffcnt: i64 = be32(z, kfrec0 + 116)
172 let hs: i64 = be32(z, 78 + huffidx*8)
173 var he: i64 = zl; if huffidx + 1 < numRec { he = be32(z, 78 + (huffidx+1)*8) }
174 if huff_load((z as i64 + hs) as *u8, he - hs, d1cl, d1tm, d1mx, hmin, hmax) != 0 { kf_p("KF8-HUFF-FAIL bad-HUFF-record\n" as *u8); return 0-1 }
175 var ci: i64 = 1
176 while ci < huffcnt {
177 let cidx: i64 = huffidx + ci
178 let cs: i64 = be32(z, 78 + cidx*8)
179 var ce: i64 = zl; if cidx + 1 < numRec { ce = be32(z, 78 + (cidx+1)*8) }
180 cdic_load((z as i64 + cs) as *u8, ce - cs, dptr, dlen, dflag, dcnt)
181 ci = ci + 1
182 }
183 }
184
185 // ---- decompress KF8 text records (boundary+1 .. boundary+recCount) ----
186 let tbuf: *u8 = sys_mmap(KCAP)
187 var to: i64 = 0
188 var ri: i64 = B + 1
189 let last: i64 = B + recCount
190 while ri <= last {
191 if ri >= numRec { ri = last + 1 } else {
192 let rs: i64 = be32(z, 78 + ri*8)
193 var re: i64 = zl; if ri + 1 < numRec { re = be32(z, 78 + (ri+1)*8) }
194 let rlen: i64 = re - rs
195 if rlen > 0 { if rs + rlen <= zl {
196 if comp == 2 { to = to + palmdoc_decompress((z as i64 + rs) as *u8, rlen, (tbuf as i64 + to) as *u8, KCAP - to) }
197 else { if is_huff == 1 { to = to + hc_unpack((z as i64 + rs) as *u8, rlen, (tbuf as i64 + to) as *u8, KCAP - to, d1cl, d1tm, d1mx, hmin, hmax, dptr, dlen, dflag, dcnt[0]) }
198 else { var k: i64=0; while k < rlen { if to < KCAP { tbuf[to]=z[rs+k]; to=to+1 } k=k+1 } } }
199 } }
200 ri = ri + 1
201 }
202 }
203 var htmllen: i64 = to
204
205 // ---- (chapter split + per-chapter html_to_text + chap<i>.txt are done by nx_chapter_split, in book.json below) ----
206
207 // description (EXTH 103) is HTML -> strip tags + decode numeric refs for book.json
208 let descr_clean: *u8 = sys_mmap(K_MAGIC_8192); descr_clean[0] = 0 as u8
209 if descr_raw[0] != (0 as u8) {
210 let dtmp: *u8 = sys_mmap(K_MAGIC_8192)
211 let dtn: i64 = nx_html_to_text(descr_raw, kf_slen(descr_raw), dtmp, K_MAGIC_8192)
212 decode_numeric_refs(dtmp, dtn, descr_clean, K_MAGIC_8192)
213 }
214
215 ensure_dir("knowledge/staging/media/reader" as *u8)
216 let dir: *u8 = sys_mmap(512)
217 var dl: i64 = er_cat(dir, 0, "knowledge/staging/media/reader/" as *u8); dl = er_cat(dir, dl, slug); dir[dl]=0 as u8
218 ensure_dir(dir)
219 // EMBEDDED cover (offline, from the file; shared with nx_mobi_book via nx_mobi_cover). KF8 image records are
220 // absolute, so the record-0 First Image index + EXTH 201 reach the SAME cover the MOBI6 part references.
221 let covername: *u8 = sys_mmap(64); covername[0] = 0 as u8
222 mobi_extract_cover(z, zl, numRec, rec0off, coveroff, coverfound, thumboff, thumbfound, dir, covername)
223 let jp: *u8 = sys_mmap(K_MAGIC_1024)
224 var jl: i64 = er_cat(jp, 0, dir as *u8); jl = er_cat(jp, jl, "/book.json" as *u8); jp[jl]=0 as u8
225 let jfd: i64 = sys_openat_wr(jp, 0x1a4)
226 if jfd < 0 { kf_p("KF8-FAIL open-json\n" as *u8); return 0-1 }
227 kf_w(jfd, "{\"title\":\"" as *u8); er_wjson(jfd, title)
228 kf_w(jfd, "\",\"author\":\"" as *u8); er_wjson(jfd, author)
229 kf_w(jfd, "\",\"publisher\":\"" as *u8); er_wjson(jfd, publisher)
230 kf_w(jfd, "\",\"pubdate\":\"" as *u8); er_wjson(jfd, pubdate)
231 kf_w(jfd, "\",\"isbn\":\"" as *u8); er_wjson(jfd, isbn)
232 kf_w(jfd, "\",\"description\":\"" as *u8); er_wjson(jfd, descr_clean)
233 kf_w(jfd, "\",\"format\":\"kf8\",\"rfix\":1,\"slug\":\"" as *u8); er_wjson(jfd, slug) // rfix=reader-fix schema -> server re-extracts older caches
234 kf_w(jfd, "\"," as *u8)
235 let totalp: *i64 = sys_mmap(8) as *i64; totalp[0]=0
236 let nseg: i64 = chapter_split(tbuf, htmllen, dir, title, jfd, totalp, "kf8\x00" as *u8) // split -> chap<i>.txt + chapters[]/toc[]
237 let total: i64 = totalp[0]
238 kf_w(jfd, ",\"assets\":[],\"nassets\":0,\"cover\":\"" as *u8); er_wjson(jfd, covername)
239 kf_w(jfd, "\"}" as *u8)
240 sys_close(jfd)
241
242 kf_p("KF8-BOOK title=\"" as *u8); kf_w(1, title); kf_p("\" author=\"" as *u8); kf_w(1, author)
243 kf_p("\" boundary=" as *u8); kf_pn(B); kf_p(" comp=" as *u8); kf_pn(comp); kf_p(" kf8_text_records=" as *u8); kf_pn(recCount)
244 kf_p(" html_bytes=" as *u8); kf_pn(htmllen); kf_p(" chapters=" as *u8); kf_pn(nseg); kf_p(" chars=" as *u8); kf_pn(total); kf_p(" -> " as *u8); kf_p(dir); kf_p("\n" as *u8)
245 if total < 20 { return 0-1 }
246 return 0
247}
248
249func main(argc: i64, argv: *i64) -> i64 {
250 var path: *u8 = "knowledge/benchmarks/refs/libmobi-sample-huffdic.ref" as *u8
251 var slug: *u8 = "real_kf8" as *u8
252 if argc >= 3 { path = argv[1] as *u8; slug = argv[2] as *u8 }
253 let r: i64 = do_kf8(path, slug)
254 if r == 0 { kf_p("KF8-BOOK-OK\n" as *u8); sys_exit(0); return 0 }
255 kf_p("KF8-BOOK-FAIL\n" as *u8); sys_exit(1); return 1
256}