code wiki / _hdl_build / nx_jrnlguard.nx

nx_jrnlguard.nx source

↩ module page · 541 lines · 21717 B

1// nx_jrnlguard.nx -- DATA-LAYER CLOBBER/SHRINK DETECTOR (ws=ws-intel9, 2026-07-20). 2// The ecosystem guards its ROUTES (nx_route_diff), its TOOLS (nx_tooldiff), its BUILDS and 3// its CRONS -- but nothing guards the append-only JOURNALS AND PLANES that a dozen concurrent 4// sessions write all day (ws_sync, claims, pm_intake, actlog, every knowledge/store plane). 5// This is the data-layer twin, and it eats the filed anti-clobber CLASS (F744-F748). 6// CONTRACT: an append-only journal may only GROW. So: 7// bytes shrink -> RED (truncation / overwrite) 8// frames shrink -> RED (lines lost) 9// ANY per-key count DECREASES -> RED <-- THE SUBTLE ONE: a rewrite can ADD bytes while 10// silently DROPPING frames, so a totals-only check passes while data is already gone. 11// (Observed for real on the memory index 2026-07-20: it grew and lost entries at once.) 12// malformed lines rise -> WARN (corruption creeping in; a column-shifted row emits invalid 13// downstream JSON -- observed live in the commontask catalog this same session) 14// A shrink is REPORTED, never "repaired": if the shrink was an INTENTIONAL compaction, the 15// operator re-snapshots the baseline (the same discipline nx_route_diff uses for routes). 16// key column is an ARGUMENT (default 1) so one organ guards every journal shape. 17// SCALE LAW: windowed read + declared envelope (window_bytes/truncated/keys_capped). 18// license_tier: ORIGINAL No hw writes (Rule 26). 19// snapshot <journal> <baseline> [keycol] -> write baseline, JSON summary 20// check <journal> <baseline> [keycol] -> JSON verdict; exit 0 GREEN / 3 RED / 4 no-baseline 21// scan <journal> [keycol] -> structural audit JSON (READ-ONLY) 22// selftest <scratch-base> -> gate T1..T8 (caller pre-cleans) 23import "nx_syscalls.nx" 24import "nx_gate_verdict.nx" 25const JG_MAGIC_1024: i64 = 1024 26 27const JG_WIN: i64 = 4194304 28const JG_BASE: i64 = 262144 29const JG_OUT: i64 = 262144 30const JG_SOFT: i64 = 49152 31const JG_KEY_MAX: i64 = 120 32const JG_MIN_COLS: i64 = 3 33const JG_KEYS_CAP: i64 = 200 34 35func jg_cat(d: *u8, o: i64, s: *u8) -> i64 { var p: i64=o; var i: i64=0; while s[i]!=(0 as u8){ d[p]=s[i]; p=p+1; i=i+1 } return p } 36func jg_catn(d: *u8, o: i64, v: i64) -> i64 { 37 let t: *u8 = sys_mmap(28) 38 var m: i64 = v 39 var p: i64 = o 40 if m < 0 { d[p]=45 as u8; p=p+1; m=0-m } 41 var k: i64 = 0 42 if m == 0 { t[0]=48 as u8; k=1 } 43 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } 44 var i: i64 = 0 45 while i < k { d[p]=t[k-1-i]; p=p+1; i=i+1 } 46 return p 47} 48func jg_vlen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 49func jg_atoi_z(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v } 50func jg_read(path: *u8, buf: *u8, cap: i64) -> i64 { 51 let fd: i64 = sys_openat_rd(path) 52 if fd < 0 { return -1 } 53 var n: i64 = 0 54 var go: i64 = 1 55 while go == 1 { let r: i64 = sys_read(fd, ((buf as i64)+n) as *u8, cap-n); if r <= 0 { go = 0 } else { n = n + r } if n >= cap { go = 0 } } 56 sys_close(fd) 57 return n 58} 59func jg_le(q: *u8, i: i64, n: i64) -> i64 { 60 var le: i64 = i 61 var s: i64 = 1 62 while s==1 { if le>=n { s=0 } else { if q[le]==(10 as u8){s=0} else {le=le+1} } } 63 return le 64} 65func jg_col(q: *u8, ls: i64, le: i64, c: i64, out: *i64) -> i64 { 66 var col: i64 = 0 67 var p: i64 = ls 68 while col < c { 69 var s: i64 = 1 70 while s == 1 { if p >= le { return 0 } if q[p]==(9 as u8) { s = 0 } else { p = p+1 } } 71 p = p + 1 72 col = col + 1 73 } 74 var e: i64 = p 75 var s2: i64 = 1 76 while s2 == 1 { if e >= le { s2 = 0 } else { if q[e]==(9 as u8) { s2 = 0 } else { e = e+1 } } } 77 out[0] = p 78 out[1] = e 79 return 1 80} 81func jg_ncols(q: *u8, ls: i64, le: i64) -> i64 { 82 if le <= ls { return 0 } 83 var c: i64 = 1 84 var i: i64 = ls 85 while i < le { if q[i]==(9 as u8) { c = c + 1 } i = i + 1 } 86 return c 87} 88func jg_span_eq(q: *u8, s1: i64, e1: i64, s2: i64, e2: i64) -> i64 { 89 if e1-s1 != e2-s2 { return 0 } 90 var i: i64 = 0 91 while s1+i < e1 { if q[s1+i]!=q[s2+i] { return 0 } i=i+1 } 92 return 1 93} 94func jg_lit_eq(q: *u8, s: i64, e: i64, lit: *u8) -> i64 { 95 var i: i64 = 0 96 while s+i < e { if lit[i]==(0 as u8) { return 0 } if q[s+i]!=lit[i] { return 0 } i=i+1 } 97 if lit[i]!=(0 as u8) { return 0 } 98 return 1 99} 100// cross-buffer span compare (baseline key text vs journal key text) 101func jg_cross_eq(a: *u8, as1: i64, ae: i64, b: *u8, bs: i64, be: i64) -> i64 { 102 if ae-as1 != be-bs { return 0 } 103 var i: i64 = 0 104 while as1+i < ae { if a[as1+i]!=b[bs+i] { return 0 } i=i+1 } 105 return 1 106} 107// a line counts as a FRAME if it is non-empty and not a comment 108func jg_is_frame(q: *u8, ls: i64, le: i64) -> i64 { 109 if le <= ls { return 0 } 110 if q[ls]==(35 as u8) { return 0 } 111 return 1 112} 113func jg_frames(q: *u8, n: i64) -> i64 { 114 var c: i64 = 0 115 var i: i64 = 0 116 while i < n { let le: i64 = jg_le(q,i,n); if jg_is_frame(q,i,le)==1 { c = c + 1 } i = le + 1 } 117 return c 118} 119func jg_malformed(q: *u8, n: i64) -> i64 { 120 var c: i64 = 0 121 var i: i64 = 0 122 while i < n { 123 let le: i64 = jg_le(q,i,n) 124 if jg_is_frame(q,i,le)==1 { if jg_ncols(q,i,le) < JG_MIN_COLS { c = c + 1 } } 125 i = le + 1 126 } 127 return c 128} 129// first occurrence of this key span among frames before `upto`? 130func jg_key_first(q: *u8, upto: i64, keycol: i64, ks: i64, ke: i64) -> i64 { 131 let ck: *i64 = sys_mmap(16) as *i64 132 var i: i64 = 0 133 while i < upto { 134 let le: i64 = jg_le(q,i,upto) 135 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_span_eq(q,ck[0],ck[1],ks,ke)==1 { return 0 } } } 136 i = le + 1 137 } 138 return 1 139} 140func jg_key_count(q: *u8, n: i64, keycol: i64, ks: i64, ke: i64) -> i64 { 141 let ck: *i64 = sys_mmap(16) as *i64 142 var c: i64 = 0 143 var i: i64 = 0 144 while i < n { 145 let le: i64 = jg_le(q,i,n) 146 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_span_eq(q,ck[0],ck[1],ks,ke)==1 { c = c + 1 } } } 147 i = le + 1 148 } 149 return c 150} 151// count of a key given as literal text in ANOTHER buffer (baseline -> journal lookup) 152func jg_key_count_x(q: *u8, n: i64, keycol: i64, b: *u8, bs: i64, be: i64) -> i64 { 153 let ck: *i64 = sys_mmap(16) as *i64 154 var c: i64 = 0 155 var i: i64 = 0 156 while i < n { 157 let le: i64 = jg_le(q,i,n) 158 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_cross_eq(q,ck[0],ck[1],b,bs,be)==1 { c = c + 1 } } } 159 i = le + 1 160 } 161 return c 162} 163// ---------- baseline write ---------- 164func jg_write_baseline(path: *u8, q: *u8, n: i64, keycol: i64, cap_hit: *i64) -> i64 { 165 let d: *u8 = sys_mmap(JG_OUT) 166 var p: i64 = 0 167 p = jg_cat(d,p,"# nx_jrnlguard baseline v1 -- keys prefixed __ are reserved totals\n" as *u8) 168 p = jg_cat(d,p,"__bytes__\t" as *u8) 169 p = jg_catn(d,p,n) 170 d[p]=10 as u8; p=p+1 171 p = jg_cat(d,p,"__frames__\t" as *u8) 172 p = jg_catn(d,p,jg_frames(q,n)) 173 d[p]=10 as u8; p=p+1 174 p = jg_cat(d,p,"__malformed__\t" as *u8) 175 p = jg_catn(d,p,jg_malformed(q,n)) 176 d[p]=10 as u8; p=p+1 177 p = jg_cat(d,p,"__keycol__\t" as *u8) 178 p = jg_catn(d,p,keycol) 179 d[p]=10 as u8; p=p+1 180 let ck: *i64 = sys_mmap(16) as *i64 181 var keys: i64 = 0 182 cap_hit[0] = 0 183 var i: i64 = 0 184 while i < n { 185 let le: i64 = jg_le(q,i,n) 186 if jg_is_frame(q,i,le)==1 { if jg_col(q,i,le,keycol,ck)==1 { if jg_key_first(q,i,keycol,ck[0],ck[1])==1 { 187 if keys >= JG_KEYS_CAP { cap_hit[0] = cap_hit[0] + 1 } else { 188 var z: i64 = ck[0] 189 var used: i64 = 0 190 while z < ck[1] { if used < JG_KEY_MAX { d[p]=q[z]; p=p+1; used=used+1 } z=z+1 } 191 d[p]=9 as u8; p=p+1 192 p = jg_catn(d,p,jg_key_count(q,n,keycol,ck[0],ck[1])) 193 d[p]=10 as u8; p=p+1 194 keys = keys + 1 195 } 196 } } } 197 i = le + 1 198 } 199 let fd: i64 = sys_openat_wr(path, 0x1a4) 200 if fd < 0 { return -1 } 201 sys_write(fd,d,p) 202 sys_close(fd) 203 return keys 204} 205func jg_base_num(b: *u8, bn: i64, name: *u8) -> i64 { 206 let c0: *i64 = sys_mmap(16) as *i64 207 let c1: *i64 = sys_mmap(16) as *i64 208 var i: i64 = 0 209 while i < bn { 210 let le: i64 = jg_le(b,i,bn) 211 if jg_is_frame(b,i,le)==1 { if jg_col(b,i,le,0,c0)==1 { if jg_lit_eq(b,c0[0],c0[1],name)==1 { 212 if jg_col(b,i,le,1,c1)==1 { var v: i64 = 0; var z: i64 = c1[0]; while z < c1[1] { let ch: i64 = b[z] as i64; if ch>=48 { if ch<=57 { v = v*10 + (ch-48) } } z=z+1 } return v } 213 } } } 214 i = le + 1 215 } 216 return -1 217} 218func jg_key(d: *u8, o: i64, name: *u8) -> i64 { 219 d[o]=34 as u8 220 var p: i64 = o + 1 221 p = jg_cat(d,p,name) 222 d[p]=34 as u8 223 p = p + 1 224 d[p]=58 as u8 225 p = p + 1 226 return p 227} 228func jg_qlit(d: *u8, o: i64, s: *u8) -> i64 { 229 var p: i64 = o 230 d[p]=34 as u8; p=p+1 231 p = jg_cat(d,p,s) 232 d[p]=34 as u8; p=p+1 233 return p 234} 235func jg_jesc(d: *u8, o: i64, q: *u8, s: i64, e: i64, maxb: i64) -> i64 { 236 var p: i64 = o 237 var i: i64 = s 238 var used: i64 = 0 239 while i < e { 240 if used >= maxb { i = e } else { 241 let c: i64 = q[i] as i64 242 if c == 34 { d[p]=92 as u8; p=p+1; d[p]=34 as u8; p=p+1 } else { if c == 92 { d[p]=92 as u8; p=p+1; d[p]=92 as u8; p=p+1 } else { if c < 32 { d[p]=32 as u8; p=p+1 } else { d[p]=c as u8; p=p+1 } } } 243 used = used + 2 244 i = i + 1 245 } 246 } 247 return p 248} 249// ---------- the verdict ---------- 250// vx[0]=bytes_now vx[1]=bytes_base vx[2]=frames_now vx[3]=frames_base 251// vx[4]=malformed_now vx[5]=malformed_base vx[6]=keys_decreased vx[7]=keys_checked vx[8]=red 252func jg_verdict(q: *u8, n: i64, b: *u8, bn: i64, keycol: i64, vx: *i64, d: *u8, dpos: i64) -> i64 { 253 var p: i64 = dpos 254 vx[0]=n 255 vx[1]=jg_base_num(b,bn,"__bytes__" as *u8) 256 vx[2]=jg_frames(q,n) 257 vx[3]=jg_base_num(b,bn,"__frames__" as *u8) 258 vx[4]=jg_malformed(q,n) 259 vx[5]=jg_base_num(b,bn,"__malformed__" as *u8) 260 vx[6]=0 261 vx[7]=0 262 vx[8]=0 263 if vx[0] < vx[1] { vx[8] = 1 } 264 if vx[2] < vx[3] { vx[8] = 1 } 265 // per-key regression sweep: THE subtle clobber tooth 266 p = jg_key(d,p,"regressed_keys" as *u8) 267 d[p]=91 as u8; p=p+1 268 let c0: *i64 = sys_mmap(16) as *i64 269 let c1: *i64 = sys_mmap(16) as *i64 270 var first: i64 = 1 271 var i: i64 = 0 272 while i < bn { 273 let le: i64 = jg_le(b,i,bn) 274 if jg_is_frame(b,i,le)==1 { if jg_col(b,i,le,0,c0)==1 { 275 var reserved: i64 = 0 276 if c0[1]-c0[0] >= 2 { if b[c0[0]]==(95 as u8) { if b[c0[0]+1]==(95 as u8) { reserved = 1 } } } 277 if reserved == 0 { if jg_col(b,i,le,1,c1)==1 { 278 var basec: i64 = 0 279 var z: i64 = c1[0] 280 while z < c1[1] { let ch: i64 = b[z] as i64; if ch>=48 { if ch<=57 { basec = basec*10 + (ch-48) } } z=z+1 } 281 let nowc: i64 = jg_key_count_x(q,n,keycol,b,c0[0],c0[1]) 282 vx[7] = vx[7] + 1 283 if nowc < basec { 284 vx[6] = vx[6] + 1 285 vx[8] = 1 286 if p < JG_SOFT { 287 if first==0 { d[p]=44 as u8; p=p+1 } 288 first=0 289 d[p]=123 as u8; p=p+1 290 p = jg_key(d,p,"key" as *u8) 291 d[p]=34 as u8; p=p+1 292 p = jg_jesc(d,p,b,c0[0],c0[1],JG_KEY_MAX) 293 d[p]=34 as u8; p=p+1 294 d[p]=44 as u8; p=p+1 295 p = jg_key(d,p,"was" as *u8) 296 p = jg_catn(d,p,basec) 297 d[p]=44 as u8; p=p+1 298 p = jg_key(d,p,"now" as *u8) 299 p = jg_catn(d,p,nowc) 300 d[p]=125 as u8; p=p+1 301 } 302 } 303 } } 304 } } 305 i = le + 1 306 } 307 d[p]=93 as u8; p=p+1 308 d[p]=44 as u8; p=p+1 309 return p 310} 311func jg_emit_common(d: *u8, dpos: i64, jp: *u8, n: i64, keycol: i64, trunc: i64, mode: *u8) -> i64 { 312 var p: i64 = dpos 313 d[p]=123 as u8; p=p+1 314 p = jg_key(d,p,"v" as *u8) 315 p = jg_catn(d,p,1) 316 d[p]=44 as u8; p=p+1 317 p = jg_key(d,p,"tool" as *u8) 318 p = jg_qlit(d,p,"nx_jrnlguard" as *u8) 319 d[p]=44 as u8; p=p+1 320 p = jg_key(d,p,"mode" as *u8) 321 p = jg_qlit(d,p,mode) 322 d[p]=44 as u8; p=p+1 323 p = jg_key(d,p,"journal" as *u8) 324 d[p]=34 as u8; p=p+1 325 p = jg_jesc(d,p,jp,0,jg_vlen(jp),200) 326 d[p]=34 as u8; p=p+1 327 d[p]=44 as u8; p=p+1 328 p = jg_key(d,p,"keycol" as *u8) 329 p = jg_catn(d,p,keycol) 330 d[p]=44 as u8; p=p+1 331 p = jg_key(d,p,"window_bytes" as *u8) 332 p = jg_catn(d,p,JG_WIN) 333 d[p]=44 as u8; p=p+1 334 p = jg_key(d,p,"truncated" as *u8) 335 p = jg_catn(d,p,trunc) 336 d[p]=44 as u8; p=p+1 337 return p 338} 339func jg_wr(path: *u8, body: *u8) -> i64 { 340 let fd: i64 = sys_openat_wr(path, 0x1a4) 341 if fd < 0 { return -1 } 342 sys_write(fd, body, jg_vlen(body)) 343 sys_close(fd) 344 return 0 345} 346func jg_mkpath(dst: *u8, src: *u8, suf: *u8) -> i64 { 347 var p: i64 = 0 348 p = jg_cat(dst,p,src) 349 p = jg_cat(dst,p,suf) 350 dst[p]=0 as u8 351 return p 352} 353func jg_find(d: *u8, dn: i64, lit: *u8) -> i64 { 354 let ll: i64 = jg_vlen(lit) 355 if ll == 0 { return 0 } 356 var i: i64 = 0 357 while i + ll <= dn { 358 var k: i64 = 0 359 var ok: i64 = 1 360 while k < ll { if d[i+k]!=lit[k] { ok=0; k=ll } else { k=k+1 } } 361 if ok==1 { return 1 } 362 i = i + 1 363 } 364 return 0 365} 366 367// ---------- selftest gate ---------- 368func jg_selftest(base: *u8) -> i64 { 369 let ctr: *i64 = gv_ctr() 370 gv_head("nx_jrnlguard selftest -- data-layer clobber teeth (caller pre-cleans paths)" as *u8) 371 let q: *u8 = sys_mmap(JG_WIN) 372 let bb: *u8 = sys_mmap(JG_BASE) 373 let vx: *i64 = sys_mmap(128) as *i64 374 let ch: *i64 = sys_mmap(16) as *i64 375 let d: *u8 = sys_mmap(JG_OUT) 376 let jp: *u8 = sys_mmap(JG_MAGIC_1024) 377 let bp: *u8 = sys_mmap(JG_MAGIC_1024) 378 let rcap: i64 = JG_WIN - 8 379 jg_mkpath(jp, base, ".jrnl" as *u8) 380 jg_mkpath(bp, base, ".base" as *u8) 381 // T1 absent journal -> honest ABSENT (-1), no fabrication 382 var n: i64 = jg_read(jp,q,rcap) 383 var ok1: i64 = 0 384 if n == (0-1) { ok1=1 } 385 gv_check("T1 absent journal reported ABSENT never fabricated" as *u8, ok1, ctr) 386 // seed a 4-frame journal, 2 verbs 387 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n2\tBEAT\ta\tx\tnote2\n3\tBEAT\ta\tx\tnote3\n4\tDONE\ta\tx\tnote4\n" as *u8) 388 n = jg_read(jp,q,rcap) 389 let keys: i64 = jg_write_baseline(bp,q,n,1,ch) 390 var bn: i64 = jg_read(bp,bb,JG_BASE-8) 391 var ok2: i64 = 0 392 if keys==3 { if jg_base_num(bb,bn,"__frames__" as *u8)==4 { ok2=1 } } 393 gv_check("T2 baseline records frames and distinct keys" as *u8, ok2, ctr) 394 // T3 unchanged -> GREEN 395 jg_verdict(q,n,bb,bn,1,vx,d,0) 396 var ok3: i64 = 0 397 if vx[8]==0 { ok3=1 } 398 gv_check("T3 unchanged journal verdict GREEN" as *u8, ok3, ctr) 399 // T4 lawful append-only growth -> GREEN 400 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n2\tBEAT\ta\tx\tnote2\n3\tBEAT\ta\tx\tnote3\n4\tDONE\ta\tx\tnote4\n5\tBEAT\ta\tx\tnote5\n" as *u8) 401 n = jg_read(jp,q,rcap) 402 jg_verdict(q,n,bb,bn,1,vx,d,0) 403 var ok4: i64 = 0 404 if vx[8]==0 { if vx[2]==5 { ok4=1 } } 405 gv_check("T4 append-only growth stays GREEN" as *u8, ok4, ctr) 406 // T5 TRUNCATION (bytes+frames shrink) -> RED 407 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\n" as *u8) 408 n = jg_read(jp,q,rcap) 409 jg_verdict(q,n,bb,bn,1,vx,d,0) 410 var ok5: i64 = 0 411 if vx[8]==1 { if vx[2] < vx[3] { ok5=1 } } 412 gv_check("T5 truncation detected RED" as *u8, ok5, ctr) 413 // T6 THE SUBTLE CLOBBER: file GROWS in bytes+frames but a key's frames were DROPPED 414 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1-rewritten-with-much-more-text-than-before\n9\tNOTE\ta\tx\tbrand new frame\n8\tNOTE\ta\tx\tanother brand new frame\n7\tNOTE\ta\tx\tand another one here\n6\tNOTE\ta\tx\tand yet another padding frame\n" as *u8) 415 n = jg_read(jp,q,rcap) 416 let dl6: i64 = jg_verdict(q,n,bb,bn,1,vx,d,0) 417 var ok6: i64 = 0 418 if vx[0] > vx[1] { if vx[2] >= vx[3] { if vx[8]==1 { if vx[6]==2 { ok6=1 } } } } 419 gv_check("T6 grew in bytes+frames yet lost keys detected RED" as *u8, ok6, ctr) 420 // T7 the RED report names the lost keys with was/now counts 421 var ok7: i64 = 0 422 if dl6 > 0 { if jg_find(d,dl6,"BEAT" as *u8)==1 { if jg_find(d,dl6,"DONE" as *u8)==1 { if jg_find(d,dl6,"was" as *u8)==1 { ok7=1 } } } } 423 gv_check("T7 report names each lost key with was and now" as *u8, ok7, ctr) 424 // T8 malformed-line audit counts column-shifted corruption 425 jg_wr(jp, "1\tKICKOFF\ta\tx\tnote1\nthis-line-has-no-tabs-at-all\n2\tBEAT\ta\tx\tnote2\n" as *u8) 426 n = jg_read(jp,q,rcap) 427 var ok8: i64 = 0 428 if jg_malformed(q,n)==1 { if jg_frames(q,n)==3 { ok8=1 } } 429 gv_check("T8 malformed corrupted line audited not silently parsed" as *u8, ok8, ctr) 430 let rc: i64 = gv_verdict("JRNLGUARD-GATE" as *u8, ctr, "data-layer clobber teeth green" as *u8) 431 return rc 432} 433 434func main(argc: i64, argv: *i64) -> i64 { 435 if argc < 3 { gv_puts("usage: nx_jrnlguard {snapshot <journal> <baseline> [keycol] | check <journal> <baseline> [keycol] | scan <journal> [keycol] | selftest <base>}\n" as *u8); sys_exit(2); return 2 } 436 let verb: *u8 = argv[1] as *u8 437 let vl: i64 = jg_vlen(verb) 438 if jg_lit_eq(verb,0,vl,"selftest" as *u8)==1 { let rc: i64 = jg_selftest(argv[2] as *u8); sys_exit(rc); return rc } 439 let jp: *u8 = argv[2] as *u8 440 let q: *u8 = sys_mmap(JG_WIN) 441 let rcap: i64 = JG_WIN - 8 442 let nr: i64 = jg_read(jp,q,rcap) 443 if nr < 0 { gv_puts("NX-JRNLGUARD ABSENT journal=" as *u8); gv_puts(jp); gv_puts("\n" as *u8); sys_exit(4); return 4 } 444 let n: i64 = nr 445 var trunc: i64 = 0 446 if n >= rcap { trunc = 1 } 447 let d: *u8 = sys_mmap(JG_OUT) 448 let ch: *i64 = sys_mmap(16) as *i64 449 if jg_lit_eq(verb,0,vl,"scan" as *u8)==1 { 450 var kc: i64 = 1 451 if argc >= 4 { let v: i64 = jg_atoi_z(argv[3] as *u8); if v >= 0 { kc = v } } 452 var p: i64 = jg_emit_common(d,0,jp,n,kc,trunc,"scan" as *u8) 453 p = jg_key(d,p,"bytes" as *u8) 454 p = jg_catn(d,p,n) 455 d[p]=44 as u8; p=p+1 456 p = jg_key(d,p,"frames" as *u8) 457 p = jg_catn(d,p,jg_frames(q,n)) 458 d[p]=44 as u8; p=p+1 459 p = jg_key(d,p,"malformed" as *u8) 460 p = jg_catn(d,p,jg_malformed(q,n)) 461 d[p]=125 as u8; p=p+1 462 d[p]=10 as u8; p=p+1 463 sys_write(1,d,p) 464 sys_exit(0) 465 return 0 466 } 467 if argc < 4 { gv_puts("snapshot|check need <journal> <baseline>\n" as *u8); sys_exit(2); return 2 } 468 let bp: *u8 = argv[3] as *u8 469 var kc2: i64 = 1 470 if argc >= 5 { let v2: i64 = jg_atoi_z(argv[4] as *u8); if v2 >= 0 { kc2 = v2 } } 471 if jg_lit_eq(verb,0,vl,"snapshot" as *u8)==1 { 472 let keys: i64 = jg_write_baseline(bp,q,n,kc2,ch) 473 if keys < 0 { gv_puts("NX-JRNLGUARD BASELINE-WRITE-FAILED\n" as *u8); sys_exit(1); return 1 } 474 var p: i64 = jg_emit_common(d,0,jp,n,kc2,trunc,"snapshot" as *u8) 475 p = jg_key(d,p,"bytes" as *u8) 476 p = jg_catn(d,p,n) 477 d[p]=44 as u8; p=p+1 478 p = jg_key(d,p,"frames" as *u8) 479 p = jg_catn(d,p,jg_frames(q,n)) 480 d[p]=44 as u8; p=p+1 481 p = jg_key(d,p,"distinct_keys" as *u8) 482 p = jg_catn(d,p,keys) 483 d[p]=44 as u8; p=p+1 484 p = jg_key(d,p,"keys_capped" as *u8) 485 p = jg_catn(d,p,ch[0]) 486 d[p]=44 as u8; p=p+1 487 p = jg_key(d,p,"malformed" as *u8) 488 p = jg_catn(d,p,jg_malformed(q,n)) 489 d[p]=125 as u8; p=p+1 490 d[p]=10 as u8; p=p+1 491 sys_write(1,d,p) 492 sys_exit(0) 493 return 0 494 } 495 if jg_lit_eq(verb,0,vl,"check" as *u8)==1 { 496 let bb: *u8 = sys_mmap(JG_BASE) 497 let bnr: i64 = jg_read(bp,bb,JG_BASE-8) 498 if bnr <= 0 { gv_puts("NX-JRNLGUARD NO-BASELINE (run snapshot first; fail-closed, nothing judged)\n" as *u8); sys_exit(4); return 4 } 499 let vx: *i64 = sys_mmap(128) as *i64 500 var p: i64 = jg_emit_common(d,0,jp,n,kc2,trunc,"check" as *u8) 501 p = jg_verdict(q,n,bb,bnr,kc2,vx,d,p) 502 p = jg_key(d,p,"bytes_now" as *u8) 503 p = jg_catn(d,p,vx[0]) 504 d[p]=44 as u8; p=p+1 505 p = jg_key(d,p,"bytes_baseline" as *u8) 506 p = jg_catn(d,p,vx[1]) 507 d[p]=44 as u8; p=p+1 508 p = jg_key(d,p,"frames_now" as *u8) 509 p = jg_catn(d,p,vx[2]) 510 d[p]=44 as u8; p=p+1 511 p = jg_key(d,p,"frames_baseline" as *u8) 512 p = jg_catn(d,p,vx[3]) 513 d[p]=44 as u8; p=p+1 514 p = jg_key(d,p,"malformed_now" as *u8) 515 p = jg_catn(d,p,vx[4]) 516 d[p]=44 as u8; p=p+1 517 p = jg_key(d,p,"malformed_baseline" as *u8) 518 p = jg_catn(d,p,vx[5]) 519 d[p]=44 as u8; p=p+1 520 p = jg_key(d,p,"keys_checked" as *u8) 521 p = jg_catn(d,p,vx[7]) 522 d[p]=44 as u8; p=p+1 523 p = jg_key(d,p,"keys_regressed" as *u8) 524 p = jg_catn(d,p,vx[6]) 525 d[p]=44 as u8; p=p+1 526 p = jg_key(d,p,"verdict" as *u8) 527 if vx[8]==1 { p = jg_qlit(d,p,"RED" as *u8) } else { p = jg_qlit(d,p,"GREEN" as *u8) } 528 d[p]=44 as u8; p=p+1 529 p = jg_key(d,p,"note" as *u8) 530 p = jg_qlit(d,p,"append-only journals may only grow; a RED means truncation or dropped frames -- if the shrink was an INTENTIONAL compaction, re-run snapshot to re-baseline" as *u8) 531 d[p]=125 as u8; p=p+1 532 d[p]=10 as u8; p=p+1 533 sys_write(1,d,p) 534 if vx[8]==1 { sys_exit(3); return 3 } 535 sys_exit(0) 536 return 0 537 } 538 gv_puts("unknown verb\n" as *u8) 539 sys_exit(2) 540 return 2 541}