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}