code wiki / _hdl_build / nx_reader_zoomserve.nx
nx_reader_zoomserve.nx source
↩ module page · 191 lines · 8458 B
1// nx_reader_zoomserve.nx -- the reader daemon's INTELLIGENT-ZOOM prep endpoint (lib; the lib+gate split so
2// nx_reader_zoomserve_gate exercises the handler in-process, no socket). GET /api/zoomprep?slug=<slug>&
3// file=<pagefile>&rtl=<0|1> ensures the tile pyramid + panel map exist for ONE comic/scan page:
4// reader/<slug>/<file> --fork ./_offc/nx_reader_page_prep.elf--> reader/<slug>/zoom/<stem>/{dz.json,
5// panels.json, <level>/<col>_<row>.jpg}
6// The tiles are then served by the EXISTING allowlisted /api/book route (p=reader/<slug>/zoom/<stem>/...), so
7// serving needs ZERO new code -- this endpoint only ensures the artifacts exist (lazy, cache-first, idempotent).
8// FAIL-CLOSED: slug must be a single safe component (alnum/_/-), file a safe leaf (alnum/_/-/., no "..", no
9// leading dot, exactly path-component chars) -> 403; missing source page -> 404; prep failure -> 500 with the
10// exit code; SUCCESS is decided by dz.json EXISTING after the fork (never by exit code alone -- no fake green).
11// The daemon behind THE WALL calls zs_handle_zoomprep; CWD is the repo root (paths repo-relative, same contract
12// as ms_handle_book / ms_run_extractor). license_tier: ORIGINAL
13import "nx_syscalls.nx"
14
15const ZS_ROOT: *u8 = "knowledge/staging/media/reader/"
16const ZS_PREP_ELF: *u8 = "./_offc/nx_reader_page_prep.elf"
17const ZS_TILESIZE: *u8 = "512" // argv literal as a const (inline "..." as *u8 as i64 in an argv[] once miscompiled -> EFAULT/127; see nx_hostctl HC_READER note)
18
19func zs_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
20func zs_apps(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 }
21func zs_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 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 }
22
23// ?key=value extractor (self-contained twin of ms_qval; value stops at '&', space, or end)
24func zs_qval(p: *u8, plen: i64, key: *u8, out: *u8, outcap: i64) -> i64 {
25 let kl: i64 = zs_slen(key)
26 var i: i64 = 0
27 while i + kl + 1 <= plen {
28 var hit: i64 = 1
29 var j: i64 = 0
30 while j < kl { if p[i + j] != key[j] { hit = 0; j = kl } else { j = j + 1 } }
31 if hit == 1 { if p[i + kl] == (61 as u8) {
32 var s: i64 = i + kl + 1
33 var o: i64 = 0
34 while s < plen {
35 let c: i64 = p[s] as i64
36 if c == 38 { s = plen } else { if c == 32 { s = plen } else {
37 if o < outcap - 1 { out[o] = p[s]; o = o + 1 }
38 s = s + 1
39 } }
40 }
41 out[o] = 0 as u8
42 return o
43 } }
44 i = i + 1
45 }
46 out[0] = 0 as u8
47 return 0
48}
49
50// slug: single safe path component, alnum/_/- only (mirror of ms_slug_safe)
51func zs_slug_safe(s: *u8) -> i64 {
52 let n: i64 = zs_slen(s)
53 if n <= 0 { return 0 }
54 if n > 200 { return 0 }
55 var i: i64 = 0
56 while i < n {
57 let c: i64 = s[i] as i64
58 var ok: i64 = 0
59 if c >= 48 { if c <= 57 { ok = 1 } }
60 if c >= 65 { if c <= 90 { ok = 1 } }
61 if c >= 97 { if c <= 122 { ok = 1 } }
62 if c == 95 { ok = 1 }
63 if c == 45 { ok = 1 }
64 if ok == 0 { return 0 }
65 i = i + 1
66 }
67 return 1
68}
69
70// page-file leaf: alnum/_/-/. ; no "..", no leading dot (so no traversal, no dotfiles, no "/")
71func zs_fname_safe(s: *u8) -> i64 {
72 let n: i64 = zs_slen(s)
73 if n <= 0 { return 0 }
74 if n > 128 { return 0 }
75 if s[0] == (46 as u8) { return 0 }
76 var i: i64 = 0
77 while i < n {
78 let c: i64 = s[i] as i64
79 var ok: i64 = 0
80 if c >= 48 { if c <= 57 { ok = 1 } }
81 if c >= 65 { if c <= 90 { ok = 1 } }
82 if c >= 97 { if c <= 122 { ok = 1 } }
83 if c == 95 { ok = 1 }
84 if c == 45 { ok = 1 }
85 if c == 46 { ok = 1 }
86 if ok == 0 { return 0 }
87 i = i + 1
88 }
89 var j: i64 = 0
90 while j + 1 < n { if s[j] == (46 as u8) { if s[j + 1] == (46 as u8) { return 0 } } j = j + 1 }
91 return 1
92}
93
94func zs_exists(path: *u8) -> i64 { let fd: i64 = sys_openat_rd(path); if fd < 0 { return 0 } sys_close(fd); return 1 }
95func zs_mkdir(path: *u8) -> i64 { __syscall(258, 0 - 100, path as i64, 0x1ed, 0, 0, 0); return 0 }
96
97func zs_emit(cfd: i64, status: *u8, body: *u8) -> i64 {
98 let bn: i64 = zs_slen(body)
99 let hdr: *u8 = sys_mmap(512)
100 var h: i64 = 0
101 h = zs_apps(hdr, h, "HTTP/1.1 " as *u8)
102 h = zs_apps(hdr, h, status)
103 h = zs_apps(hdr, h, "\r\nContent-Type: application/json; charset=utf-8\r\nCache-Control: no-store\r\nContent-Length: " as *u8)
104 h = zs_dec(hdr, h, bn)
105 h = zs_apps(hdr, h, "\r\nConnection: close\r\n\r\n" as *u8)
106 sys_write(cfd, hdr, h)
107 if bn > 0 { sys_write(cfd, body, bn) }
108 sys_close(cfd)
109 return 0
110}
111
112// fork+exec the page-prep elf (mirrors ms_thumb_run / ms_run_extractor). Returns child exit (0 = ok, 127 = absent).
113func zs_prep_run(src: *u8, outdir: *u8, rtl: *u8) -> i64 {
114 let argv: *i64 = sys_mmap(56) as *i64
115 argv[0] = ZS_PREP_ELF as i64
116 argv[1] = src as i64
117 argv[2] = outdir as i64
118 argv[3] = rtl as i64
119 argv[4] = ZS_TILESIZE as i64
120 argv[5] = 0
121 let envp: *i64 = sys_mmap(8) as *i64
122 envp[0] = 0
123 let pid: i64 = sys_fork()
124 if pid == 0 { sys_execve(ZS_PREP_ELF, argv, envp); sys_exit(127) }
125 let st: *i64 = sys_mmap(16) as *i64
126 sys_wait4(pid, st, 0)
127 return (st[0] >> 8) & 0xff
128}
129
130// GET /api/zoomprep?slug=&file=&rtl= -> ensure reader/<slug>/zoom/<stem>/ tiles+panels exist. See header.
131func zs_handle_zoomprep(cfd: i64, p: *u8, plen: i64) -> i64 {
132 let slug: *u8 = sys_mmap(256)
133 zs_qval(p, plen, "slug" as *u8, slug, 256)
134 let file: *u8 = sys_mmap(160)
135 zs_qval(p, plen, "file" as *u8, file, 160)
136 let rtlb: *u8 = sys_mmap(8)
137 zs_qval(p, plen, "rtl" as *u8, rtlb, 8)
138 if rtlb[0] != (48 as u8) { rtlb[0] = 49 as u8; rtlb[1] = 0 as u8 } // anything but "0" -> "1"
139
140 if zs_slug_safe(slug) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad slug\"}" as *u8) }
141 if zs_fname_safe(file) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad file\"}" as *u8) }
142
143 // src = reader/<slug>/<file> -- must already exist (the extracted comic page)
144 let src: *u8 = sys_mmap(1024)
145 var so: i64 = zs_apps(src, 0, ZS_ROOT)
146 so = zs_apps(src, so, slug)
147 src[so] = 47 as u8; so = so + 1
148 so = zs_apps(src, so, file)
149 src[so] = 0 as u8
150 if zs_exists(src) == 0 { return zs_emit(cfd, "404 Not Found" as *u8, "{\"ok\":0,\"err\":\"no such page\"}" as *u8) }
151
152 // stem = file minus its last .ext
153 let stem: *u8 = sys_mmap(160)
154 var fn: i64 = zs_slen(file)
155 var dot: i64 = fn
156 var di: i64 = 0
157 while di < fn { if file[di] == (46 as u8) { dot = di } di = di + 1 }
158 var si: i64 = 0
159 while si < dot { stem[si] = file[si]; si = si + 1 }
160 stem[dot] = 0 as u8
161
162 // zoomdir parent + outdir = reader/<slug>/zoom/<stem>
163 let zdir: *u8 = sys_mmap(1024)
164 var zo: i64 = zs_apps(zdir, 0, ZS_ROOT)
165 zo = zs_apps(zdir, zo, slug)
166 zo = zs_apps(zdir, zo, "/zoom" as *u8)
167 zdir[zo] = 0 as u8
168 let outdir: *u8 = sys_mmap(1024)
169 var oo: i64 = zs_apps(outdir, 0, zdir)
170 outdir[oo] = 47 as u8; oo = oo + 1
171 oo = zs_apps(outdir, oo, stem)
172 outdir[oo] = 0 as u8
173
174 // cache-first: dz.json already there -> done (idempotent, O(1))
175 let dz: *u8 = sys_mmap(1024)
176 var dzo: i64 = zs_apps(dz, 0, outdir)
177 dzo = zs_apps(dz, dzo, "/dz.json" as *u8)
178 dz[dzo] = 0 as u8
179 if zs_exists(dz) == 1 { return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":1,\"cached\":1}" as *u8) }
180
181 zs_mkdir(zdir) // parent (idempotent; the prep elf mkdirs the leaf)
182 let rc: i64 = zs_prep_run(src, outdir, rtlb)
183 // truth = dz.json ON DISK (exit code alone is never trusted -- no fake green)
184 if zs_exists(dz) == 1 { return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":1,\"cached\":0}" as *u8) }
185 let eb: *u8 = sys_mmap(128)
186 var eo: i64 = zs_apps(eb, 0, "{\"ok\":0,\"err\":\"prep failed\",\"rc\":" as *u8)
187 eo = zs_dec(eb, eo, rc)
188 eo = zs_apps(eb, eo, "}" as *u8)
189 eb[eo] = 0 as u8
190 return zs_emit(cfd, "500 Internal Server Error" as *u8, eb)
191}