code wiki / _hdl_build / nx_manga_add.nx
nx_manga_add.nx source
↩ module page · 213 lines · 12185 B
1// nx_manga_add.nx -- the reader daemon's "Add manga" endpoint (lib; lib+gate split). POST /api/manga/add with a
2// url-encoded body `url=<page>&title=<name>` -> derive a safe slug from the title (or a hash of the url) -> fork
3// ./_offc/nx_manga_get.elf <url> <slug> <ukey> <title> so the download lands in the CALLER's PRIVATE area
4// (reader/u_<ukey>/<slug>). Returns {"ok":1,"slug":..,"pages":N} on success (decided by cbx.json on disk, never
5// exit code alone). The heavy download runs in the fork (its TLS mmaps reclaimed on exit); the handler waits and
6// reports. FAIL-CLOSED: url must be http(s) and printable; title/slug sanitized; body cap. Reuses zs_*/rl_* (DRY).
7// license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_reader_zoomserve.nx" // zs_slen / zs_apps / zs_dec / zs_emit / zs_exists / zs_mkdir / ZS_ROOT
10import "nx_reader_library.nx" // rl_personal_slug_dir / rl_ukey_dir / rl_add_personal
11const MA_MAGIC_2000: i64 = 2000
12const MA_MAGIC_16777619: i64 = 16777619
13const MA_MAGIC_4096: i64 = 4096
14const MA_MAGIC_1024: i64 = 1024
15
16const MA_GET_ELF: *u8 = "./_offc/nx_manga_get.elf"
17const MA_IMPORT_ELF: *u8 = "./_offc/nx_media_import.elf" // the POLITE general importer (robots-honored)
18const MA_BODY_CAP: i64 = 4096
19
20// url-decode + extract a form field value (application/x-www-form-urlencoded) into out (NUL-term). Returns len.
21// Stops at '&'. Decodes %XX and '+'.
22func ma_form_val(body: *u8, blen: i64, key: *u8, out: *u8, cap: i64) -> i64 {
23 let kl: i64 = zs_slen(key)
24 var i: i64 = 0
25 while i + kl + 1 <= blen {
26 // match key at a field boundary: i==0 or body[i-1]=='&'
27 var boundary: i64 = 0
28 if i == 0 { boundary = 1 } else { if body[i-1] == (38 as u8) { boundary = 1 } }
29 if boundary == 1 {
30 var hit: i64 = 1
31 var j: i64 = 0
32 while j < kl { if body[i+j] != key[j] { hit = 0; j = kl } else { j = j + 1 } }
33 if hit == 1 { if body[i+kl] == (61 as u8) {
34 var s: i64 = i + kl + 1
35 var o: i64 = 0
36 while s < blen {
37 let c: i64 = body[s] as i64
38 if c == 38 { s = blen } else {
39 if c == 37 { if s + 2 < blen {
40 let h1: i64 = body[s+1] as i64
41 let h2: i64 = body[s+2] as i64
42 var v1: i64 = h1 - 48; if h1 >= 97 { v1 = h1 - 87 } else { if h1 >= 65 { v1 = h1 - 55 } }
43 var v2: i64 = h2 - 48; if h2 >= 97 { v2 = h2 - 87 } else { if h2 >= 65 { v2 = h2 - 55 } }
44 if o < cap - 1 { out[o] = ((v1 * 16) + v2) as u8; o = o + 1 }
45 s = s + 3
46 } else { s = blen } }
47 else { if c == 43 { if o < cap - 1 { out[o] = 32 as u8; o = o + 1 } s = s + 1 } else { if o < cap - 1 { out[o] = body[s]; o = o + 1 } s = s + 1 } }
48 }
49 }
50 out[o] = 0 as u8
51 return o
52 } }
53 }
54 i = i + 1
55 }
56 out[0] = 0 as u8
57 return 0
58}
59
60// 1 if url starts http:// or https:// AND is all printable non-space (a sane fetch target). else 0.
61func ma_url_ok(url: *u8, n: i64) -> i64 {
62 if n < 8 { return 0 }
63 if n > MA_MAGIC_2000 { return 0 }
64 // https:// or http://
65 let h: *u8 = "http" as *u8
66 var i: i64 = 0
67 while i < 4 { if url[i] != h[i] { return 0 } i = i + 1 }
68 var ok_scheme: i64 = 0
69 if url[4] == (115 as u8) { if url[5]==(58 as u8) { if url[6]==(47 as u8) { if url[7]==(47 as u8) { ok_scheme = 1 } } } } // https://
70 if url[4] == (58 as u8) { if url[5]==(47 as u8) { if url[6]==(47 as u8) { ok_scheme = 1 } } } // http://
71 if ok_scheme == 0 { return 0 }
72 var k: i64 = 0
73 while k < n { let c: i64 = url[k] as i64; if c <= 32 { return 0 } if c == 34 { return 0 } if c == 39 { return 0 } if c == 96 { return 0 } k = k + 1 }
74 return 1
75}
76
77// derive a safe slug ([a-z0-9_-], <=48) from the title; append an 8-hex fnv of the url for uniqueness.
78func ma_derive_slug(title: *u8, tn: i64, url: *u8, un: i64, out: *u8) -> i64 {
79 var h: i64 = 0x811c9dc5
80 var i: i64 = 0
81 while i < un { h = ((h ^ ((url[i] as i64) & 0xff)) * MA_MAGIC_16777619) & 0xffffffff; i = i + 1 }
82 var o: i64 = 0
83 i = 0
84 while i < tn { if o < 40 {
85 var c: i64 = title[i] as i64
86 if c >= 65 { if c <= 90 { c = c + 32 } }
87 var okc: i64 = 0
88 if c >= 48 { if c <= 57 { okc = 1 } }
89 if c >= 97 { if c <= 122 { okc = 1 } }
90 if c == 95 { okc = 1 }
91 if c == 45 { okc = 1 }
92 if okc == 0 { c = 95 }
93 // collapse: skip a leading '_'
94 if o == 0 { if c == 95 { c = 0 } }
95 if c != 0 { out[o] = c as u8; o = o + 1 }
96 } i = i + 1 }
97 if o == 0 { out[o] = 109 as u8; o = o + 1 } // 'm'
98 out[o] = 95 as u8; o = o + 1
99 let hx: *u8 = "0123456789abcdef" as *u8
100 var b: i64 = 28
101 while b >= 0 { out[o] = hx[(h >> b) & 0xf]; o = o + 1; b = b - 4 }
102 out[o] = 0 as u8
103 return o
104}
105
106// ASYNC (Mihon model): fork the worker DETACHED (double-fork -> reparented to init) so the download runs in the
107// BACKGROUND. The daemon connection returns IMMEDIATELY instead of blocking ~30-60s (which timed out the browser
108// POST and made a WORKING download look failed -- operator 2026-07-03, proven server-side by nx_reader_diag). The
109// book appears in /api/personal when the worker finishes; the UI polls to reveal it. title==0 => import (3 args).
110func ma_fork_detached(elf: *u8, url: *u8, slug: *u8, ukey: *u8, title: *u8) -> i64 {
111 let pid1: i64 = sys_fork()
112 if pid1 == 0 {
113 __syscall(112, 0, 0, 0, 0, 0, 0) // setsid: new session, detach from the daemon connection
114 let pid2: i64 = sys_fork()
115 if pid2 == 0 {
116 let argv: *i64 = sys_mmap(64) as *i64
117 argv[0] = elf as i64; argv[1] = url as i64; argv[2] = slug as i64; argv[3] = ukey as i64
118 var ai: i64 = 4
119 if (title as i64) != 0 { argv[4] = title as i64; ai = 5 }
120 argv[ai] = 0
121 let envp: *i64 = sys_mmap(8) as *i64; envp[0] = 0
122 sys_execve(elf, argv, envp); sys_exit(127)
123 }
124 sys_exit(0) // intermediate exits -> the grandchild (worker) is orphaned -> init adopts + reaps it (no zombie)
125 }
126 let st: *i64 = sys_mmap(16) as *i64
127 sys_wait4(pid1, st, 0) // reap the intermediate (exits instantly) -> returns FAST
128 return 0
129}
130func ma_run_get(url: *u8, slug: *u8, ukey: *u8, title: *u8) -> i64 { return ma_fork_detached(MA_GET_ELF, url, slug, ukey, title) }
131
132// ASYNC detached import (robots-honored). Runs in the background; the book appears in /api/personal when done.
133func ma_run_import(url: *u8, slug: *u8, ukey: *u8) -> i64 { return ma_fork_detached(MA_IMPORT_ELF, url, slug, ukey, 0 as *u8) }
134
135// POST /api/import body=url=..&title=.. -> POLITELY import images (honors robots.txt) into the caller's private area.
136// A robots.txt Disallow returns {"ok":0,"refused":"robots.txt"} (200) -- an HONEST refusal, not a failure.
137func ma_handle_import(cfd: i64, req: *u8, body_off: i64, body_len: i64, ukey: *u8) -> i64 {
138 if body_len <= 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"no body\"}" as *u8) }
139 var bl: i64 = body_len
140 if bl > MA_BODY_CAP { bl = MA_BODY_CAP }
141 let body: *u8 = ((req as i64) + body_off) as *u8
142 let url: *u8 = sys_mmap(MA_MAGIC_4096)
143 ma_form_val(body, bl, "url" as *u8, url, MA_MAGIC_4096)
144 if ma_url_ok(url, zs_slen(url)) == 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"bad url (need http/https)\"}" as *u8) }
145 let title: *u8 = sys_mmap(512)
146 let tn: i64 = ma_form_val(body, bl, "title" as *u8, title, 512)
147 if tn == 0 { title[0] = 105 as u8; title[1] = 109 as u8; title[2] = 103 as u8; title[3] = 0 as u8 } // "img"
148 let slug: *u8 = sys_mmap(128)
149 ma_derive_slug(title, zs_slen(title), url, zs_slen(url), slug)
150
151 ma_run_import(url, slug, ukey) // DETACHED background import (robots-honored in the worker) -> returns instantly
152 let ok: *u8 = sys_mmap(256)
153 var oo: i64 = zs_apps(ok, 0, "{\"ok\":1,\"status\":\"downloading\",\"slug\":\"" as *u8)
154 oo = zs_apps(ok, oo, slug)
155 oo = zs_apps(ok, oo, "\",\"area\":\"personal\"}" as *u8)
156 ok[oo] = 0 as u8
157 return zs_emit(cfd, "200 OK" as *u8, ok)
158}
159
160// POST /api/manga/add body=url=..&title=.. -> download into reader/u_<ukey>/<slug>/. See header.
161func ma_handle_add(cfd: i64, req: *u8, body_off: i64, body_len: i64, ukey: *u8) -> i64 {
162 if body_len <= 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"no body\"}" as *u8) }
163 var bl: i64 = body_len
164 if bl > MA_BODY_CAP { bl = MA_BODY_CAP }
165 let body: *u8 = ((req as i64) + body_off) as *u8
166 let url: *u8 = sys_mmap(MA_MAGIC_4096)
167 ma_form_val(body, bl, "url" as *u8, url, MA_MAGIC_4096)
168 if ma_url_ok(url, zs_slen(url)) == 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"bad url (need http/https)\"}" as *u8) }
169 let title: *u8 = sys_mmap(512)
170 let tn: i64 = ma_form_val(body, bl, "title" as *u8, title, 512)
171 if tn == 0 { title[0] = 109 as u8; title[1] = 97 as u8; title[2] = 110 as u8; title[3] = 103 as u8; title[4] = 97 as u8; title[5] = 0 as u8 } // "manga"
172 let slug: *u8 = sys_mmap(128)
173 ma_derive_slug(title, zs_slen(title), url, zs_slen(url), slug)
174
175 ma_run_get(url, slug, ukey, title) // DETACHED background download -> returns instantly (no 60s block)
176 // ASYNC: the worker writes pages + registers the book; the UI polls /api/personal to reveal it when done.
177 let ok: *u8 = sys_mmap(256)
178 var oo: i64 = zs_apps(ok, 0, "{\"ok\":1,\"status\":\"downloading\",\"slug\":\"" as *u8)
179 oo = zs_apps(ok, oo, slug)
180 oo = zs_apps(ok, oo, "\",\"area\":\"personal\"}" as *u8)
181 ok[oo] = 0 as u8
182 return zs_emit(cfd, "200 OK" as *u8, ok)
183}
184
185// ---- GET /api/dlstatus?slug= -> live download progress for THIS user's in-flight book (operator: modern UI "progress").
186// Reads the worker's <bookdir>/dl.json {total,done}; cbx.json present => finished. Owner-scoped by ukey (no cross-user).
187func ma_dlfind(buf: *u8, n: i64, key: *u8) -> i64 {
188 var kl: i64 = 0; while key[kl] != (0 as u8) { kl = kl + 1 }
189 var i: i64 = 0
190 while i + kl <= n { var j: i64 = 0; while j < kl { if buf[i+j] != key[j] { j = kl + 9 } else { j = j + 1 } } if j == kl { return i + kl } i = i + 1 }
191 return 0 - 1
192}
193func ma_dlint(buf: *u8, n: i64, key: *u8) -> i64 {
194 let p: i64 = ma_dlfind(buf, n, key); if p < 0 { return 0 }
195 var v: i64 = 0; var i: i64 = p
196 while i < n { let c: i64 = buf[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); i = i + 1 } else { i = n } } else { i = n } }
197 return v
198}
199func ma_handle_dlstatus(cfd: i64, path: *u8, plen: i64, ukey: *u8) -> i64 {
200 let slug: *u8 = sys_mmap(128); let sl: i64 = zs_qval(path, plen, "slug" as *u8, slug, 128)
201 if sl <= 0 { return zs_emit(cfd, "200 OK" as *u8, "{\"total\":0,\"done\":0,\"finished\":0}" as *u8) }
202 let dir: *u8 = sys_mmap(MA_MAGIC_1024); rl_personal_slug_dir(ukey, slug, dir)
203 let cbx: *u8 = sys_mmap(MA_MAGIC_1024); var o: i64 = zs_apps(cbx, 0, dir); o = zs_apps(cbx, o, "/cbx.json" as *u8); cbx[o] = 0 as u8
204 var finished: i64 = 0; if zs_exists(cbx) == 1 { finished = 1 }
205 let dlp: *u8 = sys_mmap(MA_MAGIC_1024); var o2: i64 = zs_apps(dlp, 0, dir); o2 = zs_apps(dlp, o2, "/dl.json" as *u8); dlp[o2] = 0 as u8
206 var total: i64 = 0; var done: i64 = 0
207 let fd: i64 = sys_openat_rd(dlp)
208 if fd >= 0 { let db: *u8 = sys_mmap(256); let dn: i64 = sys_read(fd, db, 255); sys_close(fd); if dn > 0 { total = ma_dlint(db, dn, "\"total\":" as *u8); done = ma_dlint(db, dn, "\"done\":" as *u8) } }
209 let out: *u8 = sys_mmap(128); var oo: i64 = zs_apps(out, 0, "{\"total\":" as *u8)
210 oo = zs_dec(out, oo, total); oo = zs_apps(out, oo, ",\"done\":" as *u8); oo = zs_dec(out, oo, done)
211 oo = zs_apps(out, oo, ",\"finished\":" as *u8); oo = zs_dec(out, oo, finished); oo = zs_apps(out, oo, "}" as *u8); out[oo] = 0 as u8
212 return zs_emit(cfd, "200 OK" as *u8, out)
213}