code wiki / (root) / nx_dir.nx

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}