nx_path_v1.nx source
↩ module page · 225 lines · 6594 B
1// path.nx -- POSIX filesystem path manipulation.
2//
3// Same operations as Python's posixpath / Node's path.posix /
4// Go's filepath: join / dirname / basename / extension / clean /
5// absolute-check. Byte-oriented (not Unicode-aware), which
6// matches how filesystems actually work.
7//
8// Forward-slash separator only. Windows backslashes fall outside
9// the sovereign-Linux target (Milk-V Pioneer + NishiOS).
10//
11// Invariants:
12// P1 Zero-alloc when possible: offsets into caller's buffer
13// for basename/dirname/extension. Only `path_join` writes
14// into a fresh output buffer.
15// P2 Empty-path handling matches POSIX: dirname(\"\")=\".\"
16// basename(\"\")=\"\" extension(\"\")=\"\".
17// P3 Trailing slashes: dirname(\"/a/b/\")=\"/a\" matches
18// POSIX; basename(\"/a/b/\")=\"b\" after trim.
19
20// nx_safety_envelope:
21// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
22// sil_target: SIL1
23// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
24// verdict: NOT_YET_EVALUATED
25
26import "nx_syscalls.nx"
27
28const PATH_ERR_SHORT: i64 = -1
29
30// Return index of LAST '/' in path[0..n], or -1 if none.
31func path_last_slash(path: *u8, n: i64) -> i64 {
32 var i: i64 = n - 1
33 while i >= 0 {
34 if path[i] == 0x2F { return i }
35 i = i - 1
36 }
37 return -1
38}
39
40// Strip trailing slashes (but leave root \"/\" alone). Returns
41// new effective length.
42func path_trim_trailing(path: *u8, n: i64) -> i64 {
43 var e: i64 = n
44 while e > 1 {
45 if path[e - 1] != 0x2F { break }
46 e = e - 1
47 }
48 return e
49}
50
51// basename(path) -> offset + length. \"/a/b/c\" -> \"c\";
52// \"/a/b/\" -> \"b\" (after trailing-slash trim).
53func path_basename(path: *u8, n: i64,
54 off_out: *i64, len_out: *i64) -> i64 {
55 let eff: i64 = path_trim_trailing(path, n)
56 if eff == 0 {
57 *off_out = 0
58 *len_out = 0
59 return 0
60 }
61 let slash: i64 = path_last_slash(path, eff)
62 if slash < 0 {
63 *off_out = 0
64 *len_out = eff
65 } else {
66 *off_out = slash + 1
67 *len_out = eff - slash - 1
68 }
69 return 0
70}
71
72// dirname(path) -> offset + length.
73// \"/a/b/c\" -> \"/a/b\"
74// \"/a\" -> \"/\"
75// \"a\" -> \".\" (via synthesised constant below)
76// \"\" -> \".\"
77func path_dirname(path: *u8, n: i64,
78 off_out: *i64, len_out: *i64,
79 synth_out: *u8) -> i64 {
80 let eff: i64 = path_trim_trailing(path, n)
81 if eff == 0 {
82 synth_out[0] = 0x2E // '.'
83 *off_out = -1 // signals \"use synth_out\"
84 *len_out = 1
85 return 0
86 }
87 let slash: i64 = path_last_slash(path, eff)
88 if slash < 0 {
89 synth_out[0] = 0x2E
90 *off_out = -1
91 *len_out = 1
92 return 0
93 }
94 if slash == 0 {
95 // Root.
96 synth_out[0] = 0x2F
97 *off_out = -1
98 *len_out = 1
99 return 0
100 }
101 *off_out = 0
102 *len_out = slash
103 return 0
104}
105
106// extension(path) -> (offset, length) of the final \".ext\" including
107// the dot, or length 0 if none. Recognises only extensions AFTER
108// the basename's last '/' so \"/a.b/c\" has no extension.
109func path_extension(path: *u8, n: i64,
110 off_out: *i64, len_out: *i64) -> i64 {
111 let bn_off_slot: *i64 = (sys_mmap(16)) as *i64
112 let bn_len_slot: *i64 = (sys_mmap(16)) as *i64
113 path_basename(path, n, bn_off_slot, bn_len_slot)
114 let bn_off: i64 = *bn_off_slot
115 let bn_len: i64 = *bn_len_slot
116 var i: i64 = bn_off + bn_len - 1
117 while i >= bn_off {
118 if path[i] == 0x2E {
119 if i > bn_off { // leading dot doesn't count
120 *off_out = i
121 *len_out = bn_off + bn_len - i
122 return 0
123 }
124 }
125 i = i - 1
126 }
127 *off_out = 0
128 *len_out = 0
129 return 0
130}
131
132// Is path absolute?
133func path_is_absolute(path: *u8, n: i64) -> i64 {
134 if n == 0 { return 0 }
135 if path[0] == 0x2F { return 1 }
136 return 0
137}
138
139// Join two paths into out. If `b` is absolute it replaces `a`.
140// Otherwise inserts \"/\" separator if needed.
141func path_join(a: *u8, a_len: i64,
142 b: *u8, b_len: i64,
143 out: *u8, cap: i64) -> i64 {
144 if path_is_absolute(b, b_len) == 1 {
145 if cap < b_len { return PATH_ERR_SHORT }
146 var i: i64 = 0
147 while i < b_len { out[i] = b[i]; i = i + 1 }
148 return b_len
149 }
150 if a_len == 0 {
151 if cap < b_len { return PATH_ERR_SHORT }
152 var i: i64 = 0
153 while i < b_len { out[i] = b[i]; i = i + 1 }
154 return b_len
155 }
156 var need_sep: i64 = 1
157 if a[a_len - 1] == 0x2F { need_sep = 0 }
158
159 let total: i64 = a_len + need_sep + b_len
160 if cap < total { return PATH_ERR_SHORT }
161 var i: i64 = 0
162 while i < a_len { out[i] = a[i]; i = i + 1 }
163 if need_sep == 1 {
164 out[a_len] = 0x2F
165 }
166 var j: i64 = 0
167 while j < b_len {
168 out[a_len + need_sep + j] = b[j]
169 j = j + 1
170 }
171 return total
172}
173
174// Compile-only smoke.
175func main() -> i64 {
176 let off: *i64 = (sys_mmap(16)) as *i64
177 let len: *i64 = (sys_mmap(16)) as *i64
178 let synth: *u8 = sys_mmap(16)
179
180 // basename.
181 path_basename("/home/elder/notes.md", 20, off, len)
182 if *off != 12 { return 1 }
183 if *len != 8 { return 2 } // "notes.md"
184
185 // basename with trailing slash.
186 path_basename("/a/b/", 5, off, len)
187 if *len != 1 { return 3 } // "b"
188
189 // dirname.
190 path_dirname("/home/elder/notes.md", 20, off, len, synth)
191 if *off != 0 { return 4 }
192 if *len != 11 { return 5 } // "/home/elder"
193
194 // dirname for root.
195 path_dirname("/a", 2, off, len, synth)
196 if *off != -1 { return 6 }
197 if *len != 1 { return 7 }
198 if synth[0] != 0x2F { return 8 }
199
200 // extension.
201 path_extension("/a/b.txt", 8, off, len)
202 if *off != 4 { return 9 }
203 if *len != 4 { return 10 } // ".txt"
204
205 // extension missing.
206 path_extension("/a/b", 4, off, len)
207 if *len != 0 { return 11 }
208
209 // is_absolute.
210 if path_is_absolute("/a", 2) != 1 { return 12 }
211 if path_is_absolute("a", 1) != 0 { return 13 }
212
213 // join.
214 let out: *u8 = sys_mmap(64)
215 let jn: i64 = path_join("/a", 2, "b/c", 3, out, 64)
216 if jn != 6 { return 14 } // "/a/b/c"
217 if out[2] != 0x2F { return 15 }
218
219 // join absolute right -> replaces.
220 let jn2: i64 = path_join("/a", 2, "/x", 2, out, 64)
221 if jn2 != 2 { return 16 }
222 if out[0] != 0x2F { return 17 }
223 if out[1] != 0x78 { return 18 }
224 return 0
225}