code wiki / _hdl_build / nx_media_cover_lib.nx
nx_media_cover_lib.nx source
↩ module page · 229 lines · 11508 B
1// nx_media_cover_lib.nx -- SOVEREIGN book-cover resolution for the media server (/api/cover).
2// A book's embedded cover is extracted to knowledge/staging/media/reader/<slug>/<coverfile> as a side effect of
3// decoding (nx_epub_book / nx_mobi_book / nx_kf8_book all write the image + record "cover":"<file>" in book.json).
4// This lib maps a NAS book path -> that on-disk cover image, extracting on cache-miss, so the library grid can show
5// real covers WITHOUT generating or downloading anything (operator: no internet, extract embedded only). Shared by
6// nx_media_server (serve) and nx_media_cover_gate (test) so the slug + resolution logic has ONE definition.
7// license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_itoa_lib.nx" // THE shared MSB-first emitter -- mc_dec composes it
10const MC_MAGIC_1469598103: i64 = 1469598103
11const MC_MAGIC_72057594037927931: i64 = 72057594037927931
12const MC_MAGIC_262144: i64 = 262144
13
14const MC_READER_ROOT: *u8 = "knowledge/staging/media/reader/"
15
16func mc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
17func mc_app(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { dst[off+i] = src[i]; i = i + 1 } return off + n }
18func mc_apps(dst: *u8, off: i64, s: *u8) -> i64 { return mc_app(dst, off, s, mc_slen(s)) }
19// ★COMPOSES nxi_buf (2026-08-15, found by nx_itoaclone). Same three defects as every clone of this
20// shape: a negative emitted ZERO BYTES, INT64_MIN would emit a lone '-', and sys_mmap(28) PER CALL was
21// never freed. Identical contract (digits only, no NUL, returns the offset after them).
22func mc_dec(dst: *u8, off: i64, v: i64) -> i64 { return nxi_buf(dst, off, v) }
23func mc_hexc(d: i64) -> i64 { if d < 10 { return 48 + d } return 87 + d }
24
25// 1 if hay[0..hl] contains the needle (path-traversal guard).
26func mc_contains(hay: *u8, hl: i64, needle: *u8) -> i64 {
27 let nl: i64 = mc_slen(needle)
28 if nl == 0 { return 0 }
29 var i: i64 = 0
30 while i + nl <= hl {
31 var k: i64 = 0; var hit: i64 = 1
32 while k < nl { if hay[i+k] != needle[k] { hit = 0; k = nl } else { k = k + 1 } }
33 if hit == 1 { return 1 }
34 i = i + 1
35 }
36 return 0
37}
38
39// EXACT copy of nx_media_server ms_pathslug -- the slug MUST match the one the open/extract path uses, or the
40// cover dir won't be found. 'b' + 14 hex of an FNV-ish hash of the full NAS path.
41func mc_pathslug(p: *u8, n: i64, out: *u8) -> i64 {
42 var h: i64 = MC_MAGIC_1469598103
43 var i: i64 = 0
44 while i < n { h = (h * 131 + (p[i] as i64)) % MC_MAGIC_72057594037927931; i = i + 1 }
45 if h < 0 { h = 0 - h }
46 out[0] = 98 as u8
47 let tmp: *u8 = sys_mmap(32)
48 var k: i64 = 0; var hh: i64 = h
49 while k < 14 { tmp[k] = mc_hexc(hh % 16) as u8; hh = hh / 16; k = k + 1 }
50 var j: i64 = 0
51 while j < 14 { out[1 + j] = tmp[13 - j]; j = j + 1 }
52 out[15] = 0 as u8
53 return 15
54}
55
56// content-type by extension for the served image (jpg/jpeg/png/gif/webp); octet-stream otherwise.
57func mc_ctype(path: *u8) -> *u8 {
58 let n: i64 = mc_slen(path)
59 var dot: i64 = 0 - 1; var i: i64 = 0
60 while i < n { if path[i] == (46 as u8) { dot = i } i = i + 1 }
61 if dot < 0 { return "application/octet-stream" as *u8 }
62 let e1: i64 = (path[dot+1] as i64) | 32
63 var e2: i64 = 0; if dot + 2 < n { e2 = (path[dot+2] as i64) | 32 }
64 var e3: i64 = 0; if dot + 3 < n { e3 = (path[dot+3] as i64) | 32 }
65 if e1 == 106 { if e2 == 112 { return "image/jpeg" as *u8 } } // jp(g/eg)
66 if e1 == 112 { if e2 == 110 { if e3 == 103 { return "image/png" as *u8 } } } // png
67 if e1 == 103 { if e2 == 105 { if e3 == 102 { return "image/gif" as *u8 } } } // gif
68 if e1 == 119 { if e2 == 101 { if e3 == 98 { return "image/webp" as *u8 } } } // web(p)
69 return "application/octet-stream" as *u8
70}
71
72// pull the "cover":"<file>" value out of book.json into out (NUL-terminated). returns length (0 if absent).
73func mc_cover_field(jbuf: *u8, jn: i64, out: *u8, cap: i64) -> i64 {
74 let key: *u8 = "\"cover\":\"" as *u8
75 let kl: i64 = mc_slen(key)
76 var i: i64 = 0
77 while i + kl <= jn {
78 var hit: i64 = 1; var k: i64 = 0
79 while k < kl { if jbuf[i+k] != key[k] { hit = 0; k = kl } else { k = k + 1 } }
80 if hit == 1 {
81 var s: i64 = i + kl; var o: i64 = 0
82 while s < jn { if jbuf[s] == (34 as u8) { s = jn } else { if o < cap - 1 { out[o] = jbuf[s]; o = o + 1 } s = s + 1 } }
83 out[o] = 0 as u8
84 return o
85 }
86 i = i + 1
87 }
88 out[0] = 0 as u8
89 return 0
90}
91
92// fork the right decoder to extract the cover to reader/<slug>/. epub -> nx_epub_book, mobi/azw/azw3 ->
93// nx_mobi_book (full extraction; cover recorded in book.json). cbz/cbr/cb7 -> nx_cbx_pages --cover
94// (FAST: only the natural-first page image is pulled to <slug>/cover.jpg -- a shelf of comics must not
95// pay a full 100MB unpack per cover request). pdf/fb2 remain text-card fallbacks.
96func mc_extract(path: *u8, pn: i64, slug: *u8) -> i64 {
97 var dot: i64 = 0 - 1; var x: i64 = 0
98 while x < pn { if path[x] == (46 as u8) { dot = x } x = x + 1 }
99 if dot < 0 { return 0 }
100 if dot + 3 >= pn { return 0 }
101 let e1: i64 = (path[dot+1] as i64) | 32
102 let e2: i64 = (path[dot+2] as i64) | 32
103 let e3: i64 = (path[dot+3] as i64) | 32
104 var ext: *u8 = 0 as *u8
105 var coverarg: i64 = 0
106 if e1 == 101 { if e2 == 112 { if e3 == 117 { ext = "./_offc/nx_epub_book.elf\x00" as *u8 } } } // epub
107 if e1 == 109 { if e2 == 111 { if e3 == 98 { ext = "./_offc/nx_mobi_book.elf\x00" as *u8 } } } // mob(i)
108 if e1 == 97 { if e2 == 122 { if e3 == 119 { ext = "./_offc/nx_mobi_book.elf\x00" as *u8 } } } // azw(/azw3)
109 if e1 == 99 { if e2 == 98 { ext = "./_offc/nx_cbx_pages.elf\x00" as *u8; coverarg = 1 } } // cb(z/r/7) -> --cover fast path
110 if (ext as i64) == 0 { return 0 }
111 let pid: i64 = sys_fork()
112 if pid == 0 {
113 let av: *i64 = sys_mmap(64) as *i64
114 av[0] = ext as i64; av[1] = path as i64; av[2] = slug as i64
115 if coverarg == 1 { av[3] = "--cover" as *u8 as i64; av[4] = 0 } else { av[3] = 0 }
116 sys_execve(ext, av, 0 as *i64)
117 sys_exit(127)
118 }
119 let st: *i64 = sys_mmap(8) as *i64
120 sys_wait4(pid, st, 0)
121 return 1
122}
123
124// pull the FIRST "file":"<name>" out of cbx.json (pages are natural-sorted, so first = page 0 = the cover).
125func mc_first_page_field(jbuf: *u8, jn: i64, out: *u8, cap: i64) -> i64 {
126 let key: *u8 = "\"file\":\"" as *u8
127 let kl: i64 = mc_slen(key)
128 var i: i64 = 0
129 while i + kl <= jn {
130 var hit: i64 = 1; var k: i64 = 0
131 while k < kl { if jbuf[i+k] != key[k] { hit = 0; k = kl } else { k = k + 1 } }
132 if hit == 1 {
133 var s: i64 = i + kl; var o: i64 = 0
134 while s < jn { if jbuf[s] == (34 as u8) { s = jn } else { if o < cap - 1 { out[o] = jbuf[s]; o = o + 1 } s = s + 1 } }
135 out[o] = 0 as u8
136 return o
137 }
138 i = i + 1
139 }
140 out[0] = 0 as u8
141 return 0
142}
143
144// try to resolve a cover for an ALREADY-EXTRACTED slug dir (no forking): 1 = outfull set, 0 = nothing yet.
145// Order: book.json "cover" field -> <slug>/cover.jpg (comic fast-cover) -> cbx.json page 0 (comics + imgbooks).
146func mc_try_resolved(slug: *u8, outfull: *u8) -> i64 {
147 let jp: *u8 = sys_mmap(512); var jo: i64 = 0
148 jo = mc_apps(jp, jo, MC_READER_ROOT)
149 var si: i64 = 0; while slug[si] != (0 as u8) { jp[jo] = slug[si]; jo = jo + 1; si = si + 1 }
150 let base: i64 = jo
151 // 1) book.json "cover"
152 jo = mc_apps(jp, base, "/book.json" as *u8); jp[jo] = 0 as u8
153 let lb: *i64 = sys_mmap(8) as *i64
154 let jbuf: *u8 = sys_read_file(jp, lb)
155 if (jbuf as i64) != 0 {
156 let cover: *u8 = sys_mmap(256)
157 let cn: i64 = mc_cover_field(jbuf, lb[0], cover, 256)
158 if cn > 0 {
159 var fo: i64 = 0
160 fo = mc_apps(outfull, fo, MC_READER_ROOT)
161 var s2: i64 = 0; while slug[s2] != (0 as u8) { outfull[fo] = slug[s2]; fo = fo + 1; s2 = s2 + 1 }
162 outfull[fo] = 47 as u8; fo = fo + 1
163 var ci: i64 = 0; while cover[ci] != (0 as u8) { outfull[fo] = cover[ci]; fo = fo + 1; ci = ci + 1 }
164 outfull[fo] = 0 as u8
165 let f1: i64 = sys_openat_rd(outfull)
166 if f1 >= 0 { sys_close(f1); return 1 }
167 }
168 }
169 // 2) <slug>/cover.jpg (written by nx_cbx_pages --cover)
170 jo = mc_apps(jp, base, "/cover.jpg" as *u8); jp[jo] = 0 as u8
171 let f2: i64 = sys_openat_rd(jp)
172 if f2 >= 0 {
173 sys_close(f2)
174 var fo2: i64 = 0; var s3: i64 = 0
175 while jp[s3] != (0 as u8) { outfull[fo2] = jp[s3]; fo2 = fo2 + 1; s3 = s3 + 1 }
176 outfull[fo2] = 0 as u8
177 return 1
178 }
179 // 3) cbx.json page 0 (an extracted comic / promoted image-book IS its own cover source)
180 jo = mc_apps(jp, base, "/cbx.json" as *u8); jp[jo] = 0 as u8
181 let lb3: *i64 = sys_mmap(8) as *i64
182 let cbuf: *u8 = sys_read_file(jp, lb3)
183 if (cbuf as i64) != 0 {
184 let pg: *u8 = sys_mmap(256)
185 let pn3: i64 = mc_first_page_field(cbuf, lb3[0], pg, 256)
186 if pn3 > 0 {
187 var fo3: i64 = 0
188 fo3 = mc_apps(outfull, fo3, MC_READER_ROOT)
189 var s4: i64 = 0; while slug[s4] != (0 as u8) { outfull[fo3] = slug[s4]; fo3 = fo3 + 1; s4 = s4 + 1 }
190 outfull[fo3] = 47 as u8; fo3 = fo3 + 1
191 var pi: i64 = 0; while pg[pi] != (0 as u8) { outfull[fo3] = pg[pi]; fo3 = fo3 + 1; pi = pi + 1 }
192 outfull[fo3] = 0 as u8
193 let f3: i64 = sys_openat_rd(outfull)
194 if f3 >= 0 { sys_close(f3); return 1 }
195 }
196 }
197 return 0
198}
199
200// resolve a NAS book path -> its on-disk cover image file (extracting on cache-miss).
201// returns 1 = ok (outfull = cover file path, NUL-terminated); 0 = no cover; -1 = rejected (path traversal).
202// Resolution order (mc_try_resolved): book.json "cover" -> <slug>/cover.jpg -> cbx.json page 0; one
203// extraction attempt on cold miss (epub/mobi full decode; cbz/cbr FAST --cover), then re-resolve.
204func mc_cover_resolve(path: *u8, pn: i64, outfull: *u8, cap: i64) -> i64 {
205 if mc_contains(path, pn, ".." as *u8) == 1 { return 0 - 1 }
206 let slug: *u8 = sys_mmap(32); mc_pathslug(path, pn, slug)
207 if mc_try_resolved(slug, outfull) == 1 { return 1 }
208 if mc_extract(path, pn, slug) == 0 { return 0 }
209 if mc_try_resolved(slug, outfull) == 1 { return 1 }
210 return 0
211}
212
213// serve the resolved cover image over an HTTP connection (streamed, with a day of cache). Caller already
214// resolved + authorized. Closes cfd.
215func mc_serve_image(cfd: i64, full: *u8) -> i64 {
216 let ct: *u8 = mc_ctype(full)
217 let fd: i64 = sys_openat_rd(full)
218 if fd < 0 { let d: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; sys_write(cfd, d, mc_slen(d)); sys_close(cfd); return 0 }
219 let sz: i64 = sys_lseek(fd, 0, 2); sys_lseek(fd, 0, 0)
220 let hdr: *u8 = sys_mmap(256); var h: i64 = 0
221 h = mc_apps(hdr, h, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); h = mc_apps(hdr, h, ct)
222 h = mc_apps(hdr, h, "\r\nCache-Control: max-age=86400\r\nContent-Length: " as *u8); h = mc_dec(hdr, h, sz)
223 h = mc_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8)
224 sys_write(cfd, hdr, h)
225 let chunk: *u8 = sys_mmap(MC_MAGIC_262144); var go: i64 = 1
226 while go == 1 { let r: i64 = sys_read(fd, chunk, MC_MAGIC_262144); if r <= 0 { go = 0 } else { sys_write(cfd, chunk, r) } }
227 sys_close(fd); sys_close(cfd)
228 return 0
229}