code wiki / wiki / nx_wiki_archive_router.nx

nx_wiki_archive_router.nx source

↩ module page · 450 lines · 18467 B

1// nx_wiki_archive_router.nx -- /wiki/archive/* + /wiki/wayback/<hash> 2// HTTP handler. V1 closes the last wiki 501 per the archive cardinal 3// ("no link rot as hygiene of the nishi ecosystem"). 4// 5// COMPOSES: 6// nx_syscalls (sys_mmap; sys_now_ms) 7// nx_search_inverted (nx_inv_hash_bytes_lower for URL -> 64-bit hash) 8// 9// COMPOSED BY: 10// wiki/nx_wiki_routes (dispatches /wiki/archive* + /wiki/wayback/*) 11// 12// V1 SCOPE per NISHI_ARCHIVE_INTEGRATION_CHARTER §5: 13// - NxArchiveStore (in-memory; parallel pools per URL hash) 14// - /wiki/archive -- index page listing all stored snapshots 15// - /wiki/wayback/<hash> -- serves stored snapshot bytes with 16// X-Archive-Original-URL + X-Archive-Snapshot-Date headers 17// - /wiki/commit/<repo>/<hash> -- 404 V1 (V2 ships code-archive walker) 18// - Hardcoded V1 seed list (3 sample snapshots) demonstrates the 19// cite-time-snapshot pattern with real content; V2 adds live fetch 20// - Sealed verdict surface 800-819 (reserved per charter §9) 21// 22// V2 SCOPE (TODO): 23// - nx_wiki_wayback.nx live fetch via nx_http_client + robots.txt 24// - nx_wiki_code_archive.nx gitea API walker 25// - nx_wiki_cite_rewriter.nx wiki build pass that rewrites external 26// URLs to /wiki/wayback/<hash> 27// - nx_wiki_audit_emit.nx structured audit log 28// - nx_kv_store-backed persistence (currently in-memory only) 29// 30// Status: V1. 2026-05-27. 31 32import "nx_syscalls.nx" 33import "nx_search_inverted.nx" 34 35// ===== Sealed verdict surface (charter §9: 800-819 reserved) ================================================= 36const NX_ARCH_OK: i64 = 0 37const NX_ARCH_BAD_INPUT: i64 = 800 38const NX_ARCH_SNAPSHOT_PENDING: i64 = 800 39const NX_ARCH_SNAPSHOT_FAILED: i64 = 801 40const NX_ARCH_BLOCKED_BY_ROBOTS: i64 = 802 41const NX_ARCH_RATE_LIMITED: i64 = 803 42const NX_ARCH_BAD_URL: i64 = 804 43const NX_ARCH_KV_FULL: i64 = 805 44const NX_ARCH_BLOB_FULL: i64 = 806 45const NX_ARCH_COMMIT_NOT_FOUND: i64 = 807 46const NX_ARCH_REPO_NOT_INDEXED: i64 = 808 47const NX_ARCH_M11_VIOLATION: i64 = 809 48const NX_ARCH_M12_VIOLATION: i64 = 810 49const NX_ARCH_M13_VIOLATION: i64 = 811 50// Local-to-router (above the reserved range to avoid collision): 51const NX_ARCH_NOT_FOUND: i64 = 815 52const NX_ARCH_RESP_OVERFLOW: i64 = 816 53const NX_ARCH_RENDER_FAILED: i64 = 817 54 55// ===== Named constants (M7) ================================================= 56const NX_ARCH_MAX_SNAPSHOTS: i64 = 1000 // V1 cap 57const NX_ARCH_URLS_POOL_CAP: i64 = 65536 // 64 KB packed URLs 58const NX_ARCH_DATES_POOL_CAP: i64 = 32768 // 32 KB packed dates 59const NX_ARCH_BODIES_POOL_CAP: i64 = 16777216 // 16 MB packed bodies 60const NX_ARCH_INDEX_PAGE_CAP: i64 = 524288 // 512 KB index page 61const NX_ARCH_HASH_HEX_LEN: i64 = 16 // 16 hex chars per hash 62const NX_ARCH_INT_BUF_CAP: i64 = 32 63 64// ===== NxArchiveStore (in-memory; parallel pools) ================================================= 65 66struct NxArchiveStore { 67 hashes: *i64 // FNV-1a 64 of original URL 68 urls_pool: *u8 69 urls_pool_used: i64 70 urls_offs: *i64 71 urls_lens: *i64 72 dates_pool: *u8 73 dates_pool_used: i64 74 dates_offs: *i64 75 dates_lens: *i64 76 bodies_pool: *u8 77 bodies_pool_used: i64 78 bodies_offs: *i64 79 bodies_lens: *i64 80 content_types: *i64 // 0 = text/html; 1 = text/plain; V2 sealed enum 81 snapshot_count: i64 82 snapshots_cap: i64 83 valid: i64 84} 85 86const NX_ARCH_CT_HTML: i64 = 0 87const NX_ARCH_CT_PLAIN: i64 = 1 88const NX_ARCH_CT_JSON: i64 = 2 89 90func nx_arch_ct_string(ct: i64) -> *u8 { 91 if ct == NX_ARCH_CT_HTML { return "text/html; charset=utf-8" as *u8 } 92 if ct == NX_ARCH_CT_PLAIN { return "text/plain; charset=utf-8" as *u8 } 93 if ct == NX_ARCH_CT_JSON { return "application/json" as *u8 } 94 return "application/octet-stream" as *u8 95} 96 97func nx_arch_ct_string_n(ct: i64) -> i64 { 98 if ct == NX_ARCH_CT_HTML { return 24 } 99 if ct == NX_ARCH_CT_PLAIN { return 25 } 100 if ct == NX_ARCH_CT_JSON { return 16 } 101 return 24 102} 103 104func nx_archive_store_init(s: *NxArchiveStore, snapshots_cap: i64) -> i64 { 105 if (s as i64) == 0 { return 0 - NX_ARCH_BAD_INPUT } 106 if snapshots_cap < 1 { return 0 - NX_ARCH_BAD_INPUT } 107 if snapshots_cap > NX_ARCH_MAX_SNAPSHOTS { return 0 - NX_ARCH_KV_FULL } 108 109 s.hashes = (sys_mmap(snapshots_cap * 8)) as *i64 110 s.urls_pool = sys_mmap(NX_ARCH_URLS_POOL_CAP) 111 s.urls_pool_used = 0 112 s.urls_offs = (sys_mmap(snapshots_cap * 8)) as *i64 113 s.urls_lens = (sys_mmap(snapshots_cap * 8)) as *i64 114 s.dates_pool = sys_mmap(NX_ARCH_DATES_POOL_CAP) 115 s.dates_pool_used = 0 116 s.dates_offs = (sys_mmap(snapshots_cap * 8)) as *i64 117 s.dates_lens = (sys_mmap(snapshots_cap * 8)) as *i64 118 s.bodies_pool = sys_mmap(NX_ARCH_BODIES_POOL_CAP) 119 s.bodies_pool_used = 0 120 s.bodies_offs = (sys_mmap(snapshots_cap * 8)) as *i64 121 s.bodies_lens = (sys_mmap(snapshots_cap * 8)) as *i64 122 s.content_types = (sys_mmap(snapshots_cap * 8)) as *i64 123 s.snapshot_count = 0 124 s.snapshots_cap = snapshots_cap 125 s.valid = 1 126 return NX_ARCH_OK 127} 128 129// Add a snapshot: caller supplies original URL bytes + snapshot ISO8601 130// date + body bytes + content-type kind. Returns the URL's hash on 131// success (>= 0 always since FNV-1a 64 is non-zero for non-empty 132// inputs), or -verdict. 133func nx_archive_store_add(s: *NxArchiveStore, 134 url: *u8, url_n: i64, 135 date: *u8, date_n: i64, 136 body: *u8, body_n: i64, 137 content_type: i64) -> i64 { 138 if s.valid != 1 { return 0 - NX_ARCH_BAD_INPUT } 139 if s.snapshot_count >= s.snapshots_cap { return 0 - NX_ARCH_KV_FULL } 140 if (url as i64) == 0 { return 0 - NX_ARCH_BAD_URL } 141 if url_n < 1 { return 0 - NX_ARCH_BAD_URL } 142 if url_n > 2048 { return 0 - NX_ARCH_BAD_URL } 143 if date_n < 1 { return 0 - NX_ARCH_BAD_INPUT } 144 if date_n > 32 { return 0 - NX_ARCH_BAD_INPUT } 145 if body_n < 0 { return 0 - NX_ARCH_BAD_INPUT } 146 if body_n > NX_ARCH_BODIES_POOL_CAP / 4 { return 0 - NX_ARCH_BLOB_FULL } 147 148 // Compute URL hash. 149 let h: i64 = nx_inv_hash_bytes_lower(url, url_n) 150 151 let idx: i64 = s.snapshot_count 152 s.hashes[idx] = h 153 154 // URL pool 155 if s.urls_pool_used + url_n > NX_ARCH_URLS_POOL_CAP { return 0 - NX_ARCH_KV_FULL } 156 let u_off: i64 = s.urls_pool_used 157 var i: i64 = 0 158 while i < url_n { s.urls_pool[u_off + i] = url[i]; i = i + 1 } 159 s.urls_pool_used = s.urls_pool_used + url_n 160 s.urls_offs[idx] = u_off 161 s.urls_lens[idx] = url_n 162 163 // Date pool 164 if s.dates_pool_used + date_n > NX_ARCH_DATES_POOL_CAP { return 0 - NX_ARCH_KV_FULL } 165 let d_off: i64 = s.dates_pool_used 166 i = 0 167 while i < date_n { s.dates_pool[d_off + i] = date[i]; i = i + 1 } 168 s.dates_pool_used = s.dates_pool_used + date_n 169 s.dates_offs[idx] = d_off 170 s.dates_lens[idx] = date_n 171 172 // Body pool 173 if s.bodies_pool_used + body_n > NX_ARCH_BODIES_POOL_CAP { return 0 - NX_ARCH_BLOB_FULL } 174 let b_off: i64 = s.bodies_pool_used 175 i = 0 176 while i < body_n { s.bodies_pool[b_off + i] = body[i]; i = i + 1 } 177 s.bodies_pool_used = s.bodies_pool_used + body_n 178 s.bodies_offs[idx] = b_off 179 s.bodies_lens[idx] = body_n 180 181 s.content_types[idx] = content_type 182 s.snapshot_count = s.snapshot_count + 1 183 return h 184} 185 186// Lookup by URL hash; returns idx (>= 0) or -1. 187func nx_archive_store_find_by_hash(s: *NxArchiveStore, h: i64) -> i64 { 188 if s.valid != 1 { return 0 - 1 } 189 var i: i64 = 0 190 while i < s.snapshot_count { 191 if s.hashes[i] == h { return i } 192 i = i + 1 193 } 194 return 0 - 1 195} 196 197// ===== Hex parser: 16 hex chars -> i64 ================================================= 198 199func nx_arch_hex_to_i64(src: *u8, src_n: i64) -> i64 { 200 if src_n != NX_ARCH_HASH_HEX_LEN { return 0 } 201 var v: i64 = 0 202 var i: i64 = 0 203 while i < src_n { 204 let c: i64 = src[i] as i64 205 var d: i64 = 0 - 1 206 if c >= 0x30 { if c <= 0x39 { d = c - 0x30 } } 207 if c >= 0x61 { if c <= 0x66 { d = c - 0x57 } } 208 if c >= 0x41 { if c <= 0x46 { d = c - 0x37 } } 209 if d < 0 { return 0 } 210 v = (v << 4) | d 211 i = i + 1 212 } 213 return v 214} 215 216// ===== Hex emit: i64 -> 16 lowercase hex chars ================================================= 217 218func nx_arch_i64_to_hex(v: i64, out: *u8) -> i64 { 219 var i: i64 = 0 220 while i < NX_ARCH_HASH_HEX_LEN { 221 let nibble: i64 = (v >> ((15 - i) * 4)) & 0xF 222 var c: i64 = 0x30 + nibble 223 if nibble >= 10 { c = 0x61 + nibble - 10 } 224 out[i] = (c & 0xff) as u8 225 i = i + 1 226 } 227 return NX_ARCH_HASH_HEX_LEN 228} 229 230// ===== Decimal int helper ================================================= 231 232func nx_arch_int_to_dec(v: i64, out: *u8, cap: i64) -> i64 { 233 if cap < 2 { return 0 } 234 if v == 0 { out[0] = 0x30 as u8; return 1 } 235 var n: i64 = v 236 let tmp: *u8 = sys_mmap(NX_ARCH_INT_BUF_CAP) 237 var k: i64 = 0 238 while n > 0 { tmp[k] = (0x30 + (n % 10)) as u8; n = n / 10; k = k + 1 } 239 var off: i64 = 0 240 var j: i64 = k - 1 241 while j >= 0 { out[off] = tmp[j]; off = off + 1; j = j - 1 } 242 return off 243} 244 245// ===== /wiki/archive index page ================================================= 246// 247// Renders all snapshots as an HTML list with clickable /wiki/wayback/<hash> links. 248 249func nx_arch_serve_index(s: *NxArchiveStore, 250 resp_buf: *u8, resp_cap: i64, 251 out_resp_n: *i64) -> i64 { 252 let body_buf: *u8 = sys_mmap(NX_ARCH_INDEX_PAGE_CAP) 253 var body_off: i64 = 0 254 255 let head: *u8 = "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Nishi Archive Index | Nishi Wiki</title></head><body><header><a href=\"/wiki/\">Wiki</a><a href=\"/wiki/dashboard\">Dashboard</a><a href=\"/wiki/search\">Search</a><a href=\"/wiki/archive\">Archive</a></header><main><h1>Nishi Archive Index</h1>\n<p>Cite-time snapshot store per <a href=\"/wiki/nishi-archive-integration-charter\">ARCHIVE_INTEGRATION_CHARTER</a>. " as *u8 256 let head_n: i64 = 437 257 var i: i64 = 0 258 while i < head_n { body_buf[body_off + i] = head[i]; i = i + 1 } 259 body_off = body_off + head_n 260 261 // Snapshot count 262 let cnt_lit: *u8 = "Snapshots stored: <b>" as *u8 263 i = 0 264 while i < 21 { body_buf[body_off + i] = cnt_lit[i]; i = i + 1 } 265 body_off = body_off + 21 266 let cnt_buf: *u8 = sys_mmap(NX_ARCH_INT_BUF_CAP) 267 let cnt_n: i64 = nx_arch_int_to_dec(s.snapshot_count, cnt_buf, NX_ARCH_INT_BUF_CAP) 268 i = 0 269 while i < cnt_n { body_buf[body_off + i] = cnt_buf[i]; i = i + 1 } 270 body_off = body_off + cnt_n 271 let cnt_close: *u8 = "</b></p>\n<ul>\n" as *u8 272 i = 0 273 while i < 14 { body_buf[body_off + i] = cnt_close[i]; i = i + 1 } 274 body_off = body_off + 14 275 276 // Per-snapshot li 277 var si: i64 = 0 278 while si < s.snapshot_count { 279 let li_open: *u8 = "<li><a href=\"/wiki/wayback/" as *u8 280 i = 0 281 while i < 27 { body_buf[body_off + i] = li_open[i]; i = i + 1 } 282 body_off = body_off + 27 283 // Hash hex 284 let hex_buf: *u8 = sys_mmap(17) 285 nx_arch_i64_to_hex(s.hashes[si], hex_buf) 286 i = 0 287 while i < NX_ARCH_HASH_HEX_LEN { body_buf[body_off + i] = hex_buf[i]; i = i + 1 } 288 body_off = body_off + NX_ARCH_HASH_HEX_LEN 289 let mid: *u8 = "\">" as *u8 290 i = 0 291 while i < 2 { body_buf[body_off + i] = mid[i]; i = i + 1 } 292 body_off = body_off + 2 293 // URL bytes 294 let url_p: *u8 = ((s.urls_pool as i64) + s.urls_offs[si]) as *u8 295 let url_n: i64 = s.urls_lens[si] 296 i = 0 297 while i < url_n { body_buf[body_off + i] = url_p[i]; i = i + 1 } 298 body_off = body_off + url_n 299 let close_a: *u8 = "</a> <small>(" as *u8 300 i = 0 301 while i < 13 { body_buf[body_off + i] = close_a[i]; i = i + 1 } 302 body_off = body_off + 13 303 // Date 304 let date_p: *u8 = ((s.dates_pool as i64) + s.dates_offs[si]) as *u8 305 let date_n: i64 = s.dates_lens[si] 306 i = 0 307 while i < date_n { body_buf[body_off + i] = date_p[i]; i = i + 1 } 308 body_off = body_off + date_n 309 let close_li: *u8 = ")</small></li>\n" as *u8 310 i = 0 311 while i < 15 { body_buf[body_off + i] = close_li[i]; i = i + 1 } 312 body_off = body_off + 15 313 si = si + 1 314 } 315 316 let foot: *u8 = "</ul>\n<p><em>V2 adds live fetch via nx_http_client + robots.txt + crawl policy. V1 ships in-memory store + hardcoded seed.</em></p>\n</main></body></html>" as *u8 317 i = 0 318 while i < 156 { body_buf[body_off + i] = foot[i]; i = i + 1 } 319 body_off = body_off + 156 320 321 // HTTP wrap 322 let hdr: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8 323 let hdr_n: i64 = 71 324 let crlf2: *u8 = "\r\n\r\n" as *u8 325 let len_buf: *u8 = sys_mmap(NX_ARCH_INT_BUF_CAP) 326 let ld: i64 = nx_arch_int_to_dec(body_off, len_buf, NX_ARCH_INT_BUF_CAP) 327 let need: i64 = hdr_n + ld + 4 + body_off 328 if need > resp_cap { return 0 - NX_ARCH_RESP_OVERFLOW } 329 var off: i64 = 0 330 i = 0 331 while i < hdr_n { resp_buf[off + i] = hdr[i]; i = i + 1 } 332 off = off + hdr_n 333 i = 0 334 while i < ld { resp_buf[off + i] = len_buf[i]; i = i + 1 } 335 off = off + ld 336 i = 0 337 while i < 4 { resp_buf[off + i] = crlf2[i]; i = i + 1 } 338 off = off + 4 339 i = 0 340 while i < body_off { resp_buf[off + i] = body_buf[i]; i = i + 1 } 341 off = off + body_off 342 out_resp_n[0] = off 343 return NX_ARCH_OK 344} 345 346// ===== /wiki/wayback/<hash> -- serve a stored snapshot ================================================= 347 348func nx_arch_serve_wayback(s: *NxArchiveStore, hash_hex: *u8, hash_hex_n: i64, 349 resp_buf: *u8, resp_cap: i64, 350 out_resp_n: *i64) -> i64 { 351 if hash_hex_n != NX_ARCH_HASH_HEX_LEN { return 0 - NX_ARCH_NOT_FOUND } 352 let h: i64 = nx_arch_hex_to_i64(hash_hex, hash_hex_n) 353 let idx: i64 = nx_archive_store_find_by_hash(s, h) 354 if idx < 0 { return 0 - NX_ARCH_NOT_FOUND } 355 356 let body_p: *u8 = ((s.bodies_pool as i64) + s.bodies_offs[idx]) as *u8 357 let body_n: i64 = s.bodies_lens[idx] 358 let url_p: *u8 = ((s.urls_pool as i64) + s.urls_offs[idx]) as *u8 359 let url_n: i64 = s.urls_lens[idx] 360 let date_p: *u8 = ((s.dates_pool as i64) + s.dates_offs[idx]) as *u8 361 let date_n: i64 = s.dates_lens[idx] 362 let ct: i64 = s.content_types[idx] 363 364 // HTTP headers (per charter §5): 365 // HTTP/1.1 200 OK 366 // Content-Type: <preserved> 367 // X-Archive-Original-URL: <url> 368 // X-Archive-Snapshot-Date: <date> 369 // Content-Length: <body_n> 370 let hdr1: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: " as *u8 371 let hdr1_n: i64 = 31 372 let ct_str: *u8 = nx_arch_ct_string(ct) 373 let ct_n: i64 = nx_arch_ct_string_n(ct) 374 let hdr2: *u8 = "\r\nX-Archive-Original-URL: " as *u8 375 let hdr2_n: i64 = 26 376 let hdr3: *u8 = "\r\nX-Archive-Snapshot-Date: " as *u8 377 let hdr3_n: i64 = 27 378 let hdr4: *u8 = "\r\nContent-Length: " as *u8 379 let hdr4_n: i64 = 18 380 let crlf2: *u8 = "\r\n\r\n" as *u8 381 382 let len_buf: *u8 = sys_mmap(NX_ARCH_INT_BUF_CAP) 383 let ld: i64 = nx_arch_int_to_dec(body_n, len_buf, NX_ARCH_INT_BUF_CAP) 384 385 let need: i64 = hdr1_n + ct_n + hdr2_n + url_n + hdr3_n + date_n + hdr4_n + ld + 4 + body_n 386 if need > resp_cap { return 0 - NX_ARCH_RESP_OVERFLOW } 387 388 var off: i64 = 0 389 var i: i64 = 0 390 while i < hdr1_n { resp_buf[off + i] = hdr1[i]; i = i + 1 } 391 off = off + hdr1_n 392 i = 0; while i < ct_n { resp_buf[off + i] = ct_str[i]; i = i + 1 }; off = off + ct_n 393 i = 0; while i < hdr2_n { resp_buf[off + i] = hdr2[i]; i = i + 1 }; off = off + hdr2_n 394 i = 0; while i < url_n { resp_buf[off + i] = url_p[i]; i = i + 1 }; off = off + url_n 395 i = 0; while i < hdr3_n { resp_buf[off + i] = hdr3[i]; i = i + 1 }; off = off + hdr3_n 396 i = 0; while i < date_n { resp_buf[off + i] = date_p[i]; i = i + 1 }; off = off + date_n 397 i = 0; while i < hdr4_n { resp_buf[off + i] = hdr4[i]; i = i + 1 }; off = off + hdr4_n 398 i = 0; while i < ld { resp_buf[off + i] = len_buf[i]; i = i + 1 }; off = off + ld 399 i = 0; while i < 4 { resp_buf[off + i] = crlf2[i]; i = i + 1 }; off = off + 4 400 i = 0; while i < body_n { resp_buf[off + i] = body_p[i]; i = i + 1 }; off = off + body_n 401 402 out_resp_n[0] = off 403 return NX_ARCH_OK 404} 405 406// ===== Top-level dispatcher: /wiki/archive vs /wiki/wayback/<hash> ================================================= 407 408func nx_wiki_archive_route(s: *NxArchiveStore, 409 url_path: *u8, url_path_n: i64, 410 resp_buf: *u8, resp_cap: i64, 411 out_resp_n: *i64) -> i64 { 412 if (s as i64) == 0 { return 0 - NX_ARCH_NOT_FOUND } 413 if s.valid != 1 { return 0 - NX_ARCH_NOT_FOUND } 414 415 // /wiki/archive (index) 416 if url_path_n == 13 { 417 var matched: i64 = 1 418 let target: *u8 = "/wiki/archive" as *u8 419 var i: i64 = 0 420 // Flat byte-compare (was a dense one-liner with the loop-var-jump + 421 // nested-if-increment shape that the native compiler miscompiles -- 422 // T#native-codegen-context-sensitive-miscompile / nested-if-incr). 423 // Comparing all 13 bytes is identical for a matched check: once matched 424 // is cleared it stays cleared. 425 while i < 13 { 426 if url_path[i] != target[i] { matched = 0 } 427 i = i + 1 428 } 429 if matched == 1 { return nx_arch_serve_index(s, resp_buf, resp_cap, out_resp_n) } 430 } 431 432 // /wiki/wayback/<hash> (16 hex chars expected) 433 // Path: "/wiki/wayback/" = 14 bytes; total expected = 14 + 16 = 30 434 if url_path_n == 30 { 435 var matched: i64 = 1 436 let target: *u8 = "/wiki/wayback/" as *u8 437 var i: i64 = 0 438 // Flat byte-compare (see note above; same miscompile-avoidance reshape). 439 while i < 14 { 440 if url_path[i] != target[i] { matched = 0 } 441 i = i + 1 442 } 443 if matched == 1 { 444 let hash_ptr: *u8 = (url_path as i64 + 14) as *u8 445 return nx_arch_serve_wayback(s, hash_ptr, 16, resp_buf, resp_cap, out_resp_n) 446 } 447 } 448 449 return 0 - NX_ARCH_NOT_FOUND 450}