nx_legal_store.nx source
↩ module page · 125 lines · 5373 B
1// nx_legal_store.nx -- LEGAL RUNG D7: durable persistence for the legal stores.
2//
3// module: nishi-core.legal.store
4// capability: LEGAL_DOC_PERSIST
5//
6// The logic cores (D1 vault / D2 envelope+recipients / D4 annotations) hold their
7// state in caller-allocated flat i64 record arrays -- correct, but lost on a
8// restart. This rung makes them DURABLE: serialize a flat array to a per-tenant
9// file with an integrity header, and load it back byte-identical so the portal
10// daemon survives a restart.
11//
12// FORMAT : [magic i64][stride i64][count i64] then count*stride i64 records.
13// INTEGRITY: load verifies the magic AND that the stored stride matches the
14// caller's expected stride -- so a vault file can never be misread as
15// an envelope file (different stride) -> silent corruption refused.
16// PER-TENANT: the path is <base><tenant>.<name>, tenant validated [A-Za-z0-9_-]
17// (nx_vault_valid_tid, the proven discipline) -> no cross-tenant
18// traversal; one tenant can never load another's store.
19//
20// HONEST scope: this proves the persistence CONTRACT (save/load/survive-restart/
21// tenant-isolated/integrity-checked) over a plain per-tenant file. The production
22// path composes nx_seg_store (the same durable, concurrent-safe segmented store
23// the email mailbox already persists to) -- a pattern-covered refinement, not a
24// new primitive.
25//
26// Composes: nx_doc_vault (nx_vault_valid_tid tenant discipline). license_tier: ORIGINAL
27// lineage_id: nishi_legal_store_d7
28import "nx_syscalls.nx"
29import "nx_doc_vault.nx"
30
31const LS_MAGIC: i64 = 0x4E584C5331 // "NXLS1" -- recognizes a legal-store file
32const LS_OK: i64 = 0
33const LS_ERR_TENANT: i64 = 1
34const LS_ERR_OPEN: i64 = 2
35const LS_ERR_MAGIC: i64 = 3
36const LS_ERR_STRIDE: i64 = 4
37
38func ls_cat(out: *u8, o: i64, s: *u8) -> i64 { var k: i64 = 0; while s[k] != (0 as u8) { out[o] = s[k]; o = o + 1; k = k + 1 } return o }
39
40// build the per-tenant store path <base><tenant>.<name> (tenant validated). -1 on bad tenant.
41func ls_path(out: *u8, base: *u8, tenant: *u8, name: *u8) -> i64 {
42 if nx_vault_valid_tid(tenant) == 0 { return 0 - 1 }
43 var o: i64 = 0
44 o = ls_cat(out, o, base)
45 o = ls_cat(out, o, tenant)
46 out[o] = 46 as u8; o = o + 1 // '.'
47 o = ls_cat(out, o, name)
48 out[o] = 0 as u8
49 return o
50}
51
52// persist a flat record array (count records of `stride` i64 each) to path. Returns LS_OK / LS_ERR_*.
53func ls_save(flat: *i64, count: i64, stride: i64, path: *u8) -> i64 {
54 let fd: i64 = sys_openat_wr(path, 420)
55 if fd < 0 { return LS_ERR_OPEN }
56 let hdr: *i64 = sys_mmap(32) as *i64
57 hdr[0] = LS_MAGIC; hdr[1] = stride; hdr[2] = count
58 sys_write(fd, hdr as *u8, 24)
59 if count > 0 { sys_write(fd, flat as *u8, count * stride * 8) }
60 sys_close(fd)
61 return LS_OK
62}
63
64// load records from path into flat (capacity cap records, expected `stride`). Returns the record
65// count, or -1 if absent, or -LS_ERR_* on a corrupt/mismatched file (never silent garbage).
66func ls_load(flat: *i64, cap: i64, stride: i64, path: *u8) -> i64 {
67 let fd: i64 = sys_openat_rd(path)
68 if fd < 0 { return 0 - 1 }
69 let hdr: *i64 = sys_mmap(32) as *i64
70 let hr: i64 = sys_read(fd, hdr as *u8, 24)
71 if hr != 24 { sys_close(fd); return 0 - LS_ERR_MAGIC }
72 if hdr[0] != LS_MAGIC { sys_close(fd); return 0 - LS_ERR_MAGIC }
73 if hdr[1] != stride { sys_close(fd); return 0 - LS_ERR_STRIDE }
74 let count: i64 = hdr[2]
75 if count > cap { sys_close(fd); return 0 - LS_ERR_STRIDE }
76 if count > 0 {
77 let want: i64 = count * stride * 8
78 var got: i64 = 0
79 while got < want {
80 let r: i64 = sys_read(fd, (flat as i64 + got) as *u8, want - got)
81 if r <= 0 { sys_close(fd); return 0 - LS_ERR_MAGIC }
82 got = got + r
83 }
84 }
85 sys_close(fd)
86 return count
87}
88
89const LS_BMAGIC: i64 = 0x4E584C42 // "NXLB" -- recognizes a legal-blob (raw document bytes) file
90
91// persist a raw document BLOB (the actual bytes, not an i64 record array) to path.
92// [magic i64][len i64][bytes]. Returns LS_OK / LS_ERR_*.
93func ls_blob_save(bytes: *u8, len: i64, path: *u8) -> i64 {
94 let fd: i64 = sys_openat_wr(path, 420)
95 if fd < 0 { return LS_ERR_OPEN }
96 let hdr: *i64 = sys_mmap(32) as *i64
97 hdr[0] = LS_BMAGIC; hdr[1] = len
98 sys_write(fd, hdr as *u8, 16)
99 if len > 0 { sys_write(fd, bytes, len) }
100 sys_close(fd)
101 return LS_OK
102}
103
104// load a document blob from path into out (capacity cap). Returns the byte length,
105// or -1 if absent, or -LS_ERR_* on a corrupt/oversized file (never silent garbage).
106func ls_blob_load(out: *u8, cap: i64, path: *u8) -> i64 {
107 let fd: i64 = sys_openat_rd(path)
108 if fd < 0 { return 0 - 1 }
109 let hdr: *i64 = sys_mmap(32) as *i64
110 let hr: i64 = sys_read(fd, hdr as *u8, 16)
111 if hr != 16 { sys_close(fd); return 0 - LS_ERR_MAGIC }
112 if hdr[0] != LS_BMAGIC { sys_close(fd); return 0 - LS_ERR_MAGIC }
113 let len: i64 = hdr[1]
114 if len > cap { sys_close(fd); return 0 - LS_ERR_STRIDE }
115 if len > 0 {
116 var got: i64 = 0
117 while got < len {
118 let r: i64 = sys_read(fd, (out as i64 + got) as *u8, len - got)
119 if r <= 0 { sys_close(fd); return 0 - LS_ERR_MAGIC }
120 got = got + r
121 }
122 }
123 sys_close(fd)
124 return len
125}