code wiki / (root) / nx_jrnl_archive_gate.nx

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}