code wiki / _hdl_build / nx_wiki_restore.nx
nx_wiki_restore.nx source
↩ module page · 115 lines · 6343 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
39const VR_MODE_0644: i64 = 0x1a4
40
41func vr_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
42
43// write buf[0..n] to path (truncating prior content); 0 ok, <0 on open/short-write.
44func vr_write_file(path: *u8, buf: *u8, n: i64) -> i64 {
45 let fd: i64 = sys_openat_wr(path, VR_MODE_0644)
46 if fd < 0 { return 0 - 1 }
47 var off: i64 = 0
48 while off < n {
49 let wr: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
50 if wr <= 0 { sys_close(fd); return 0 - 2 }
51 off = off + wr
52 }
53 sys_close(fd)
54 return 0
55}
56
57// ===== restore_ex: resolve v# -> CID -> exact bytes -> stage -> guarded re-publish =================
58// prefix = the archive/index store prefix (production: WAR_PREFIX).
59// slug = the page slug to restore.
60// version = the retained version# to restore TO (1-based; ANY retained point).
61// stage_path = a local file path the exact bytes are written to before pub_publish_ex reads them back.
62// cs_*/ncorpus= the corpus set the guard checks the page's /wiki links against.
63// do_push_flag= 1 to actually push live; 0 to exercise the byte-path hermetically (guard verdict still
64// enforced, nothing pushed).
65// out = receives the A3 push verdict. nout[0] = restored byte length (>=0). Returns VR_OK or a
66// negative VR_* code. On an unknown v#, returns VR_NOT_FOUND WITHOUT touching the push path.
67func restore_ex(prefix: *u8, slug: *u8, version: i64, stage_path: *u8,
68 cs_ptr: *i64, cs_len: *i64, ncorpus: i64,
69 do_push_flag: i64, out: *PubResult, nout: *i64) -> i64 {
70 nout[0] = 0
71 if (slug as i64) == 0 { return VR_BAD_INPUT }
72 if vr_len(slug) < 1 { return VR_BAD_INPUT }
73 if (stage_path as i64) == 0 { return VR_BAD_INPUT }
74 if version < 1 { return VR_BAD_INPUT }
75
76 // 1. resolve v# -> retained CID (REFUSE an unknown version BEFORE any push).
77 let cid: *u8 = sys_mmap(80)
78 let cl: i64 = vp_cid_of_version(prefix, slug, version, cid)
79 if cl <= 0 { return VR_NOT_FOUND }
80
81 // 2. pull the EXACT bytes by CID (content-addressed => byte-identical to what was captured).
82 let pp: *i64 = sys_mmap(16) as *i64
83 let blen: i64 = war_get_by_cid(prefix, cid, pp, VR_SCAN_CAP)
84 if blen <= 0 { return VR_BLOB_MISSING }
85 let body: *u8 = pp[0] as *u8
86
87 // 3. stage the exact bytes to a local file so the guarded publish reads them like a production page.
88 if vr_write_file(stage_path, body, blen) != 0 { return VR_WRITE_FAILED }
89 nout[0] = blen
90
91 // 4. GUARDED re-publish (A3): the restored page goes live ONLY if the A2 guard re-ALLOWs it.
92 let pubrc: i64 = pub_publish_ex(prefix, stage_path, slug,
93 cs_ptr, cs_len, ncorpus, do_push_flag, out)
94 if pubrc == PUB_REJECTED { return VR_PUSH_REJECT }
95 return VR_OK
96}
97
98// production convenience: prefix = WAR_PREFIX (live durable store).
99func restore(slug: *u8, version: i64, stage_path: *u8,
100 cs_ptr: *i64, cs_len: *i64, ncorpus: i64,
101 do_push_flag: i64, out: *PubResult, nout: *i64) -> i64 {
102 return restore_ex(WAR_PREFIX, slug, version, stage_path, cs_ptr, cs_len, ncorpus,
103 do_push_flag, out, nout)
104}
105
106// human-readable verdict name (for the live-proof runner / logs).
107func vr_status_name(s: i64) -> *u8 {
108 if s == VR_OK { return "VR_OK" as *u8 }
109 if s == VR_BAD_INPUT { return "VR_BAD_INPUT" as *u8 }
110 if s == VR_NOT_FOUND { return "VR_NOT_FOUND" as *u8 }
111 if s == VR_BLOB_MISSING { return "VR_BLOB_MISSING" as *u8 }
112 if s == VR_WRITE_FAILED { return "VR_WRITE_FAILED" as *u8 }
113 if s == VR_PUSH_REJECT { return "VR_PUSH_REJECT" as *u8 }
114 return "VR_UNKNOWN" as *u8
115}