code wiki / _hdl_build / nx_reader_library.nx
nx_reader_library.nx source
↩ module page · 195 lines · 10357 B
1// nx_reader_library.nx -- R1 of the shared/personal LIBRARY arc (operator 2026-07-02: "the library should have a
2// shared area and a personal area so what i download or brad or kelli or jensen can have their privacy but i can
3// keep the family books in the main library"). Two tiers, both behind THE WALL, keyed by the AUTHENTICATED user
4// (sy_user_key from the auth handle -- identity is the auth layer's, never a client claim):
5// SHARED = the family library (existing idx_book.wsl over the NAS + extracted comics at reader/<slug>/, slugs
6// that do NOT start "u_"). Every authed user sees it. The owner PROMOTES a personal book here.
7// PERSONAL = a per-user PRIVATE book whose SLUG ITSELF carries the owner: "u_<ukey>_<name>", stored at
8// reader/u_<ukey>_<name>/. Because every reader route (open/zoomprep/book/pos/ann) keys off a single
9// slug, encoding the owner IN the slug means ALL of them work unchanged + one guard: a "u_"-prefixed
10// slug is reachable ONLY by the user whose ukey it carries. TRUE isolation BY CONSTRUCTION -- the slug
11// is the capability. Each user has a private index idx_book_personal_<ukey>.wsl (rows = full slugs).
12// LIB (fail-closed helpers + the isolation predicate); nx_reader_library_gate proves the isolation. Reuses zs_*.
13// license_tier: ORIGINAL
14import "nx_syscalls.nx"
15import "nx_reader_zoomserve.nx" // zs_slug_safe / zs_slen / zs_apps / zs_exists / zs_mkdir / ZS_ROOT
16
17const RL_PIDX_PRE: *u8 = "knowledge/staging/media/idx_book_personal_" // + <ukey> + ".wsl"
18
19func rl_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
20
21// the personal SLUG for (ukey, name): "u_<ukey>_<name>" -- a single safe path component (ukey + name are
22// [a-z0-9_-], so the whole slug passes zs_slug_safe and works with every slug-based route).
23func rl_personal_slug(ukey: *u8, name: *u8, out: *u8) -> i64 {
24 out[0] = 117 as u8; out[1] = 95 as u8 // "u_"
25 var o: i64 = 2
26 o = zs_apps(out, o, ukey)
27 out[o] = 95 as u8; o = o + 1
28 o = zs_apps(out, o, name)
29 out[o] = 0 as u8
30 return o
31}
32
33// reader/<personal-slug> (the on-disk book dir for a personal book)
34func rl_personal_slug_dir(ukey: *u8, name: *u8, out: *u8) -> i64 {
35 var o: i64 = zs_apps(out, 0, ZS_ROOT)
36 o = o + rl_personal_slug(ukey, name, ((out as i64) + o) as *u8)
37 out[o] = 0 as u8
38 return o
39}
40
41func rl_personal_index(ukey: *u8, out: *u8) -> i64 {
42 var o: i64 = zs_apps(out, 0, RL_PIDX_PRE)
43 o = zs_apps(out, o, ukey)
44 o = zs_apps(out, o, ".wsl" as *u8)
45 out[o] = 0 as u8
46 return o
47}
48
49func rl_read(path: *u8, buf: *u8, cap: i64) -> i64 {
50 let fd: i64 = sys_openat_rd(path)
51 if fd < 0 { return 0 - 1 }
52 var tot: i64 = 0; var r: i64 = 1
53 while r > 0 { let dst: *u8 = ((buf as i64) + tot) as *u8; r = sys_read(fd, dst, cap - tot); if r > 0 { tot = tot + r } }
54 sys_close(fd)
55 return tot
56}
57
58// ★ THE ISOLATION PREDICATE (slug form). Returns 1 if the REQUESTER (req_ukey) may access book `slug`, else 0.
59// shared (slug does NOT start "u_") -> allowed for everyone.
60// personal(slug starts "u_") -> allowed ONLY if slug == "u_<req_ukey>_..." (whole ukey
61// component followed by '_'). User B cannot name user A's slug.
62func rl_slug_guard(req_ukey: *u8, slug: *u8) -> i64 {
63 let sn: i64 = rl_slen(slug)
64 if sn < 1 { return 0 }
65 // shared?
66 if sn < 2 { return 1 }
67 if slug[0] != (117 as u8) { return 1 } // not 'u'
68 if slug[1] != (95 as u8) { return 1 } // not "u_"
69 // personal: require "u_<req_ukey>_"
70 let kn: i64 = rl_slen(req_ukey)
71 if sn < 2 + kn + 1 { return 0 }
72 var j: i64 = 0
73 while j < kn { if slug[2 + j] != req_ukey[j] { return 0 } j = j + 1 }
74 if slug[2 + kn] != (95 as u8) { return 0 } // ukey must be a WHOLE component (followed by '_')
75 return 1
76}
77
78// path form: rel is "reader/<slug>/..." (what /api/book passes). Reject "../", derive the slug, apply the guard.
79func rl_may_access(req_ukey: *u8, rel: *u8) -> i64 {
80 let rn: i64 = rl_slen(rel)
81 var t: i64 = 0
82 while t + 1 < rn { if rel[t] == (46 as u8) { if rel[t+1] == (46 as u8) { return 0 } } t = t + 1 }
83 let pre: *u8 = "reader/" as *u8
84 if rn < 7 { return 0 }
85 var i: i64 = 0
86 while i < 7 { if rel[i] != pre[i] { return 0 } i = i + 1 }
87 // slug = component after "reader/"
88 let slug: *u8 = sys_mmap(256); var sl: i64 = 0
89 var k: i64 = 7
90 while k < rn { if rel[k] == (47 as u8) { k = rn } else { if sl < 200 { slug[sl] = rel[k]; sl = sl + 1 } k = k + 1 } }
91 slug[sl] = 0 as u8
92 return rl_slug_guard(req_ukey, slug)
93}
94
95// idempotently register a personal book in the user's private index: "<fullslug>\t<title>\n". Ensures the dir.
96// name = the human/base name (NOT the full slug). Returns 1 added, 0 already present, -1 bad name.
97func rl_add_personal(ukey: *u8, name: *u8, title: *u8, title_n: i64) -> i64 {
98 let slug: *u8 = sys_mmap(256); rl_personal_slug(ukey, name, slug)
99 if zs_slug_safe(slug) == 0 { return 0 - 1 }
100 let dd: *u8 = sys_mmap(1024); rl_personal_slug_dir(ukey, name, dd); zs_mkdir(dd)
101 let ip: *u8 = sys_mmap(1024); rl_personal_index(ukey, ip)
102 let sn: i64 = rl_slen(slug)
103 let buf: *u8 = sys_mmap(1048576)
104 let n: i64 = rl_read(ip, buf, 1048576)
105 if n > 0 {
106 var i: i64 = 0; var ls: i64 = 0
107 while i <= n {
108 var nl: i64 = 0; if i == n { nl = 1 } else { if buf[i] == (10 as u8) { nl = 1 } }
109 if nl == 1 {
110 var tb: i64 = 0 - 1; var kk: i64 = ls
111 while kk < i { if buf[kk] == (9 as u8) { if tb < 0 { tb = kk } } kk = kk + 1 }
112 var flen: i64 = i - ls
113 if tb >= 0 { flen = tb - ls }
114 if flen == sn { var eq: i64 = 1; var c: i64 = 0; while c < sn { if buf[ls + c] != slug[c] { eq = 0 } c = c + 1 } if eq == 1 { return 0 } }
115 ls = i + 1
116 }
117 i = i + 1
118 }
119 }
120 let fd: i64 = sys_openat_append(ip, 0x1a4)
121 if fd < 0 { return 0 - 1 }
122 sys_write(fd, slug, sn)
123 sys_write(fd, "\t" as *u8, 1)
124 if title_n > 0 { sys_write(fd, title, title_n) } else { sys_write(fd, name, rl_slen(name)) }
125 sys_write(fd, "\n" as *u8, 1)
126 sys_close(fd)
127 return 1
128}
129
130// {"kind":"book","area":"personal","items":[{"slug":"u_..","n":"title"},...]} from the user's private index.
131func rl_list_personal_json(ukey: *u8, out: *u8, cap: i64) -> i64 {
132 let ip: *u8 = sys_mmap(1024); rl_personal_index(ukey, ip)
133 let buf: *u8 = sys_mmap(1048576)
134 let n: i64 = rl_read(ip, buf, 1048576)
135 var o: i64 = zs_apps(out, 0, "{\"kind\":\"book\",\"area\":\"personal\",\"items\":[" as *u8)
136 var first: i64 = 1
137 if n > 0 {
138 var i: i64 = 0; var ls: i64 = 0
139 while i <= n {
140 var nl: i64 = 0; if i == n { nl = 1 } else { if buf[i] == (10 as u8) { nl = 1 } }
141 if nl == 1 {
142 if i > ls {
143 var tb: i64 = 0 - 1; var kk: i64 = ls
144 while kk < i { if buf[kk] == (9 as u8) { if tb < 0 { tb = kk } } kk = kk + 1 }
145 var se: i64 = i; if tb >= 0 { se = tb }
146 let slug: *u8 = sys_mmap(256); var sl: i64 = 0
147 var c: i64 = ls; while c < se { if sl < 200 { slug[sl] = buf[c]; sl = sl + 1 } c = c + 1 }
148 slug[sl] = 0 as u8
149 if zs_slug_safe(slug) == 1 { if rl_slug_guard(ukey, slug) == 1 {
150 if first == 0 { out[o] = 44 as u8; o = o + 1 }
151 first = 0
152 o = zs_apps(out, o, "{\"slug\":\"" as *u8)
153 var q: i64 = 0; while q < sl { out[o] = slug[q]; o = o + 1; q = q + 1 }
154 o = zs_apps(out, o, "\",\"n\":\"" as *u8)
155 if tb >= 0 { var tc: i64 = tb + 1; while tc < i { let ch: i64 = buf[tc] as i64; if ch >= 32 { if ch != 34 { if ch != 92 { out[o] = buf[tc]; o = o + 1 } } } tc = tc + 1 } }
156 else { var qq: i64 = 0; while qq < sl { out[o] = slug[qq]; o = o + 1; qq = qq + 1 } }
157 o = zs_apps(out, o, "\"}" as *u8)
158 } }
159 }
160 ls = i + 1
161 }
162 i = i + 1
163 }
164 }
165 o = zs_apps(out, o, "]}" as *u8)
166 out[o] = 0 as u8
167 return o
168}
169
170// PROMOTE a personal book to SHARED (owner action): full_slug "u_<ukey>_<name>" -> reader/<name>, append <name>
171// to idx_book_shared.wsl. Returns 1 ok, 0 source missing, -1 bad slug / not-owned / dest exists (never clobber).
172func rl_promote_to_shared(req_ukey: *u8, full_slug: *u8) -> i64 {
173 if zs_slug_safe(full_slug) == 0 { return 0 - 1 }
174 if rl_slug_guard(req_ukey, full_slug) == 0 { return 0 - 1 } // must be the requester's own personal slug
175 let fn: i64 = rl_slen(full_slug)
176 // name = full_slug minus the "u_<ukey>_" prefix
177 let kn: i64 = rl_slen(req_ukey)
178 let pfx: i64 = 2 + kn + 1
179 if fn <= pfx { return 0 - 1 }
180 let name: *u8 = sys_mmap(256); var ni: i64 = 0
181 var i: i64 = pfx; while i < fn { name[ni] = full_slug[i]; ni = ni + 1; i = i + 1 } name[ni] = 0 as u8
182 if zs_slug_safe(name) == 0 { return 0 - 1 }
183 let src: *u8 = sys_mmap(1024); var so: i64 = zs_apps(src, 0, ZS_ROOT); so = zs_apps(src, so, full_slug); src[so] = 0 as u8
184 let dst: *u8 = sys_mmap(1024); var doo: i64 = zs_apps(dst, 0, ZS_ROOT); doo = zs_apps(dst, doo, name); dst[doo] = 0 as u8
185 let dprobe: *u8 = sys_mmap(1024); var dpo: i64 = zs_apps(dprobe, 0, dst); dpo = zs_apps(dprobe, dpo, "/cbx.json" as *u8); dprobe[dpo] = 0 as u8
186 if zs_exists(dprobe) == 1 { return 0 - 1 }
187 let sprobe: *u8 = sys_mmap(1024); var spo: i64 = zs_apps(sprobe, 0, src); spo = zs_apps(sprobe, spo, "/cbx.json" as *u8); sprobe[spo] = 0 as u8
188 var have: i64 = zs_exists(sprobe)
189 if have == 0 { spo = zs_apps(sprobe, 0, src); spo = zs_apps(sprobe, spo, "/book.json" as *u8); sprobe[spo] = 0 as u8; have = zs_exists(sprobe) }
190 if have == 0 { return 0 }
191 if sys_renameat(src, dst) != 0 { return 0 }
192 let fd: i64 = sys_openat_append("knowledge/staging/media/idx_book_shared.wsl" as *u8, 0x1a4)
193 if fd >= 0 { sys_write(fd, name, rl_slen(name)); sys_write(fd, "\n" as *u8, 1); sys_close(fd) }
194 return 1
195}