nx_dir.nx source
↩ module page · 321 lines · 11265 B
1// nx_dir.nx -- higher-level directory walker over nx_dirent.
2//
3// module: nishi-core.io.dir
4// depends: nishi-core.io.dirent, nishi-core.io.fcntl,
5// nishi-core.io.syscalls
6// disk_kb: 4
7// capability: IO
8// wired_status: FULLY_WIRED
9//
10// license_tier: PUBLIC_NISHI_SUBSTRATE
11// genealogy_id: posix_1003_1_2017_readdir_pattern +
12// nishi_no_skipping_build_the_missing_dependency_cardinal +
13// nishi_bounded_loop_jpl_rule_2_cardinal
14//
15// Per cardinal [[feedback-no-skipping-build-the-missing-dependency]]:
16// nx_dirent.nx exposes raw getdents64 batch + iter primitives. The
17// honesty audits (nx_claim_audit / nx_adversarial_pattern_audit /
18// nx_canary_value whole-file scan) all wanted a per-file callback
19// abstraction over a directory. This primitive IS that brick.
20//
21// Per cardinal [[feedback-bounded-loop-discipline-jpl-rule-2]]:
22// every loop here has an explicit BUDGET ceiling + sealed
23// LoopVerdict. No sentinel-flag hacks.
24
25// Note: nx_fcntl + nx_dirent both import "syscalls.nx"; we match
26// them to avoid double-defining sys_read_file etc. at link time.
27import "syscalls.nx"
28import "nx_fcntl.nx"
29import "nx_dirent.nx"
30
31// ===== Verdict ====================================================
32
33const NX_DIR_OK: i64 = 1
34const NX_DIR_EMPTY: i64 = 2 // directory has no entries (besides . and ..)
35const NX_DIR_TRUNCATED: i64 = 3 // entries omitted because out_capacity reached
36const NX_DIR_NAME_ARENA_EXHAUSTED: i64 = 4 // copy aborted before full enumeration
37const NX_DIR_OPEN_FAILED: i64 = 5
38const NX_DIR_GETDENTS_FAILED: i64 = 6
39const NX_DIR_BAD_ARGS: i64 = 7
40
41func nx_dir_verdict_name(v: i64) -> *u8 {
42 if v == NX_DIR_OK { return "OK" }
43 if v == NX_DIR_EMPTY { return "EMPTY" }
44 if v == NX_DIR_TRUNCATED { return "TRUNCATED" }
45 if v == NX_DIR_NAME_ARENA_EXHAUSTED { return "NAME_ARENA_EXHAUSTED" }
46 if v == NX_DIR_OPEN_FAILED { return "OPEN_FAILED" }
47 if v == NX_DIR_GETDENTS_FAILED { return "GETDENTS_FAILED" }
48 if v == NX_DIR_BAD_ARGS { return "BAD_ARGS" }
49 return "UNKNOWN"
50}
51
52// ===== Bounded-loop budgets =======================================
53
54const NX_DIR_GETDENTS_BUF_BYTES: i64 = 8192
55const NX_DIR_MAX_READ_ROUNDS: i64 = 16384 // 16384 * 8KiB = 128MiB worth of dir bytes
56const NX_DIR_MAX_ENTRIES_PER_BATCH: i64 = 65536 // upper bound per nx_dirent_iter walk
57
58// ===== NxDirRow (caller-facing single-row result) =================
59//
60// Names are copied into caller-supplied name_arena (NUL-terminated).
61// name_ptr remains valid as long as name_arena is held.
62
63struct NxDirRow {
64 name_ptr: *u8,
65 name_len: i64,
66 dtype: i64, // NX_DT_REG / NX_DT_DIR / NX_DT_LNK / NX_DT_UNKNOWN
67 is_dotlike: i64, // 1 for "." or ".."
68}
69
70const NX_DIR_ROW_BYTES: i64 = 32
71
72// ===== Per-entry result struct (returned via out_rows array) ======
73
74struct NxDirResult {
75 verdict: i64, // NX_DIR_*
76 n_filled: i64,
77 n_skipped: i64, // dot-like entries skipped if filter set
78 name_bytes_used: i64,
79 fd: i64, // already-closed; left for callers wanting forensics
80}
81
82const NX_DIR_RESULT_BYTES: i64 = 40
83
84// ===== Internal name-copy helper ==================================
85
86func nx_dir_name_strlen(name: *u8) -> i64 {
87 var n: i64 = 0
88 var iter: i64 = 0
89 var verdict: i64 = 0
90 while verdict == 0 && iter < NX_DIR_MAX_ENTRIES_PER_BATCH {
91 if name[n] == 0 { verdict = 1 }
92 if verdict == 0 {
93 n = n + 1
94 iter = iter + 1
95 }
96 }
97 return n
98}
99
100// dotlike: ".\0" or "..\0"
101func nx_dir_is_dotlike(name: *u8) -> i64 {
102 if name[0] != 0x2E { return 0 } // not '.'
103 if name[1] == 0 { return 1 } // "."
104 if name[1] == 0x2E {
105 if name[2] == 0 { return 1 } // ".."
106 }
107 return 0
108}
109
110// Copy name + NUL into arena at arena_off. Returns new arena_off, or
111// -1 if it would overflow capacity.
112func nx_dir_arena_copy_name(
113 src: *u8,
114 src_len: i64,
115 arena: *u8,
116 arena_cap: i64,
117 arena_off: i64
118) -> i64 {
119 let needed: i64 = src_len + 1
120 if arena_off + needed > arena_cap { return -1 }
121 var i: i64 = 0
122 var iter: i64 = 0
123 var verdict: i64 = 0
124 while verdict == 0 && iter < NX_DIR_MAX_ENTRIES_PER_BATCH {
125 if i >= src_len { verdict = 1 }
126 if verdict == 0 {
127 arena[arena_off + i] = src[i]
128 i = i + 1
129 iter = iter + 1
130 }
131 }
132 arena[arena_off + src_len] = 0
133 return arena_off + needed
134}
135
136// ===== nx_dir_list -- enumerate one directory =====================
137//
138// Opens dir_path (NUL-terminated), reads all entries in bounded
139// batches, copies each name into name_arena, fills out_rows[0..n] up
140// to out_capacity. Sets fields on out_result. Closes fd before
141// returning regardless of verdict.
142//
143// include_dotlike == 0 => "." and ".." are skipped (counted in
144// n_skipped). include_dotlike == 1 => they are emitted.
145
146func nx_dir_list(
147 dir_path: *u8,
148 out_rows: *NxDirRow,
149 out_capacity: i64,
150 name_arena: *u8,
151 name_arena_cap: i64,
152 include_dotlike: i64,
153 out_result: *NxDirResult
154) -> i64 {
155 if out_result == 0 as *NxDirResult { return NX_DIR_BAD_ARGS }
156 out_result.verdict = NX_DIR_BAD_ARGS
157 out_result.n_filled = 0
158 out_result.n_skipped = 0
159 out_result.name_bytes_used = 0
160 out_result.fd = -1
161
162 if dir_path == 0 as *u8 { return NX_DIR_BAD_ARGS }
163 if out_rows == 0 as *NxDirRow { return NX_DIR_BAD_ARGS }
164 if out_capacity <= 0 { return NX_DIR_BAD_ARGS }
165 if name_arena == 0 as *u8 { return NX_DIR_BAD_ARGS }
166 if name_arena_cap <= 0 { return NX_DIR_BAD_ARGS }
167
168 let fd: i64 = nx_openat(NX_AT_FDCWD, dir_path,
169 NX_O_RDONLY | NX_O_DIRECTORY, 0)
170 if fd < 0 {
171 out_result.verdict = NX_DIR_OPEN_FAILED
172 return NX_DIR_OPEN_FAILED
173 }
174 out_result.fd = fd
175
176 let buf: *u8 = sys_mmap(NX_DIR_GETDENTS_BUF_BYTES)
177 let dr_raw: *u8 = sys_mmap(NX_DIRENT_BYTES)
178 let dr: *NxDirent = dr_raw as *NxDirent
179
180 var n_filled: i64 = 0
181 var n_skipped: i64 = 0
182 var arena_off: i64 = 0
183 var truncated: i64 = 0
184 var name_arena_exhausted: i64 = 0
185
186 var read_round: i64 = 0
187 var outer_verdict: i64 = 0
188 while outer_verdict == 0 && read_round < NX_DIR_MAX_READ_ROUNDS {
189 let n: i64 = nx_dirent_read(fd, buf, NX_DIR_GETDENTS_BUF_BYTES)
190 if n < 0 {
191 sys_close(fd)
192 out_result.verdict = NX_DIR_GETDENTS_FAILED
193 out_result.n_filled = n_filled
194 out_result.n_skipped = n_skipped
195 out_result.name_bytes_used = arena_off
196 return NX_DIR_GETDENTS_FAILED
197 }
198 if n == 0 { outer_verdict = 1 }
199 if outer_verdict == 0 {
200 var off: i64 = 0
201 var inner_iter: i64 = 0
202 var inner_verdict: i64 = 0
203 while inner_verdict == 0 && inner_iter < NX_DIR_MAX_ENTRIES_PER_BATCH {
204 let next_off: i64 = nx_dirent_iter(buf, off, n, dr)
205 if next_off < 0 { inner_verdict = 1 }
206 if inner_verdict == 0 {
207 let dot: i64 = nx_dir_is_dotlike(dr.name)
208 var emit: i64 = 1
209 if dot == 1 {
210 if include_dotlike == 0 { emit = 0 }
211 }
212 if emit == 0 { n_skipped = n_skipped + 1 }
213 if emit == 1 {
214 if n_filled >= out_capacity {
215 truncated = 1
216 inner_verdict = 1
217 outer_verdict = 1
218 }
219 if inner_verdict == 0 {
220 let nl: i64 = nx_dir_name_strlen(dr.name)
221 let new_off: i64 = nx_dir_arena_copy_name(
222 dr.name, nl, name_arena, name_arena_cap, arena_off)
223 if new_off < 0 {
224 name_arena_exhausted = 1
225 inner_verdict = 1
226 outer_verdict = 1
227 }
228 if inner_verdict == 0 {
229 let row_p: i64 = (out_rows as i64) + n_filled * NX_DIR_ROW_BYTES
230 let row: *NxDirRow = row_p as *NxDirRow
231 row.name_ptr = (name_arena as i64 + arena_off) as *u8
232 row.name_len = nl
233 row.dtype = dr.dtype
234 row.is_dotlike = dot
235 arena_off = new_off
236 n_filled = n_filled + 1
237 }
238 }
239 }
240 off = next_off
241 inner_iter = inner_iter + 1
242 }
243 }
244 }
245 read_round = read_round + 1
246 }
247
248 sys_close(fd)
249 out_result.n_filled = n_filled
250 out_result.n_skipped = n_skipped
251 out_result.name_bytes_used = arena_off
252
253 if truncated == 1 {
254 out_result.verdict = NX_DIR_TRUNCATED
255 return NX_DIR_TRUNCATED
256 }
257 if name_arena_exhausted == 1 {
258 out_result.verdict = NX_DIR_NAME_ARENA_EXHAUSTED
259 return NX_DIR_NAME_ARENA_EXHAUSTED
260 }
261 if n_filled == 0 {
262 out_result.verdict = NX_DIR_EMPTY
263 return NX_DIR_EMPTY
264 }
265 out_result.verdict = NX_DIR_OK
266 return NX_DIR_OK
267}
268
269// ===== Filename-suffix filter helper ==============================
270//
271// Returns 1 if name (length name_len) ends with suffix (length
272// suffix_len); 0 otherwise. Substrate-wide convenience for
273// "*.nx" / "*.toml" / "*.jsonl" style filters at call sites.
274
275func nx_dir_name_ends_with(
276 name: *u8,
277 name_len: i64,
278 suffix: *u8,
279 suffix_len: i64
280) -> i64 {
281 if name_len < suffix_len { return 0 }
282 if suffix_len <= 0 { return 1 }
283 let start: i64 = name_len - suffix_len
284 var i: i64 = 0
285 var iter: i64 = 0
286 var verdict: i64 = 0
287 var is_match: i64 = 1
288 while verdict == 0 && iter < NX_DIR_MAX_ENTRIES_PER_BATCH {
289 if i >= suffix_len { verdict = 1 }
290 if verdict == 0 {
291 if name[start + i] != suffix[i] {
292 is_match = 0
293 verdict = 1
294 }
295 if verdict == 0 {
296 i = i + 1
297 iter = iter + 1
298 }
299 }
300 }
301 return is_match
302}
303
304// ===== Row accessor helpers =======================================
305
306func nx_dir_row_at(rows: *NxDirRow, idx: i64) -> *NxDirRow {
307 return ((rows as i64) + idx * NX_DIR_ROW_BYTES) as *NxDirRow
308}
309
310func nx_dir_row_is_regular_file(row: *NxDirRow) -> i64 {
311 if row == 0 as *NxDirRow { return 0 }
312 if row.dtype == NX_DT_REG { return 1 }
313 return 0
314}
315
316func nx_dir_row_is_subdirectory(row: *NxDirRow) -> i64 {
317 if row == 0 as *NxDirRow { return 0 }
318 if row.dtype == NX_DT_DIR { return 1 }
319 if row.is_dotlike == 1 { return 0 }
320 return 0
321}