code wiki / _hdl_build / nx_toolsafety_lib.nx
nx_toolsafety_lib.nx source
↩ module page · 608 lines · 27036 B
1// nx_toolsafety_lib.nx -- THE ONE MCP SAFETY-FLAG DERIVER (2026-08-25). Write reach, destructive reach,
2// network reach and exec reach for an organ, derived from its TRANSITIVE import closure, with line
3// comments AND string literals stripped, and syscall DEFINITIONS skipped.
4//
5// WHY A LIB, AND WHY NOW. Two derivers already shipped and DISAGREED BY CONSTRUCTION. nx_schema_backfill
6// (sb_derive_flags) scans ONE FILE, so an organ that writes through an imported helper reaches the write
7// syscall in the LIB and is published readOnly=1 -- wrong in the UNSAFE direction. nx_toolflags folds ONE
8// level of the import closure and strips comments, and its own header set the condition for this
9// extraction in advance: two consumers is not yet a lib, but if a third appears, extract it and delete
10// the copy. The third consumer arrived (the drift control), so this is that extraction.
11// WHEN TWO ORGANS MUST AGREE, MAKE DISAGREEMENT IMPOSSIBLE BY CONSTRUCTION: THERE IS ONLY ONE OF IT.
12//
13// THREE CHANGES FROM THE INCUMBENTS, EACH FOR A REASON THAT WAS MEASURED, NOT PREFERRED:
14// 1. TRANSITIVE closure, not one level. A helper that reaches a writer through a helper of its own is
15// two hops away and one hop cannot see it. The depth LADDER is published per subject, so the
16// contribution of the extra depth is evidence rather than a claim about what it bought.
17// 2. STRING LITERALS ARE STRIPPED TOO. The incumbent declared this a rare limitation erring
18// conservative. It stops being rare the moment the deriver is a LIB that every consumer imports:
19// a marker table written as literals would put write, destructive and network markers into the
20// closure of every organ that composed it. That is why the table is a CONF and this source names
21// no marker at all -- but stripping literals is what makes the property hold for any OTHER scanner
22// too, including the ones already shipped.
23// 3. sys_read_file, so there is no source-size cap left to be inconclusive about. Both incumbents cap
24// at 256 KiB and honestly refuse past it; the primitive that closes that hole already exists, sizes
25// its buffer from the file and cannot short-read.
26//
27// FAIL-CLOSED EVERYWHERE. Unreadable subject, unreadable marker table, an empty marker class, or a walk
28// that hits its budget all return INCONCLUSIVE. None of them may return "nothing matched", because
29// nothing-matched is byte-identical to "this organ is read-only", which is the permissive direction and
30// the entire defect this ruler exists to stop.
31// license_tier: ORIGINAL No hw writes (Rule 26).
32import "nx_syscalls.nx"
33
34const TS_MARKCONF: *u8 = "knowledge/tool_safety_markers.conf" as *u8
35// sized: the marker table declares four classes; a fifth needs a code change here on purpose, because a
36// silently-ignored class would be a marker set that reads as matching nothing.
37const TS_CLASSES: i64 = 4
38const TS_CW: i64 = 0
39const TS_CD: i64 = 1
40const TS_CN: i64 = 2
41const TS_CX: i64 = 3
42// sized: the shipping table carries 5 markers in its largest class; this is generous headroom and the
43// loader REFUSES past it rather than dropping a marker (a dropped marker fails permissive).
44const TS_MAXMARK: i64 = 64
45const TS_MARKCAP: i64 = 64
46const TS_PATHCAP: i64 = 512
47// sized: distinct sources reachable across every closure this lib is asked about in one process. The
48// memo is what makes a transitive walk affordable: nx_syscalls.nx is in essentially every closure and is
49// scanned ONCE. Filling it forces INCONCLUSIVE, never a quiet partial.
50const TS_MEMOSLOTS: i64 = 8192
51const TS_IMPBLK: i64 = 1536
52// sized: files visited in ONE subject's closure walk.
53const TS_MAXWALK: i64 = 1024
54const TS_LF: i64 = 10
55const TS_TAB: i64 = 9
56const TS_HASH_OFF: i64 = 1469598103934665603
57const TS_HASH_PRIME: i64 = 1099511628211
58// odd multiplier for the rolling hash; wrapping i64 multiply, magnitude folded positive each round
59const TS_HASH_MUL: i64 = 131
60const TS_CH_SLASH: i64 = 47
61const TS_CH_QUOTE: i64 = 34
62const TS_CH_BSLASH: i64 = 92
63const TS_CH_SPACE: i64 = 32
64const TS_CH_HASH: i64 = 35
65const TS_CH_F: i64 = 102
66const TS_CH_U: i64 = 117
67const TS_CH_N: i64 = 110
68const TS_CH_C: i64 = 99
69const TS_CH_I: i64 = 105
70const TS_CH_M: i64 = 109
71const TS_CH_P: i64 = 112
72const TS_CH_O: i64 = 111
73const TS_CH_R: i64 = 114
74const TS_CH_T: i64 = 116
75const TS_FUNCPFX: i64 = 5
76const TS_IMPPFX: i64 = 8
77const TS_ROOT_HDL: *u8 = "buildroot/runtime/_hdl_build/" as *u8
78const TS_ROOT_RT: *u8 = "buildroot/runtime/" as *u8
79// depth argument sentinel: walk until the closure is exhausted
80const TS_DEPTH_ALL: i64 = 0 - 1
81
82static TS_MK_P: i64 = 0
83static TS_MKN_P: i64 = 0
84static TS_READY: i64 = 0
85static TS_MEMO_K: i64 = 0
86static TS_MEMO_W: i64 = 0
87static TS_MEMO_D: i64 = 0
88static TS_MEMO_N: i64 = 0
89static TS_MEMO_X: i64 = 0
90static TS_MEMO_OK: i64 = 0
91static TS_MEMO_PATH: i64 = 0
92static TS_MEMO_IMP: i64 = 0
93static TS_MEMO_USED: i64 = 0
94static TS_MEMO_FULL: i64 = 0
95static TS_STAT_FILES: i64 = 0
96static TS_STAT_BYTES: i64 = 0
97static TS_QUEUE_P: i64 = 0
98// The memo slot of the FIRST closure file that supplied the write marker, or -1. A COUNT WITHOUT A
99// WORKLIST IS NOT ACTIONABLE, AND A WORKLIST WITHOUT THE REASON IS STILL ONE STEP SHORT: the attributing
100// file is already in hand at measure time and throwing it away costs the reader one re-run per row.
101// It also separates the two populations that matter -- an organ whose OWN source writes, and an organ
102// that merely imports a lib which can.
103// NOTE: a static may not carry a non-zero initialiser in this dialect (the assembler refuses
104// G3_NONZERO_INIT_STATIC on both lanes), so "no attribution yet" is carried by its OWN flag rather than
105// by a -1 sentinel. Slot 0 is a perfectly valid memo slot, so a 0 sentinel would silently blame it.
106static TS_WSRC: i64 = 0
107static TS_WSRC_OK: i64 = 0
108static TS_TRACE: i64 = 0
109static TS_QDEPTH_P: i64 = 0
110static TS_SEEN_P: i64 = 0
111static TS_SCRATCH_P: i64 = 0
112
113func ts_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
114
115func ts_cat(d: *u8, o: i64, s: *u8) -> i64 {
116 var i: i64 = 0
117 var p: i64 = o
118 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 }
119 return p
120}
121
122// Polynomial rolling hash over the NUL-terminated string. Never returns 0: slot 0 means EMPTY in the
123// memo, so a real key hashing to 0 would read as an empty slot forever -- a cache that never hits.
124//
125// THE FIRST VERSION OF THIS FUNCTION WAS WRONG AND IT FAILED PERMISSIVE, WHICH IS THE EXACT DEFECT CLASS
126// THIS WHOLE LIB EXISTS TO CATCH. It reduced h modulo a prime and then multiplied by that same prime
127// every round, which collapses the state onto a subgroup and collides freely between same-length
128// strings. MEASURED by the trace verb: nx_divprobe's closure visited 2 files instead of 3 --
129// "buildroot/runtime/nx_syscalls.nx" collided with "buildroot/runtime/nx_divprobe.nx" (both 32 bytes)
130// and the shim was silently dropped from the walk. Fewer files scanned means less reach found, so the
131// bug's error direction was TOWARD read-only.
132// A HASH COLLISION IN A VISITED-SET IS NOT A PERFORMANCE BUG, IT IS SILENT DATA LOSS -- AND HERE IT LOST
133// IT IN THE FLATTERING DIRECTION, WHICH IS WHY NOTHING LOOKED WRONG.
134// Fixed in BOTH places it could bite: the hash distributes properly now, AND every dedup hit is verified
135// by string compare, so a future collision costs a comparison instead of a closure file.
136func ts_hash(s: *u8) -> i64 {
137 var h: i64 = TS_HASH_OFF
138 var i: i64 = 0
139 while s[i] != (0 as u8) {
140 h = h * TS_HASH_MUL
141 h = h + (s[i] as i64)
142 if h < 0 { h = 0 - h }
143 i = i + 1
144 }
145 if h < 0 { h = 0 - h }
146 if h == 0 { h = TS_HASH_PRIME }
147 return h
148}
149
150func ts_streq(a: *u8, b: *u8) -> i64 {
151 var i: i64 = 0
152 while a[i] != (0 as u8) {
153 if a[i] != b[i] { return 0 }
154 i = i + 1
155 }
156 if b[i] != (0 as u8) { return 0 }
157 return 1
158}
159
160// ---- marker table ---------------------------------------------------------------------------------
161// Returns 1 loaded, 0 REFUSED. A refusal is the fail-closed answer: no caller may proceed to publish a
162// quad without markers, because a scan with no markers matches nothing and reads exactly like a clean,
163// read-only organ.
164func ts_load_markers() -> i64 {
165 if TS_READY == 1 { return 1 }
166 let szp: *i64 = sys_mmap(16) as *i64
167 let buf: *u8 = sys_read_file(TS_MARKCONF, szp)
168 if (buf as i64) == 0 { return 0 }
169 let n: i64 = szp[0]
170 if n <= 0 { return 0 }
171 let mk: *u8 = sys_mmap(TS_CLASSES * TS_MAXMARK * TS_MARKCAP)
172 let mkn: *i64 = sys_mmap(TS_CLASSES * 8) as *i64
173 var c: i64 = 0
174 while c < TS_CLASSES { mkn[c] = 0; c = c + 1 }
175 var overflow: i64 = 0
176 var ls: i64 = 0
177 var i: i64 = 0
178 while i <= n {
179 var eol: i64 = 0
180 if i == n { eol = 1 } else { if buf[i] == (TS_LF as u8) { eol = 1 } }
181 if eol == 1 {
182 let le: i64 = i
183 var use: i64 = 1
184 if le <= ls { use = 0 }
185 if use == 1 { if buf[ls] == (TS_CH_HASH as u8) { use = 0 } }
186 if use == 1 {
187 var tab: i64 = 0 - 1
188 var q: i64 = ls
189 while q < le { if buf[q] == (TS_TAB as u8) { if tab < 0 { tab = q } } q = q + 1 }
190 if tab > 0 {
191 let kl: i64 = tab - ls
192 var cls: i64 = 0 - 1
193 if buf[ls] == (TS_CH_U as u8) { cls = 0 - 1 }
194 if kl == 5 { if buf[ls] == (119 as u8) { cls = TS_CW } }
195 if kl == 11 { if buf[ls] == (100 as u8) { cls = TS_CD } }
196 if kl == 7 { if buf[ls] == (TS_CH_N as u8) { cls = TS_CN } }
197 if kl == 4 { if buf[ls] == (101 as u8) { cls = TS_CX } }
198 if cls >= 0 {
199 let ml: i64 = le - tab - 1
200 if ml > 0 {
201 if ml >= TS_MARKCAP { overflow = 1 } else {
202 let idx: i64 = mkn[cls]
203 if idx >= TS_MAXMARK { overflow = 1 } else {
204 let base: i64 = (cls * TS_MAXMARK + idx) * TS_MARKCAP
205 var k: i64 = 0
206 while k < ml { mk[base + k] = buf[tab + 1 + k]; k = k + 1 }
207 mk[base + ml] = 0 as u8
208 mkn[cls] = idx + 1
209 }
210 }
211 }
212 }
213 }
214 }
215 ls = i + 1
216 }
217 i = i + 1
218 }
219 sys_free_file(buf, n)
220 if overflow == 1 { return 0 }
221 var empty: i64 = 0
222 c = 0
223 while c < TS_CLASSES { if mkn[c] == 0 { empty = 1 } c = c + 1 }
224 if empty == 1 { return 0 }
225 TS_MK_P = mk as i64
226 TS_MKN_P = mkn as i64
227 TS_READY = 1
228 return 1
229}
230
231func ts_marker_count(cls: i64) -> i64 {
232 if TS_READY == 0 { return 0 }
233 let mkn: *i64 = TS_MKN_P as *i64
234 return mkn[cls]
235}
236
237func ts_marker_at(cls: i64, idx: i64) -> *u8 {
238 let mk: *u8 = TS_MK_P as *u8
239 return ((mk as i64) + (cls * TS_MAXMARK + idx) * TS_MARKCAP) as *u8
240}
241
242// ---- the stripper ---------------------------------------------------------------------------------
243// Drops line comments AND string literals, so what remains is code. A DEFINITION and a CALL are the same
244// bytes and only their left context tells them apart, so ts_has handles that separately; a marker inside
245// PROSE or inside a LITERAL is not a reach at all and must not survive to be counted.
246// Newlines are PRESERVED for comments (line structure stays) and a newline INSIDE a string literal does
247// not close it -- this dialect permits one, and treating it as a terminator shifts the mask and dumps
248// real code into the dropped region.
249func ts_strip(src: *u8, n: i64, out: *u8) -> i64 {
250 var o: i64 = 0
251 var i: i64 = 0
252 var mode: i64 = 0
253 while i < n {
254 let c: i64 = src[i] as i64
255 if mode == 1 {
256 if c == TS_LF { mode = 0; out[o] = TS_LF as u8; o = o + 1 }
257 i = i + 1
258 } else {
259 if mode == 2 {
260 if c == TS_CH_BSLASH { i = i + 2 } else {
261 if c == TS_CH_QUOTE { mode = 0 }
262 i = i + 1
263 }
264 } else {
265 var starts: i64 = 0
266 if c == TS_CH_SLASH { if i + 1 < n { if src[i+1] == (TS_CH_SLASH as u8) { starts = 1 } } }
267 if starts == 1 { mode = 1; i = i + 2 } else {
268 if c == TS_CH_QUOTE { mode = 2; i = i + 1 } else {
269 out[o] = src[i]; o = o + 1; i = i + 1
270 }
271 }
272 }
273 }
274 }
275 return o
276}
277
278// A CALL, not a DEFINITION. Every organ imports the syscall shim, which DEFINES func sys_openat_wr and
279// the rest; counting a definition marks the whole estate a writer the instant imports are folded in.
280// Occurrences immediately preceded by the five bytes of "func " are declarations and are skipped. The
281// rule is structural, so no filename is special-cased and it holds for any future shim.
282func ts_has(buf: *u8, n: i64, needle: *u8) -> i64 {
283 let m: i64 = ts_slen(needle)
284 if m == 0 { return 0 }
285 var i: i64 = 0
286 while i + m <= n {
287 var j: i64 = 0
288 var hit: i64 = 1
289 while j < m { if buf[i+j] != needle[j] { hit = 0; j = m } else { j = j + 1 } }
290 if hit == 1 {
291 var pfx: i64 = 0
292 if i >= TS_FUNCPFX {
293 if (buf[i-5] as i64) == TS_CH_F { pfx = pfx + 1 }
294 if (buf[i-4] as i64) == TS_CH_U { pfx = pfx + 1 }
295 if (buf[i-3] as i64) == TS_CH_N { pfx = pfx + 1 }
296 if (buf[i-2] as i64) == TS_CH_C { pfx = pfx + 1 }
297 if (buf[i-1] as i64) == TS_CH_SPACE { pfx = pfx + 1 }
298 }
299 if pfx < TS_FUNCPFX { return 1 }
300 }
301 i = i + 1
302 }
303 return 0
304}
305
306// box[TS_CW..TS_CX] are ORed into, never cleared: the caller accumulates across a closure.
307func ts_scan_buf(buf: *u8, n: i64, box: *i64) -> i64 {
308 var cls: i64 = 0
309 while cls < TS_CLASSES {
310 if box[cls] == 0 {
311 let cnt: i64 = ts_marker_count(cls)
312 var k: i64 = 0
313 while k < cnt {
314 if ts_has(buf, n, ts_marker_at(cls, k)) == 1 { box[cls] = 1; k = cnt } else { k = k + 1 }
315 }
316 }
317 cls = cls + 1
318 }
319 return 0
320}
321
322// Collect import module names out of the RAW bytes into a NUL-terminated, LF-separated block. Parsed
323// from raw on purpose: the stripper removes the very literal the module name lives in, and a
324// commented-out import counted as present errs toward MORE files scanned, which is the safe direction.
325func ts_imports_buf(buf: *u8, n: i64, out: *u8) -> i64 {
326 var o: i64 = 0
327 var i: i64 = 0
328 while i + TS_IMPPFX < n {
329 var m: i64 = 1
330 if (buf[i] as i64) != TS_CH_I { m = 0 }
331 if m == 1 { if (buf[i+1] as i64) != TS_CH_M { m = 0 } }
332 if m == 1 { if (buf[i+2] as i64) != TS_CH_P { m = 0 } }
333 if m == 1 { if (buf[i+3] as i64) != TS_CH_O { m = 0 } }
334 if m == 1 { if (buf[i+4] as i64) != TS_CH_R { m = 0 } }
335 if m == 1 { if (buf[i+5] as i64) != TS_CH_T { m = 0 } }
336 if m == 1 { if (buf[i+6] as i64) != TS_CH_SPACE { m = 0 } }
337 if m == 1 { if (buf[i+7] as i64) != TS_CH_QUOTE { m = 0 } }
338 if m == 1 {
339 var p: i64 = i + TS_IMPPFX
340 var st: i64 = o
341 var go: i64 = 1
342 while go == 1 {
343 if p >= n { go = 0 } else {
344 if buf[p] == (TS_CH_QUOTE as u8) { go = 0 } else {
345 if o < TS_IMPBLK - 2 { out[o] = buf[p]; o = o + 1 }
346 p = p + 1
347 }
348 }
349 }
350 if o > st { out[o] = TS_LF as u8; o = o + 1 }
351 i = p + 1
352 } else { i = i + 1 }
353 }
354 out[o] = 0 as u8
355 return o
356}
357
358func ts_resolve(mod: *u8, out: *u8) -> i64 {
359 var o: i64 = ts_cat(out, 0, TS_ROOT_HDL)
360 o = ts_cat(out, o, mod)
361 out[o] = 0 as u8
362 var fd: i64 = sys_openat_rd(out)
363 if fd >= 0 { sys_close(fd); return 1 }
364 o = ts_cat(out, 0, TS_ROOT_RT)
365 o = ts_cat(out, o, mod)
366 out[o] = 0 as u8
367 fd = sys_openat_rd(out)
368 if fd >= 0 { sys_close(fd); return 1 }
369 return 0
370}
371
372func ts_memo_init() -> i64 {
373 if TS_MEMO_K != 0 { return 1 }
374 TS_MEMO_K = sys_mmap(TS_MEMOSLOTS * 8) as i64
375 TS_MEMO_W = sys_mmap(TS_MEMOSLOTS * 8) as i64
376 TS_MEMO_D = sys_mmap(TS_MEMOSLOTS * 8) as i64
377 TS_MEMO_N = sys_mmap(TS_MEMOSLOTS * 8) as i64
378 TS_MEMO_X = sys_mmap(TS_MEMOSLOTS * 8) as i64
379 TS_MEMO_OK = sys_mmap(TS_MEMOSLOTS * 8) as i64
380 TS_MEMO_PATH = sys_mmap(TS_MEMOSLOTS * TS_PATHCAP) as i64
381 TS_MEMO_IMP = sys_mmap(TS_MEMOSLOTS * TS_IMPBLK) as i64
382 TS_QUEUE_P = sys_mmap(TS_MAXWALK * TS_PATHCAP) as i64
383 TS_QDEPTH_P = sys_mmap(TS_MAXWALK * 8) as i64
384 TS_SEEN_P = sys_mmap(TS_MAXWALK * 8) as i64
385 TS_SCRATCH_P = sys_mmap(TS_PATHCAP) as i64
386 return 1
387}
388
389// Slot for path, scanning the file on first sight. Returns the slot index, or -1 if the memo is FULL.
390// A full memo is INCONCLUSIVE and is reported; it is never allowed to degrade into a fresh scan whose
391// result silently is not cached, because that turns a budget into a performance cliff nobody can see.
392func ts_slot(path: *u8) -> i64 {
393 ts_memo_init()
394 let keys: *i64 = TS_MEMO_K as *i64
395 let paths: *u8 = TS_MEMO_PATH as *u8
396 let h: i64 = ts_hash(path)
397 var probe: i64 = h - ((h / TS_MEMOSLOTS) * TS_MEMOSLOTS)
398 var tries: i64 = 0
399 var slot: i64 = 0 - 1
400 while tries < TS_MEMOSLOTS {
401 if keys[probe] == 0 { slot = probe; tries = TS_MEMOSLOTS } else {
402 if keys[probe] == h {
403 let cand: *u8 = ((paths as i64) + probe * TS_PATHCAP) as *u8
404 if ts_streq(cand, path) == 1 { return probe }
405 }
406 probe = probe + 1
407 if probe >= TS_MEMOSLOTS { probe = 0 }
408 tries = tries + 1
409 }
410 }
411 if slot < 0 { TS_MEMO_FULL = 1; return 0 - 1 }
412
413 let fw: *i64 = TS_MEMO_W as *i64
414 let fd2: *i64 = TS_MEMO_D as *i64
415 let fn2: *i64 = TS_MEMO_N as *i64
416 let fx: *i64 = TS_MEMO_X as *i64
417 let fok: *i64 = TS_MEMO_OK as *i64
418 let imps: *u8 = TS_MEMO_IMP as *u8
419 keys[slot] = h
420 var pc: i64 = 0
421 while path[pc] != (0 as u8) {
422 if pc < TS_PATHCAP - 1 { paths[slot * TS_PATHCAP + pc] = path[pc] }
423 pc = pc + 1
424 }
425 paths[slot * TS_PATHCAP + pc] = 0 as u8
426 fw[slot] = 0
427 fd2[slot] = 0
428 fn2[slot] = 0
429 fx[slot] = 0
430 fok[slot] = 0
431 imps[slot * TS_IMPBLK] = 0 as u8
432
433 let szp: *i64 = sys_mmap(16) as *i64
434 let raw: *u8 = sys_read_file(path, szp)
435 if (raw as i64) == 0 { return slot }
436 let rn: i64 = szp[0]
437 if rn <= 0 { sys_free_file(raw, rn); return slot }
438 let stripped: *u8 = sys_mmap(rn + 1)
439 let sn: i64 = ts_strip(raw, rn, stripped)
440 let box: *i64 = sys_mmap(TS_CLASSES * 8) as *i64
441 var c: i64 = 0
442 while c < TS_CLASSES { box[c] = 0; c = c + 1 }
443 ts_scan_buf(stripped, sn, box)
444 let impout: *u8 = ((imps as i64) + slot * TS_IMPBLK) as *u8
445 ts_imports_buf(raw, rn, impout)
446 fw[slot] = box[TS_CW]
447 fd2[slot] = box[TS_CD]
448 fn2[slot] = box[TS_CN]
449 fx[slot] = box[TS_CX]
450 fok[slot] = 1
451 TS_STAT_FILES = TS_STAT_FILES + 1
452 TS_STAT_BYTES = TS_STAT_BYTES + rn
453 sys_munmap(stripped, rn + 1)
454 sys_free_file(raw, rn)
455 TS_MEMO_USED = TS_MEMO_USED + 1
456 return slot
457}
458
459// THE RULER. maxdepth 0 = the organ's own source only (what the single-file deriver sees); 1 = one level
460// of imports (what nx_toolflags sees); TS_DEPTH_ALL = the whole closure.
461// box[0]=write box[1]=destructive box[2]=network box[3]=exec
462// box[4]=files_visited box[5]=unresolved_imports box[6]=budget_hit box[7]=subject_readable
463// Returns 1 CONCLUSIVE, 0 INCONCLUSIVE (subject unreadable, markers unloadable, or a budget was hit).
464func ts_reach(path: *u8, maxdepth: i64, box: *i64) -> i64 {
465 var z: i64 = 0
466 while z < 8 { box[z] = 0; z = z + 1 }
467 TS_WSRC = 0
468 TS_WSRC_OK = 0
469 if ts_load_markers() == 0 { return 0 }
470 ts_memo_init()
471 let q: *u8 = TS_QUEUE_P as *u8
472 let qd: *i64 = TS_QDEPTH_P as *i64
473 let seen: *i64 = TS_SEEN_P as *i64
474 let scratch: *u8 = TS_SCRATCH_P as *u8
475 let fw: *i64 = TS_MEMO_W as *i64
476 let fdd: *i64 = TS_MEMO_D as *i64
477 let fnn: *i64 = TS_MEMO_N as *i64
478 let fxx: *i64 = TS_MEMO_X as *i64
479 let fok: *i64 = TS_MEMO_OK as *i64
480 let imps: *u8 = TS_MEMO_IMP as *u8
481
482 var qn: i64 = 1
483 var head: i64 = 0
484 var sn2: i64 = 1
485 var pc: i64 = 0
486 while path[pc] != (0 as u8) { q[pc] = path[pc]; pc = pc + 1 }
487 q[pc] = 0 as u8
488 qd[0] = 0
489 seen[0] = ts_hash(path)
490 var conclusive: i64 = 1
491
492 while head < qn {
493 let cur: *u8 = ((q as i64) + head * TS_PATHCAP) as *u8
494 let depth: i64 = qd[head]
495 let slot: i64 = ts_slot(cur)
496 if slot < 0 { conclusive = 0; box[6] = 1 } else {
497 if fok[slot] == 0 {
498 if head == 0 { conclusive = 0 } else { box[5] = box[5] + 1 }
499 } else {
500 if head == 0 { box[7] = 1 }
501 box[4] = box[4] + 1
502 if TS_TRACE == 1 {
503 sys_write(1, " visit " as *u8, 10)
504 var tl: i64 = ts_slen(cur)
505 sys_write(1, cur, tl)
506 if fw[slot] == 1 { sys_write(1, " [WRITE]" as *u8, 9) }
507 if fdd[slot] == 1 { sys_write(1, " [DESTR]" as *u8, 9) }
508 if fnn[slot] == 1 { sys_write(1, " [NET]" as *u8, 7) }
509 sys_write(1, "\n" as *u8, 1)
510 }
511 if fw[slot] == 1 { if box[TS_CW] == 0 { TS_WSRC = slot; TS_WSRC_OK = 1 } box[TS_CW] = 1 }
512 if fdd[slot] == 1 { box[TS_CD] = 1 }
513 if fnn[slot] == 1 { box[TS_CN] = 1 }
514 if fxx[slot] == 1 { box[TS_CX] = 1 }
515 var descend: i64 = 0
516 if maxdepth == TS_DEPTH_ALL { descend = 1 } else { if depth < maxdepth { descend = 1 } }
517 if descend == 1 {
518 let blk: *u8 = ((imps as i64) + slot * TS_IMPBLK) as *u8
519 var bi: i64 = 0
520 var ms: i64 = 0
521 while blk[bi] != (0 as u8) {
522 if blk[bi] == (TS_LF as u8) {
523 var ml: i64 = bi - ms
524 if ml > 0 {
525 var k: i64 = 0
526 let modn: *u8 = sys_mmap(TS_PATHCAP)
527 while k < ml { modn[k] = blk[ms + k]; k = k + 1 }
528 modn[ml] = 0 as u8
529 if ts_resolve(modn, scratch) == 1 {
530 let hh: i64 = ts_hash(scratch)
531 var dup: i64 = 0
532 var si: i64 = 0
533 // seen[] and the queue grow in lockstep, so index si names the same
534 // path in both. VERIFY THE STRING: a hash-only visited-set turns a
535 // collision into a dropped closure file, and it drops it silently.
536 while si < sn2 {
537 if seen[si] == hh {
538 let prev: *u8 = ((q as i64) + si * TS_PATHCAP) as *u8
539 if ts_streq(prev, scratch) == 1 { dup = 1; si = sn2 } else { si = si + 1 }
540 } else { si = si + 1 }
541 }
542 if dup == 0 {
543 if qn >= TS_MAXWALK { conclusive = 0; box[6] = 1 } else {
544 let dst: *u8 = ((q as i64) + qn * TS_PATHCAP) as *u8
545 var cc: i64 = 0
546 while scratch[cc] != (0 as u8) { dst[cc] = scratch[cc]; cc = cc + 1 }
547 dst[cc] = 0 as u8
548 qd[qn] = depth + 1
549 seen[sn2] = hh
550 sn2 = sn2 + 1
551 qn = qn + 1
552 }
553 }
554 } else { box[5] = box[5] + 1 }
555 sys_munmap(modn, TS_PATHCAP)
556 }
557 ms = bi + 1
558 }
559 bi = bi + 1
560 }
561 }
562 }
563 }
564 head = head + 1
565 }
566 if box[7] == 0 { conclusive = 0 }
567 return conclusive
568}
569
570// The published quad, in the SAME per-flag depth policy the shipping deriver chose, and for the same
571// reason: readOnly takes the DEEPEST view because claiming read-only wrongly is the unsafe error, while
572// destructive and openWorld take the organ's OWN source because a fat lib imported for one helper drags
573// an unlink somewhere into every closure and a warning that fires on everything carries no signal.
574// PICK THE CONSERVATIVE SIDE PER FLAG, NOT PER SCAN -- one depth for all four forces a choice between a
575// lie and a blindfold. quad[0]=readOnly quad[1]=destructive quad[2]=idempotent quad[3]=openWorld.
576// idempotent is UNDECLARED (0) and never guessed: nothing in a source declares it.
577// Returns 1 CONCLUSIVE, 0 INCONCLUSIVE -- and an INCONCLUSIVE quad must not be published at all.
578func ts_quad(path: *u8, quad: *i64, deep: *i64, own: *i64) -> i64 {
579 let okd: i64 = ts_reach(path, TS_DEPTH_ALL, deep)
580 let oko: i64 = ts_reach(path, 0, own)
581 if okd == 0 { return 0 }
582 if oko == 0 { return 0 }
583 if deep[TS_CW] == 0 { quad[0] = 1 } else { quad[0] = 0 }
584 quad[1] = own[TS_CD]
585 quad[2] = 0
586 quad[3] = own[TS_CN]
587 return 1
588}
589
590func ts_set_trace(v: i64) -> i64 { TS_TRACE = v; return v }
591
592// The closure file that supplied the write marker for the last ts_reach, copied into out. Returns 1 when
593// there was one, 0 when the subject reached no writer at all. ATTRIBUTION IS THE DIFFERENCE BETWEEN A
594// COUNT AND A CAMPAIGN: 124 rows that all name ONE shared lib are ONE decision, not 124.
595func ts_write_src(out: *u8) -> i64 {
596 out[0] = 0 as u8
597 if TS_WSRC_OK == 0 { return 0 }
598 let paths: *u8 = TS_MEMO_PATH as *u8
599 let src: *u8 = ((paths as i64) + TS_WSRC * TS_PATHCAP) as *u8
600 var i: i64 = 0
601 while src[i] != (0 as u8) { out[i] = src[i]; i = i + 1 }
602 out[i] = 0 as u8
603 return 1
604}
605
606func ts_stat_files() -> i64 { return TS_STAT_FILES }
607func ts_stat_bytes() -> i64 { return TS_STAT_BYTES }
608func ts_memo_full() -> i64 { return TS_MEMO_FULL }