code wiki / _hdl_build / nx_reader_sync.nx

nx_reader_sync.nx source

↩ module page · 250 lines · 12655 B

1// nx_reader_sync.nx -- R1 SYNC SPINE (beat-Kindle ladder #1; census cells SC01/SC37): per-USER cross-device 2// reading position for the library, behind THE WALL. Whispersync-class resume with a sovereign twist: 3// TEXT books -> AUTO-position: the daemon already serves every chapter fetch behind the wall, so recording 4// "last chapter opened" per (user, book) costs ZERO client changes -- the zero-JS text reader 5// stays zero-JS (census SC59 preserved) and still syncs across devices. 6// COMICS/scans -> EXPLICIT position: the zoom viewer (authed JS by design) POSTs {"page":N} and GETs it back 7// on open, so a phone resumes exactly where the desktop left off. 8// Storage: reader/<slug>/pos_<ukey>.json (client JSON stored VERBATIM, atomic tmp+rename, last-write-wins) + 9// reader/<slug>/auto_<ukey>.txt ("<chapfile>\t<epoch>"). <ukey> = sanitized handle + fnv32-hex8 of the RAW handle 10// (deterministic per user, collision-safe after sanitize, path-safe by construction). The user handle comes from 11// nx_sa_validate_handle at the wall -- identity is the AUTH layer's, never a client claim. 12// FAIL-CLOSED: slug via zs_slug_safe -> 403; body cap 2048 -> 413; auto-record NEVER breaks the book serve 13// (any doubt -> silent skip). Lib (no main); nx_reader_sync_gate drives the handlers in-process. 14// license_tier: ORIGINAL 15import "nx_syscalls.nx" 16import "nx_reader_zoomserve.nx" // DRY: zs_qval / zs_slug_safe / zs_fname_safe / zs_slen / zs_apps / zs_dec / zs_exists / zs_mkdir / zs_emit + ZS_ROOT 17const SY_MAGIC_16777619: i64 = 16777619 18const SY_MAGIC_1152: i64 = 1152 19 20const SY_BODY_CAP: i64 = 2048 21const SY_ANN_CAP: i64 = 65536 // annotations = the user's whole list as one verbatim JSON doc (LWW, atomic) 22 23// ukey = up to 24 sanitized handle chars ([a-z0-9_-], others -> '_', uppercase folded) + '_' + fnv1a32 hex8 of 24// the RAW handle bytes. Deterministic, path-safe, distinct for handles that collide after sanitizing. 25func sy_user_key(handle: *u8, hn: i64, out: *u8) -> i64 { 26 var h: i64 = 0x811c9dc5 27 var i: i64 = 0 28 while i < hn { h = ((h ^ ((handle[i] as i64) & 0xff)) * SY_MAGIC_16777619) & 0xffffffff; i = i + 1 } 29 var o: i64 = 0 30 i = 0 31 while i < hn { 32 if o < 24 { 33 var c: i64 = handle[i] as i64 34 if c >= 65 { if c <= 90 { c = c + 32 } } 35 var ok: i64 = 0 36 if c >= 48 { if c <= 57 { ok = 1 } } 37 if c >= 97 { if c <= 122 { ok = 1 } } 38 if c == 95 { ok = 1 } 39 if c == 45 { ok = 1 } 40 if ok == 0 { c = 95 } 41 out[o] = c as u8; o = o + 1 42 } 43 i = i + 1 44 } 45 if o == 0 { out[0] = 97 as u8; o = 1 } // 'a' -- empty handle still yields a key (hash disambiguates) 46 out[o] = 95 as u8; o = o + 1 47 let hx: *u8 = "0123456789abcdef" as *u8 48 var k: i64 = 28 49 while k >= 0 { out[o] = hx[(h >> k) & 0xf]; o = o + 1; k = k - 4 } 50 out[o] = 0 as u8 51 return o 52} 53 54// atomic small-file write: <path>.new -> rename -> path 55func sy_write_atomic(path: *u8, data: *u8, n: i64) -> i64 { 56 let tmp: *u8 = sys_mmap(SY_MAGIC_1152) 57 var o: i64 = zs_apps(tmp, 0, path) 58 o = zs_apps(tmp, o, ".new" as *u8) 59 tmp[o] = 0 as u8 60 let fd: i64 = sys_openat_wr(tmp, 0x1a4) 61 if fd < 0 { return 0 - 1 } 62 sys_write(fd, data, n) 63 sys_close(fd) 64 return sys_renameat(tmp, path) 65} 66 67func sy_read_small(path: *u8, buf: *u8, cap: i64) -> i64 { 68 let fd: i64 = sys_openat_rd(path) 69 if fd < 0 { return 0 - 1 } 70 var tot: i64 = 0; var r: i64 = 1 71 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 } } 72 sys_close(fd) 73 return tot 74} 75 76// build reader/<slug>/<kind>_<ukey>.<ext> into out; returns len 77func sy_path(slug: *u8, kind: *u8, ukey: *u8, ext: *u8, out: *u8) -> i64 { 78 var o: i64 = zs_apps(out, 0, ZS_ROOT) 79 o = zs_apps(out, o, slug) 80 out[o] = 47 as u8; o = o + 1 81 o = zs_apps(out, o, kind) 82 out[o] = 95 as u8; o = o + 1 83 o = zs_apps(out, o, ukey) 84 o = zs_apps(out, o, ext) 85 out[o] = 0 as u8 86 return o 87} 88 89// GET /api/pos?slug=<slug> -> {"auto":"<chapfile>","auto_ts":N,"pos":<verbatim client JSON | null>} 90func sy_handle_pos_get(cfd: i64, p: *u8, plen: i64, ukey: *u8) -> i64 { 91 let slug: *u8 = sys_mmap(256) 92 zs_qval(p, plen, "slug" as *u8, slug, 256) 93 if zs_slug_safe(slug) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad slug\"}" as *u8) } 94 95 let body: *u8 = sys_mmap(SY_BODY_CAP + 512) 96 var o: i64 = zs_apps(body, 0, "{\"auto\":\"" as *u8) 97 let ap: *u8 = sys_mmap(SY_MAGIC_1152) 98 sy_path(slug, "auto" as *u8, ukey, ".txt" as *u8, ap) 99 let ab: *u8 = sys_mmap(512) 100 let an: i64 = sy_read_small(ap, ab, 511) 101 var ats: i64 = 0 102 if an > 0 { 103 var i: i64 = 0 104 while i < an { if ab[i] == (9 as u8) { i = an } else { if ab[i] >= (32 as u8) { if ab[i] != (34 as u8) { if ab[i] != (92 as u8) { body[o] = ab[i]; o = o + 1 } } } i = i + 1 } } 105 // parse the epoch after the tab 106 var j: i64 = 0; var seen: i64 = 0 107 while j < an { 108 if seen == 1 { let c: i64 = ab[j] as i64; if c >= 48 { if c <= 57 { ats = ats * 10 + (c - 48) } } } 109 if ab[j] == (9 as u8) { seen = 1 } 110 j = j + 1 111 } 112 } 113 o = zs_apps(body, o, "\",\"auto_ts\":" as *u8) 114 o = zs_dec(body, o, ats) 115 o = zs_apps(body, o, ",\"pos\":" as *u8) 116 let pp: *u8 = sys_mmap(SY_MAGIC_1152) 117 sy_path(slug, "pos" as *u8, ukey, ".json" as *u8, pp) 118 let pb: *u8 = sys_mmap(SY_BODY_CAP + 16) 119 let pn: i64 = sy_read_small(pp, pb, SY_BODY_CAP) 120 if pn > 0 { var k: i64 = 0; while k < pn { body[o] = pb[k]; o = o + 1; k = k + 1 } } else { o = zs_apps(body, o, "null" as *u8) } 121 o = zs_apps(body, o, "}" as *u8) 122 body[o] = 0 as u8 123 return zs_emit(cfd, "200 OK" as *u8, body) 124} 125 126// POST /api/pos?slug=<slug> (body = the client's position JSON, <= SY_BODY_CAP) -> stored verbatim, LWW. 127func sy_handle_pos_put(cfd: i64, p: *u8, plen: i64, req: *u8, body_off: i64, body_len: i64, ukey: *u8) -> i64 { 128 let slug: *u8 = sys_mmap(256) 129 zs_qval(p, plen, "slug" as *u8, slug, 256) 130 if zs_slug_safe(slug) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad slug\"}" as *u8) } 131 if body_len <= 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"no body\"}" as *u8) } 132 if body_len > SY_BODY_CAP { return zs_emit(cfd, "413 Payload Too Large" as *u8, "{\"ok\":0,\"err\":\"pos too large\"}" as *u8) } 133 // the book dir must already exist (an extracted book) -- sync state never creates book dirs 134 let dir: *u8 = sys_mmap(SY_MAGIC_1152) 135 var dl: i64 = zs_apps(dir, 0, ZS_ROOT) 136 dl = zs_apps(dir, dl, slug) 137 dir[dl] = 0 as u8 138 if zs_exists(dir) == 0 { 139 // dir-existence via open on the dir path fails for directories on some kernels; probe cbx.json/book.json instead 140 let probe: *u8 = sys_mmap(SY_MAGIC_1152) 141 var po: i64 = zs_apps(probe, 0, dir) 142 po = zs_apps(probe, po, "/cbx.json" as *u8) 143 probe[po] = 0 as u8 144 var hav: i64 = zs_exists(probe) 145 if hav == 0 { 146 po = zs_apps(probe, 0, dir) 147 po = zs_apps(probe, po, "/book.json" as *u8) 148 probe[po] = 0 as u8 149 hav = zs_exists(probe) 150 } 151 if hav == 0 { return zs_emit(cfd, "404 Not Found" as *u8, "{\"ok\":0,\"err\":\"no such book\"}" as *u8) } 152 } 153 let pp: *u8 = sys_mmap(SY_MAGIC_1152) 154 sy_path(slug, "pos" as *u8, ukey, ".json" as *u8, pp) 155 let src: *u8 = ((req as i64) + body_off) as *u8 156 if sy_write_atomic(pp, src, body_len) != 0 { return zs_emit(cfd, "500 Internal Server Error" as *u8, "{\"ok\":0,\"err\":\"store failed\"}" as *u8) } 157 return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":1}" as *u8) 158} 159 160// R2 ANNOTATIONS (census SC24/25, beat-Kindle ladder #2): per-user highlights/notes for a book, stored VERBATIM 161// as ONE JSON list (client-owned, LWW whole-list, atomic) at reader/<slug>/ann_<ukey>.json. The client puts a 162// CONTENT anchor inside each annotation (e.g. the page-file stem / a text-snippet hash) -- anchors that survive 163// re-conversion are the exceed angle Kindle's positional anchors cannot match. GET doubles as the machine- 164// readable EXPORT (the stored JSON verbatim). Same guards as /api/pos; bigger cap. 165// GET /api/ann?slug=<slug> -> {"ann":<verbatim client JSON | null>} 166func sy_handle_ann_get(cfd: i64, p: *u8, plen: i64, ukey: *u8) -> i64 { 167 let slug: *u8 = sys_mmap(256) 168 zs_qval(p, plen, "slug" as *u8, slug, 256) 169 if zs_slug_safe(slug) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad slug\"}" as *u8) } 170 let body: *u8 = sys_mmap(SY_ANN_CAP + 64) 171 var o: i64 = zs_apps(body, 0, "{\"ann\":" as *u8) 172 let ap: *u8 = sys_mmap(SY_MAGIC_1152) 173 sy_path(slug, "ann" as *u8, ukey, ".json" as *u8, ap) 174 let ab: *u8 = sys_mmap(SY_ANN_CAP + 16) 175 let an: i64 = sy_read_small(ap, ab, SY_ANN_CAP) 176 if an > 0 { var k: i64 = 0; while k < an { body[o] = ab[k]; o = o + 1; k = k + 1 } } else { o = zs_apps(body, o, "null" as *u8) } 177 o = zs_apps(body, o, "}" as *u8) 178 body[o] = 0 as u8 179 return zs_emit(cfd, "200 OK" as *u8, body) 180} 181 182// POST /api/ann?slug=<slug> (body = the user's whole annotation list JSON, <= SY_ANN_CAP) -> stored verbatim, LWW. 183func sy_handle_ann_put(cfd: i64, p: *u8, plen: i64, req: *u8, body_off: i64, body_len: i64, ukey: *u8) -> i64 { 184 let slug: *u8 = sys_mmap(256) 185 zs_qval(p, plen, "slug" as *u8, slug, 256) 186 if zs_slug_safe(slug) == 0 { return zs_emit(cfd, "403 Forbidden" as *u8, "{\"ok\":0,\"err\":\"bad slug\"}" as *u8) } 187 if body_len <= 0 { return zs_emit(cfd, "400 Bad Request" as *u8, "{\"ok\":0,\"err\":\"no body\"}" as *u8) } 188 if body_len > SY_ANN_CAP { return zs_emit(cfd, "413 Payload Too Large" as *u8, "{\"ok\":0,\"err\":\"ann too large\"}" as *u8) } 189 let dir: *u8 = sys_mmap(SY_MAGIC_1152) 190 var dl: i64 = zs_apps(dir, 0, ZS_ROOT) 191 dl = zs_apps(dir, dl, slug) 192 dir[dl] = 0 as u8 193 let probe: *u8 = sys_mmap(SY_MAGIC_1152) 194 var po: i64 = zs_apps(probe, 0, dir) 195 po = zs_apps(probe, po, "/cbx.json" as *u8) 196 probe[po] = 0 as u8 197 var hav: i64 = zs_exists(probe) 198 if hav == 0 { 199 po = zs_apps(probe, 0, dir) 200 po = zs_apps(probe, po, "/book.json" as *u8) 201 probe[po] = 0 as u8 202 hav = zs_exists(probe) 203 } 204 if hav == 0 { return zs_emit(cfd, "404 Not Found" as *u8, "{\"ok\":0,\"err\":\"no such book\"}" as *u8) } 205 let pp: *u8 = sys_mmap(SY_MAGIC_1152) 206 sy_path(slug, "ann" as *u8, ukey, ".json" as *u8, pp) 207 let src: *u8 = ((req as i64) + body_off) as *u8 208 if sy_write_atomic(pp, src, body_len) != 0 { return zs_emit(cfd, "500 Internal Server Error" as *u8, "{\"ok\":0,\"err\":\"store failed\"}" as *u8) } 209 return zs_emit(cfd, "200 OK" as *u8, "{\"ok\":1}" as *u8) 210} 211 212// AUTO-position hook: called (fail-silent) on every authed /api/book fetch. If the path is a chapter fetch 213// (p=reader/<slug>/chap*.txt) record "<chapfile>\t<epoch>" at reader/<slug>/auto_<ukey>.txt (atomic). The 214// zero-JS text reader thus syncs its coarse position across devices with NO client change. 215func sy_record_auto(p: *u8, plen: i64, ukey: *u8, now_s: i64) -> i64 { 216 // extract the p= value (the book-relative path) 217 let rel: *u8 = sys_mmap(512) 218 let rl: i64 = zs_qval(p, plen, "p" as *u8, rel, 512) 219 if rl < 12 { return 0 } 220 // expect "reader/<slug>/<file>" 221 if rel[0] != (114 as u8) { return 0 } // r 222 if rel[6] != (47 as u8) { return 0 } // "reader/" 223 var i: i64 = 7 224 let slug: *u8 = sys_mmap(256) 225 var sl: i64 = 0 226 while i < rl { if rel[i] == (47 as u8) { i = rl } else { if sl < 200 { slug[sl] = rel[i]; sl = sl + 1 } i = i + 1 } } 227 slug[sl] = 0 as u8 228 if zs_slug_safe(slug) == 0 { return 0 } 229 // file = after the slash 230 let file: *u8 = sys_mmap(256) 231 var fl: i64 = 0 232 var j: i64 = 7 + sl + 1 233 while j < rl { if fl < 128 { file[fl] = rel[j]; fl = fl + 1 } j = j + 1 } 234 file[fl] = 0 as u8 235 if zs_fname_safe(file) == 0 { return 0 } 236 // only chapter fetches: "chap" prefix 237 if fl < 5 { return 0 } 238 if file[0] != (99 as u8) { return 0 } 239 if file[1] != (104 as u8) { return 0 } 240 if file[2] != (97 as u8) { return 0 } 241 if file[3] != (112 as u8) { return 0 } 242 let rec: *u8 = sys_mmap(512) 243 var o: i64 = zs_apps(rec, 0, file) 244 rec[o] = 9 as u8; o = o + 1 245 o = zs_dec(rec, o, now_s) 246 let ap: *u8 = sys_mmap(SY_MAGIC_1152) 247 sy_path(slug, "auto" as *u8, ukey, ".txt" as *u8, ap) 248 sy_write_atomic(ap, rec, o) 249 return 1 250}