code wiki / _hdl_build / nx_wiki_restore.nx
nx_wiki_restore.nx source
↩ module page · 114 lines · 6304 B
1// nx_wiki_restore.nx -- A4a: FULL RESTORE to ANY retained rollback point.
2//
3// THE OPERATOR PRECONDITION (verbatim intent): roll back "not just to another broken one but have FULL
4// RESTORE via MULTIPLE rollback points." restore(slug, v#) makes the LIVE page byte-exact the content of
5// version v# -- ANY v#, not just the last. The bytes come from the content-addressed archive by CID, so
6// what gets republished is GUARANTEED identical to what was captured (a CID names exactly one byte string).
7//
8// COMPOSITION (no storage/crypto reinvented):
9// nx_wiki_versioned_publish vp_cid_of_version (resolve v# -> CID from the durable version index)
10// nx_wiki_archive war_get_by_cid (no-loss retrieval of the EXACT bytes by CID)
11// nx_wiki_publish pub_publish_ex (A3 GUARDED re-publish -- the restore goes live ONLY if
12// the A2 guard re-ALLOWs it; a restore is itself a guarded publish)
13// nx_seg_store (transitively, via the archive store)
14//
15// ADDITIVE: a restore writes NOTHING destructive. It reads an immutable blob and re-publishes it; the
16// version index is unchanged (the restored content already has its own retained v#). It is also safe to
17// pair with vpub -- a caller that wants the restore itself recorded as a NEW head simply vpub's the
18// restored bytes afterward (the live-proof runner does exactly this is NOT required: restore re-pushes the
19// exact old bytes; re-capturing is optional and additive).
20//
21// REFUSAL (liar-kill): an unknown v# returns VR_NOT_FOUND and the push path is NEVER reached -- a restore
22// cannot silently push garbage for a version that was never retained.
23//
24// Pure NishiLang, NO sql/.sh/.py/.js, no new .tsv/.conf. license_tier: ORIGINAL
25import "nx_wiki_versioned_publish.nx"
26import "nx_wiki_archive.nx"
27import "nx_wiki_publish.nx"
28import "nx_syscalls.nx"
29
30// ===== sealed verdict surface (codes 4950-4959) ===================================================
31const VR_OK: i64 = 0
32const VR_BAD_INPUT: i64 = 0 - 4950 // null/empty slug, or v# < 1
33const VR_NOT_FOUND: i64 = 0 - 4951 // requested version# has no retained record (REFUSED, no push)
34const VR_BLOB_MISSING: i64 = 0 - 4952 // CID retained but its blob could not be resolved (store damage)
35const VR_WRITE_FAILED: i64 = 0 - 4953 // could not stage the restored bytes to a local file
36const VR_PUSH_REJECT: i64 = 0 - 4954 // A3 guard REJECTED the restored page (fail-closed)
37
38const VR_SCAN_CAP: i64 = 256 // segment-scan cap for war_get_by_cid / version-index lookups
39
40func vr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
41
42// write buf[0..n] to path (truncating prior content); 0 ok, <0 on open/short-write.
43func vr_write_file(path: *u8, buf: *u8, n: i64) -> i64 {
44 let fd: i64 = sys_openat_wr(path, MODE_0644)
45 if fd < 0 { return 0 - 1 }
46 var off: i64 = 0
47 while off < n {
48 let wr: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
49 if wr <= 0 { sys_close(fd); return 0 - 2 }
50 off = off + wr
51 }
52 sys_close(fd)
53 return 0
54}
55
56// ===== restore_ex: resolve v# -> CID -> exact bytes -> stage -> guarded re-publish =================
57// prefix = the archive/index store prefix (production: WAR_PREFIX).
58// slug = the page slug to restore.
59// version = the retained version# to restore TO (1-based; ANY retained point).
60// stage_path = a local file path the exact bytes are written to before pub_publish_ex reads them back.
61// cs_*/ncorpus= the corpus set the guard checks the page's /wiki links against.
62// do_push_flag= 1 to actually push live; 0 to exercise the byte-path hermetically (guard verdict still
63// enforced, nothing pushed).
64// out = receives the A3 push verdict. nout[0] = restored byte length (>=0). Returns VR_OK or a
65// negative VR_* code. On an unknown v#, returns VR_NOT_FOUND WITHOUT touching the push path.
66func restore_ex(prefix: *u8, slug: *u8, version: i64, stage_path: *u8,
67 cs_ptr: *i64, cs_len: *i64, ncorpus: i64,
68 do_push_flag: i64, out: *PubResult, nout: *i64) -> i64 {
69 nout[0] = 0
70 if (slug as i64) == 0 { return VR_BAD_INPUT }
71 if vr_len(slug) < 1 { return VR_BAD_INPUT }
72 if (stage_path as i64) == 0 { return VR_BAD_INPUT }
73 if version < 1 { return VR_BAD_INPUT }
74
75 // 1. resolve v# -> retained CID (REFUSE an unknown version BEFORE any push).
76 let cid: *u8 = sys_mmap(80)
77 let cl: i64 = vp_cid_of_version(prefix, slug, version, cid)
78 if cl <= 0 { return VR_NOT_FOUND }
79
80 // 2. pull the EXACT bytes by CID (content-addressed => byte-identical to what was captured).
81 let pp: *i64 = sys_mmap(16) as *i64
82 let blen: i64 = war_get_by_cid(prefix, cid, pp, VR_SCAN_CAP)
83 if blen <= 0 { return VR_BLOB_MISSING }
84 let body: *u8 = pp[0] as *u8
85
86 // 3. stage the exact bytes to a local file so the guarded publish reads them like a production page.
87 if vr_write_file(stage_path, body, blen) != 0 { return VR_WRITE_FAILED }
88 nout[0] = blen
89
90 // 4. GUARDED re-publish (A3): the restored page goes live ONLY if the A2 guard re-ALLOWs it.
91 let pubrc: i64 = pub_publish_ex(prefix, stage_path, slug,
92 cs_ptr, cs_len, ncorpus, do_push_flag, out)
93 if pubrc == PUB_REJECTED { return VR_PUSH_REJECT }
94 return VR_OK
95}
96
97// production convenience: prefix = WAR_PREFIX (live durable store).
98func restore(slug: *u8, version: i64, stage_path: *u8,
99 cs_ptr: *i64, cs_len: *i64, ncorpus: i64,
100 do_push_flag: i64, out: *PubResult, nout: *i64) -> i64 {
101 return restore_ex(WAR_PREFIX, slug, version, stage_path, cs_ptr, cs_len, ncorpus,
102 do_push_flag, out, nout)
103}
104
105// human-readable verdict name (for the live-proof runner / logs).
106func vr_status_name(s: i64) -> *u8 {
107 if s == VR_OK { return "VR_OK" as *u8 }
108 if s == VR_BAD_INPUT { return "VR_BAD_INPUT" as *u8 }
109 if s == VR_NOT_FOUND { return "VR_NOT_FOUND" as *u8 }
110 if s == VR_BLOB_MISSING { return "VR_BLOB_MISSING" as *u8 }
111 if s == VR_WRITE_FAILED { return "VR_WRITE_FAILED" as *u8 }
112 if s == VR_PUSH_REJECT { return "VR_PUSH_REJECT" as *u8 }
113 return "VR_UNKNOWN" as *u8
114}