nx_jrnl_archive_gate.nx source
↩ module page · 148 lines · 7659 B
1// nx_jrnl_archive_gate.nx -- GATE for nx_jrnl_archive (lib composed in-process, nothing forked).
2// The verb's whole contract is PRESERVE-BEFORE-REMOVE, so the load-bearing teeth are the ones where
3// the archive step FAILS and the live journal must come out byte-identical (T6), and the one where
4// the verify step can actually see a corrupted copy (T10 -- a hash compare that cannot fail is not a
5// verify). Fixtures live under /tmp/nx_jrnl_archive_gate_<pid>/ (data only; nothing is executed from
6// /tmp, which the NAS mounts noexec). license_tier: ORIGINAL No hw writes (Rule 26).
7import "nx_syscalls.nx"
8import "nx_gate_verdict.nx"
9import "nx_jrnl_archive_lib.nx"
10
11const JAG_FIX_LINES: i64 = 40 // enough rows that a short write is detectable, small enough to read by eye
12const JAG_EPOCH_A: i64 = 1700000001 // fixed epochs: archive names must be reproducible across runs
13const JAG_EPOCH_B: i64 = 1700000002
14const JAG_EPOCH_C: i64 = 1700000003
15
16func jg_pid() -> i64 { return __syscall(172, 0, 0, 0, 0, 0, 0) }
17func jg_path(base: *u8, leaf: *u8) -> *u8 {
18 let p: *u8 = sys_mmap(JA_PATH_MAX)
19 var o: i64 = ja_cat(p, 0, base)
20 o = ja_cat(p, o, "/" as *u8)
21 o = ja_cat(p, o, leaf)
22 return p
23}
24func jg_size(path: *u8) -> i64 {
25 let ln: *i64 = sys_mmap(16) as *i64
26 let b: *u8 = sys_read_file(path, ln)
27 if (b as i64) == 0 { return 0 - 1 }
28 let n: i64 = ln[0]
29 sys_free_file(b, n)
30 return n
31}
32func jg_digest(path: *u8, out: *u8) -> i64 {
33 let ln: *i64 = sys_mmap(16) as *i64
34 let b: *u8 = sys_read_file(path, ln)
35 if (b as i64) == 0 { return 0 - 1 }
36 let n: i64 = ln[0]
37 sha256_digest(b, n, out)
38 sys_free_file(b, n)
39 return n
40}
41func jg_same(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while i < JA_DIGEST_BYTES { if a[i] != b[i] { return 0 } i = i + 1 } return 1 }
42// a journal fixture: JAG_FIX_LINES rows "epoch<TAB>target<TAB>bytes" -- the comparepub shape
43func jg_make_journal(path: *u8) -> i64 {
44 let buf: *u8 = sys_mmap(JAG_FIX_LINES * 64)
45 var o: i64 = 0
46 var i: i64 = 0
47 while i < JAG_FIX_LINES {
48 o = ja_catn(buf, o, 1787500000 + i)
49 buf[o] = 9 as u8; o = o + 1
50 o = ja_cat(buf, o, "../sites/nishifamily/compare/fixture/api.json" as *u8)
51 buf[o] = 9 as u8; o = o + 1
52 o = ja_catn(buf, o, 1000 + i * 7)
53 buf[o] = 10 as u8; o = o + 1
54 i = i + 1
55 }
56 return ja_write_all(path, buf, o, JA_ARCH_MODE)
57}
58
59func main(argc: i64, argv: *i64) -> i64 {
60 gv_head("=== nx_jrnl_archive_gate -- the archive verb is preserve-before-remove BY CONSTRUCTION ===" as *u8)
61 let ctr: *i64 = gv_ctr()
62 let base: *u8 = sys_mmap(JA_PATH_MAX)
63 var bo: i64 = ja_cat(base, 0, "/tmp/nx_jrnl_archive_gate_" as *u8)
64 bo = ja_catn(base, bo, jg_pid())
65 sys_mkdir(base, JA_DIR_MODE)
66 let jr: *u8 = jg_path(base, "fixture.jrnl" as *u8)
67 let mk: i64 = jg_make_journal(jr)
68 let n0: i64 = jg_size(jr)
69 ja_puts(" fixture_bytes=" as *u8); ja_pn(n0); ja_puts("\n" as *u8)
70 var reached: i64 = 0
71 if mk == 0 { if n0 > 0 { reached = 1 } }
72 gv_check("T1 fixture-reached-the-condition (journal written, non-empty)" as *u8, reached, ctr)
73 let d0: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
74 jg_digest(jr, d0)
75 let rep: *i64 = sys_mmap(32) as *i64
76
77 // ---- the happy path: archive, verify, reset ----
78 let rcA: i64 = ja_archive(jr, 0 as *u8, JAG_EPOCH_A, rep)
79 gv_check("T2 archive-returns-OK" as *u8, rcA == 0, ctr)
80 let archA: *u8 = jg_path(base, "archive/fixture.jrnl.1700000001" as *u8)
81 let dA: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
82 let nA: i64 = jg_digest(archA, dA)
83 ja_puts(" archived_bytes=" as *u8); ja_pn(nA); ja_puts(" original_bytes=" as *u8); ja_pn(n0); ja_puts("\n" as *u8)
84 var sameA: i64 = 0
85 if nA == n0 { if jg_same(d0, dA) == 1 { sameA = 1 } }
86 gv_check("T3 archived-copy-byte-identical-to-original (re-read from disk, sha256 equal)" as *u8, sameA, ctr)
87 let nLive: i64 = jg_size(jr)
88 ja_puts(" live_bytes_after=" as *u8); ja_pn(nLive); ja_puts("\n" as *u8)
89 gv_check("T4 live-journal-reset-to-zero-bytes" as *u8, nLive == 0, ctr)
90 var repok: i64 = 0
91 if rep[0] == n0 { if rep[1] == 1 { if rep[2] == 1 { repok = 1 } } }
92 gv_check("T5 announce-carries-bytes-verified-reset (report slots match the measurement)" as *u8, repok, ctr)
93
94 // ---- neg-control: an unwritable archive dir must leave the live journal byte-identical ----
95 let jr2: *u8 = jg_path(base, "fixture2.jrnl" as *u8)
96 jg_make_journal(jr2)
97 let d2: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
98 let n2: i64 = jg_digest(jr2, d2)
99 // a directory path UNDER A FILE cannot be created or written into (ENOTDIR)
100 let baddir: *u8 = jg_path(base, "fixture2.jrnl/notadir" as *u8)
101 let rcB: i64 = ja_archive(jr2, baddir, JAG_EPOCH_B, rep)
102 let d2b: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
103 let n2b: i64 = jg_digest(jr2, d2b)
104 var untouched: i64 = 0
105 if n2b == n2 { if jg_same(d2, d2b) == 1 { untouched = 1 } }
106 ja_puts(" unwritable_dir_rc=" as *u8); ja_pn(rcB); ja_puts(" live_bytes_before=" as *u8); ja_pn(n2); ja_puts(" after=" as *u8); ja_pn(n2b); ja_puts("\n" as *u8)
107 gv_check("T6 neg-control-archive-write-failure-leaves-live-journal-byte-identical (rc 4, preserve-before-remove)" as *u8, untouched, ctr)
108 gv_check("T6b neg-control-archive-write-failure-returns-4" as *u8, rcB == 4, ctr)
109
110 // ---- neg-control: a missing journal is rc 3 and creates nothing ----
111 let jr3: *u8 = jg_path(base, "does_not_exist.jrnl" as *u8)
112 let rcC: i64 = ja_archive(jr3, 0 as *u8, JAG_EPOCH_C, rep)
113 let arch3: *u8 = jg_path(base, "archive/does_not_exist.jrnl.1700000003" as *u8)
114 gv_check("T7 neg-control-missing-journal-returns-3" as *u8, rcC == 3, ctr)
115 gv_check("T7b neg-control-missing-journal-creates-no-archive" as *u8, jg_size(arch3) < 0, ctr)
116
117 // ---- an empty journal: nothing to do, rc 1, no file made, live still empty ----
118 let rcD: i64 = ja_archive(jr, 0 as *u8, JAG_EPOCH_B, rep)
119 let archD: *u8 = jg_path(base, "archive/fixture.jrnl.1700000002" as *u8)
120 gv_check("T8 empty-journal-returns-1-nothing-to-archive" as *u8, rcD == 1, ctr)
121 gv_check("T8b empty-journal-creates-no-archive-file" as *u8, jg_size(archD) < 0, ctr)
122 gv_check("T8c empty-journal-stays-empty" as *u8, jg_size(jr) == 0, ctr)
123
124 // ---- the verify tooth can FAIL: corrupt one byte of the archived copy and re-hash ----
125 let ln: *i64 = sys_mmap(16) as *i64
126 let ab: *u8 = sys_read_file(archA, ln)
127 var corrupt_seen: i64 = 0
128 if (ab as i64) != 0 { if ln[0] > 0 {
129 if ab[0] == (48 as u8) { ab[0] = 49 as u8 } else { ab[0] = 48 as u8 }
130 ja_write_all(archA, ab, ln[0], JA_ARCH_MODE)
131 let dC: *u8 = sys_mmap(JA_DIGEST_BYTES + 8)
132 jg_digest(archA, dC)
133 if jg_same(d0, dC) == 0 { corrupt_seen = 1 }
134 } }
135 gv_check("T9 neg-control-one-byte-corruption-of-the-archive-changes-the-digest (the verify compare is not vacuous)" as *u8, corrupt_seen, ctr)
136
137 // ---- second fixture, named dir: the archive_dir argument is honoured ----
138 let jr4: *u8 = jg_path(base, "fixture4.jrnl" as *u8)
139 jg_make_journal(jr4)
140 let dir4: *u8 = jg_path(base, "elsewhere" as *u8)
141 let rcE: i64 = ja_archive(jr4, dir4, JAG_EPOCH_A, rep)
142 let arch4: *u8 = jg_path(base, "elsewhere/fixture4.jrnl.1700000001" as *u8)
143 var dir_ok: i64 = 0
144 if rcE == 0 { if jg_size(arch4) > 0 { dir_ok = 1 } }
145 gv_check("T10 named-archive-dir-is-honoured (file lands there, rc 0)" as *u8, dir_ok, ctr)
146
147 return gv_verdict("JRNL-ARCHIVE" as *u8, ctr, "preserve-before-remove: reset only after a re-read hash matches" as *u8)
148}