path.nx source
↩ module page · 219 lines · 6559 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
20import "syscalls.nx"
21
22const PATH_ERR_SHORT: i64 = -1
23
24// Return index of LAST '/' in path[0..n], or -1 if none.
25func path_last_slash(path: *u8, n: i64) -> i64 {
26 var i: i64 = n - 1
27 while i >= 0 {
28 if path[i] == 0x2F { return i }
29 i = i - 1
30 }
31 return -1
32}
33
34// Strip trailing slashes (but leave root \"/\" alone). Returns
35// new effective length.
36func path_trim_trailing(path: *u8, n: i64) -> i64 {
37 var e: i64 = n
38 while e > 1 {
39 if path[e - 1] != 0x2F { break }
40 e = e - 1
41 }
42 return e
43}
44
45// basename(path) -> offset + length. \"/a/b/c\" -> \"c\";
46// \"/a/b/\" -> \"b\" (after trailing-slash trim).
47func path_basename(path: *u8, n: i64,
48 off_out: *i64, len_out: *i64) -> i64 {
49 let eff: i64 = path_trim_trailing(path, n)
50 if eff == 0 {
51 *off_out = 0
52 *len_out = 0
53 return 0
54 }
55 let slash: i64 = path_last_slash(path, eff)
56 if slash < 0 {
57 *off_out = 0
58 *len_out = eff
59 } else {
60 *off_out = slash + 1
61 *len_out = eff - slash - 1
62 }
63 return 0
64}
65
66// dirname(path) -> offset + length.
67// \"/a/b/c\" -> \"/a/b\"
68// \"/a\" -> \"/\"
69// \"a\" -> \".\" (via synthesised constant below)
70// \"\" -> \".\"
71func path_dirname(path: *u8, n: i64,
72 off_out: *i64, len_out: *i64,
73 synth_out: *u8) -> i64 {
74 let eff: i64 = path_trim_trailing(path, n)
75 if eff == 0 {
76 synth_out[0] = 0x2E // '.'
77 *off_out = -1 // signals \"use synth_out\"
78 *len_out = 1
79 return 0
80 }
81 let slash: i64 = path_last_slash(path, eff)
82 if slash < 0 {
83 synth_out[0] = 0x2E
84 *off_out = -1
85 *len_out = 1
86 return 0
87 }
88 if slash == 0 {
89 // Root.
90 synth_out[0] = 0x2F
91 *off_out = -1
92 *len_out = 1
93 return 0
94 }
95 *off_out = 0
96 *len_out = slash
97 return 0
98}
99
100// extension(path) -> (offset, length) of the final \".ext\" including
101// the dot, or length 0 if none. Recognises only extensions AFTER
102// the basename's last '/' so \"/a.b/c\" has no extension.
103func path_extension(path: *u8, n: i64,
104 off_out: *i64, len_out: *i64) -> i64 {
105 let bn_off_slot: *i64 = (sys_mmap(16)) as *i64
106 let bn_len_slot: *i64 = (sys_mmap(16)) as *i64
107 path_basename(path, n, bn_off_slot, bn_len_slot)
108 let bn_off: i64 = *bn_off_slot
109 let bn_len: i64 = *bn_len_slot
110 var i: i64 = bn_off + bn_len - 1
111 while i >= bn_off {
112 if path[i] == 0x2E {
113 if i > bn_off { // leading dot doesn't count
114 *off_out = i
115 *len_out = bn_off + bn_len - i
116 return 0
117 }
118 }
119 i = i - 1
120 }
121 *off_out = 0
122 *len_out = 0
123 return 0
124}
125
126// Is path absolute?
127func path_is_absolute(path: *u8, n: i64) -> i64 {
128 if n == 0 { return 0 }
129 if path[0] == 0x2F { return 1 }
130 return 0
131}
132
133// Join two paths into out. If `b` is absolute it replaces `a`.
134// Otherwise inserts \"/\" separator if needed.
135func path_join(a: *u8, a_len: i64,
136 b: *u8, b_len: i64,
137 out: *u8, cap: i64) -> i64 {
138 if path_is_absolute(b, b_len) == 1 {
139 if cap < b_len { return PATH_ERR_SHORT }
140 var i: i64 = 0
141 while i < b_len { out[i] = b[i]; i = i + 1 }
142 return b_len
143 }
144 if a_len == 0 {
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 var need_sep: i64 = 1
151 if a[a_len - 1] == 0x2F { need_sep = 0 }
152
153 let total: i64 = a_len + need_sep + b_len
154 if cap < total { return PATH_ERR_SHORT }
155 var i: i64 = 0
156 while i < a_len { out[i] = a[i]; i = i + 1 }
157 if need_sep == 1 {
158 out[a_len] = 0x2F
159 }
160 var j: i64 = 0
161 while j < b_len {
162 out[a_len + need_sep + j] = b[j]
163 j = j + 1
164 }
165 return total
166}
167
168// Compile-only smoke.
169func main() -> i64 {
170 let off: *i64 = (sys_mmap(16)) as *i64
171 let len: *i64 = (sys_mmap(16)) as *i64
172 let synth: *u8 = sys_mmap(16)
173
174 // basename.
175 path_basename("/home/elder/notes.md", 20, off, len)
176 if *off != 12 { return 1 }
177 if *len != 8 { return 2 } // "notes.md"
178
179 // basename with trailing slash.
180 path_basename("/a/b/", 5, off, len)
181 if *len != 1 { return 3 } // "b"
182
183 // dirname.
184 path_dirname("/home/elder/notes.md", 20, off, len, synth)
185 if *off != 0 { return 4 }
186 if *len != 11 { return 5 } // "/home/elder"
187
188 // dirname for root.
189 path_dirname("/a", 2, off, len, synth)
190 if *off != -1 { return 6 }
191 if *len != 1 { return 7 }
192 if synth[0] != 0x2F { return 8 }
193
194 // extension.
195 path_extension("/a/b.txt", 8, off, len)
196 if *off != 4 { return 9 }
197 if *len != 4 { return 10 } // ".txt"
198
199 // extension missing.
200 path_extension("/a/b", 4, off, len)
201 if *len != 0 { return 11 }
202
203 // is_absolute.
204 if path_is_absolute("/a", 2) != 1 { return 12 }
205 if path_is_absolute("a", 1) != 0 { return 13 }
206
207 // join.
208 let out: *u8 = sys_mmap(64)
209 let jn: i64 = path_join("/a", 2, "b/c", 3, out, 64)
210 if jn != 6 { return 14 } // "/a/b/c"
211 if out[2] != 0x2F { return 15 }
212
213 // join absolute right -> replaces.
214 let jn2: i64 = path_join("/a", 2, "/x", 2, out, 64)
215 if jn2 != 2 { return 16 }
216 if out[0] != 0x2F { return 17 }
217 if out[1] != 0x78 { return 18 }
218 return 0
219}