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