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}