code wiki / _hdl_build / nx_galx_manga.nx
nx_galx_manga.nx source
↩ module page · 148 lines · 6131 B
1// nx_galx_manga.nx -- MANGA collection + reader lib for the gallery (media-manager -> gallery migration).
2// A manga = an ordered set of page images extracted by nx_cbx_pages into knowledge/staging/media/reader/<slug>/
3// (cbx.json manifest + page000.<ext> ...). This lib is the SOVEREIGN, pure-IO backend the gallery daemon wires
4// three thin routes onto, so it is gate-testable in isolation (no daemon/socket) -- exactly the nx_mgmt_upload
5// lib+gate split. Single responsibility: list staged manga / return a manga's page manifest / resolve+guard a
6// page-file path (path-traversal fail-closed) / idempotently register a slug in the manga index. NO fork here
7// (the daemon owns extraction). Sovereign: nx_syscalls only. license_tier: ORIGINAL
8import "nx_syscalls.nx"
9
10const MG_INDEX: *u8 = "knowledge/status/galx_manga_index.txt"
11const MG_READER: *u8 = "knowledge/staging/media/reader/"
12
13func mg_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
14func mg_cat(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o + i] = s[i]; i = i + 1 } return o + i }
15func mg_utoa(dst: *u8, o: i64, v: i64) -> i64 {
16 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
17 if m == 0 { t[0] = 48 as u8; k = 1 }
18 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
19 var w: i64 = o; var i: i64 = 0
20 while i < k { dst[w] = t[k - 1 - i]; w = w + 1; i = i + 1 }
21 return w
22}
23
24// 1 iff s[0..n) is a SAFE single path component: [A-Za-z0-9._-] only, nonempty, <=128, and contains no ".."
25// (fail-closed path-traversal guard -- the security seam for /mangaraw and /api/manga/pages).
26func mg_name_ok(s: *u8, n: i64) -> i64 {
27 if n <= 0 { return 0 }
28 if n > 128 { return 0 }
29 var i: i64 = 0
30 while i < n {
31 let c: i64 = s[i] as i64
32 var ok: i64 = 0
33 if c >= 48 { if c <= 57 { ok = 1 } }
34 if c >= 65 { if c <= 90 { ok = 1 } }
35 if c >= 97 { if c <= 122 { ok = 1 } }
36 if c == 46 { ok = 1 }
37 if c == 95 { ok = 1 }
38 if c == 45 { ok = 1 }
39 if ok == 0 { return 0 }
40 i = i + 1
41 }
42 var j: i64 = 0
43 while j + 1 < n { if s[j] == (46 as u8) { if s[j + 1] == (46 as u8) { return 0 } } j = j + 1 }
44 return 1
45}
46
47func mg_read(path: *u8, buf: *u8, cap: i64) -> i64 {
48 let fd: i64 = sys_openat_rd(path)
49 if fd < 0 { return 0 - 1 }
50 var tot: i64 = 0; var r: i64 = 1
51 while r > 0 {
52 let dst: *u8 = ((buf as i64) + tot) as *u8
53 r = sys_read(fd, dst, cap - tot)
54 if r > 0 { tot = tot + r }
55 }
56 sys_close(fd)
57 return tot
58}
59
60// idempotently append slug + "\n" to the manga index (skips if the exact line already present). Returns
61// 1 = added, 0 = already there, -1 = bad slug / open fail. The daemon calls this after a successful extract.
62func mg_index_add(slug: *u8) -> i64 {
63 let sn: i64 = mg_slen(slug)
64 if mg_name_ok(slug, sn) == 0 { return 0 - 1 }
65 let buf: *u8 = sys_mmap(1048576)
66 let n: i64 = mg_read(MG_INDEX, buf, 1048576)
67 if n > 0 {
68 var i: i64 = 0; var ls: i64 = 0
69 while i <= n {
70 var nl: i64 = 0; if i == n { nl = 1 } else { if buf[i] == (10 as u8) { nl = 1 } }
71 if nl == 1 {
72 if i - ls == sn {
73 var eq: i64 = 1; var k: i64 = 0
74 while k < sn { if buf[ls + k] != slug[k] { eq = 0 } k = k + 1 }
75 if eq == 1 { return 0 }
76 }
77 ls = i + 1
78 }
79 i = i + 1
80 }
81 }
82 let fd: i64 = sys_openat_append(MG_INDEX, 0x1a4)
83 if fd < 0 { return 0 - 1 }
84 sys_write(fd, slug, sn)
85 sys_write(fd, "\n" as *u8, 1)
86 sys_close(fd)
87 return 1
88}
89
90// build {"kind":"manga","items":["slug",...],"total":N} from the index (skips malformed/unsafe slugs). Returns length.
91func mg_list_json(out: *u8, cap: i64) -> i64 {
92 let buf: *u8 = sys_mmap(1048576)
93 let n: i64 = mg_read(MG_INDEX, buf, 1048576)
94 var o: i64 = mg_cat(out, 0, "{\"kind\":\"manga\",\"items\":[" as *u8)
95 var total: i64 = 0
96 if n > 0 {
97 var i: i64 = 0; var ls: i64 = 0; var first: i64 = 1
98 while i <= n {
99 var nl: i64 = 0; if i == n { nl = 1 } else { if buf[i] == (10 as u8) { nl = 1 } }
100 if nl == 1 {
101 let ln: i64 = i - ls
102 if ln > 0 {
103 if mg_name_ok(((buf as i64) + ls) as *u8, ln) == 1 {
104 if first == 0 { out[o] = 44 as u8; o = o + 1 }
105 first = 0
106 out[o] = 34 as u8; o = o + 1
107 var k: i64 = 0; while k < ln { out[o] = buf[ls + k]; o = o + 1; k = k + 1 }
108 out[o] = 34 as u8; o = o + 1
109 total = total + 1
110 }
111 }
112 ls = i + 1
113 }
114 i = i + 1
115 }
116 }
117 o = mg_cat(out, o, "],\"total\":" as *u8)
118 o = mg_utoa(out, o, total)
119 o = mg_cat(out, o, "}" as *u8)
120 return o
121}
122
123// copy manga <slug>'s cbx.json manifest into out. Returns length, or 0 if the slug is unsafe/missing.
124func mg_pages_json(slug: *u8, out: *u8, cap: i64) -> i64 {
125 let sn: i64 = mg_slen(slug)
126 if mg_name_ok(slug, sn) == 0 { return 0 }
127 let path: *u8 = sys_mmap(1024)
128 var po: i64 = mg_cat(path, 0, MG_READER)
129 po = mg_cat(path, po, slug)
130 po = mg_cat(path, po, "/cbx.json" as *u8)
131 path[po] = 0 as u8
132 let n: i64 = mg_read(path, out, cap)
133 if n < 0 { return 0 }
134 return n
135}
136
137// resolve a page file to its on-disk path with fail-closed guards. 1 = out is a safe path under the reader dir,
138// 0 = slug/file unsafe (traversal rejected). Existence is checked by the caller (serve).
139func mg_page_path(slug: *u8, file: *u8, out: *u8) -> i64 {
140 let sn: i64 = mg_slen(slug); if mg_name_ok(slug, sn) == 0 { return 0 }
141 let fnn: i64 = mg_slen(file); if mg_name_ok(file, fnn) == 0 { return 0 }
142 var po: i64 = mg_cat(out, 0, MG_READER)
143 po = mg_cat(out, po, slug)
144 out[po] = 47 as u8; po = po + 1
145 po = mg_cat(out, po, file)
146 out[po] = 0 as u8
147 return 1
148}