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}