1 | module os |
2 | |
3 | import strings |
4 | import strings.textscanner |
5 | |
6 | // Collection of useful functions for manipulation, validation and analysis of system paths. |
7 | // The following functions handle paths depending on the operating system, |
8 | // therefore results may be different for certain operating systems. |
9 | |
10 | const ( |
11 | fslash = `/` |
12 | bslash = `\\` |
13 | dot = `.` |
14 | qmark = `?` |
15 | fslash_str = '/' |
16 | dot_dot = '..' |
17 | empty_str = '' |
18 | dot_str = '.' |
19 | ) |
20 | |
21 | // is_abs_path returns `true` if the given `path` is absolute. |
22 | pub fn is_abs_path(path string) bool { |
23 | if path.len == 0 { |
24 | return false |
25 | } |
26 | $if windows { |
27 | return is_unc_path(path) || is_drive_rooted(path) || is_normal_path(path) |
28 | } |
29 | return path[0] == os.fslash |
30 | } |
31 | |
32 | // abs_path joins the current working directory |
33 | // with the given `path` (if the `path` is relative) |
34 | // and returns the absolute path representation. |
35 | pub fn abs_path(path string) string { |
36 | wd := getwd() |
37 | if path.len == 0 { |
38 | return wd |
39 | } |
40 | npath := norm_path(path) |
41 | if npath == os.dot_str { |
42 | return wd |
43 | } |
44 | if !is_abs_path(npath) { |
45 | mut sb := strings.new_builder(npath.len) |
46 | sb.write_string(wd) |
47 | sb.write_string(path_separator) |
48 | sb.write_string(npath) |
49 | return norm_path(sb.str()) |
50 | } |
51 | return npath |
52 | } |
53 | |
54 | // norm_path returns the normalized version of the given `path` |
55 | // by resolving backlinks (..), turning forward slashes into |
56 | // back slashes on a Windows system and eliminating: |
57 | // - references to current directories (.) |
58 | // - redundant path separators |
59 | // - the last path separator |
60 | [direct_array_access] |
61 | pub fn norm_path(path string) string { |
62 | if path.len == 0 { |
63 | return os.dot_str |
64 | } |
65 | rooted := is_abs_path(path) |
66 | // get the volume name from the path |
67 | // if the current operating system is Windows |
68 | volume_len := win_volume_len(path) |
69 | mut volume := path[..volume_len] |
70 | if volume_len != 0 && volume.contains(os.fslash_str) { |
71 | volume = volume.replace(os.fslash_str, path_separator) |
72 | } |
73 | cpath := clean_path(path[volume_len..]) |
74 | if cpath.len == 0 && volume_len == 0 { |
75 | return os.dot_str |
76 | } |
77 | spath := cpath.split(path_separator) |
78 | if os.dot_dot !in spath { |
79 | return if volume_len != 0 { volume + cpath } else { cpath } |
80 | } |
81 | // resolve backlinks (..) |
82 | spath_len := spath.len |
83 | mut sb := strings.new_builder(cpath.len) |
84 | if rooted { |
85 | sb.write_string(path_separator) |
86 | } |
87 | mut new_path := []string{cap: spath_len} |
88 | mut backlink_count := 0 |
89 | for i := spath_len - 1; i >= 0; i-- { |
90 | part := spath[i] |
91 | if part == os.empty_str { |
92 | continue |
93 | } |
94 | if part == os.dot_dot { |
95 | backlink_count++ |
96 | continue |
97 | } |
98 | if backlink_count != 0 { |
99 | backlink_count-- |
100 | continue |
101 | } |
102 | new_path.prepend(part) |
103 | } |
104 | // append backlink(s) to the path if backtracking |
105 | // is not possible and the given path is not rooted |
106 | if backlink_count != 0 && !rooted { |
107 | for i in 0 .. backlink_count { |
108 | sb.write_string(os.dot_dot) |
109 | if new_path.len == 0 && i == backlink_count - 1 { |
110 | break |
111 | } |
112 | sb.write_string(path_separator) |
113 | } |
114 | } |
115 | sb.write_string(new_path.join(path_separator)) |
116 | res := sb.str() |
117 | if res.len == 0 { |
118 | if volume_len != 0 { |
119 | return volume |
120 | } |
121 | if !rooted { |
122 | return os.dot_str |
123 | } |
124 | return path_separator |
125 | } |
126 | if volume_len != 0 { |
127 | return volume + res |
128 | } |
129 | return res |
130 | } |
131 | |
132 | // existing_path returns the existing part of the given `path`. |
133 | // An error is returned if there is no existing part of the given `path`. |
134 | pub fn existing_path(path string) !string { |
135 | err := error('path does not exist') |
136 | if path.len == 0 { |
137 | return err |
138 | } |
139 | if exists(path) { |
140 | return path |
141 | } |
142 | mut volume_len := 0 |
143 | $if windows { |
144 | volume_len = win_volume_len(path) |
145 | } |
146 | if volume_len > 0 && is_slash(path[volume_len - 1]) { |
147 | volume_len++ |
148 | } |
149 | mut sc := textscanner.new(path[volume_len..]) |
150 | mut recent_path := path[..volume_len] |
151 | for sc.next() != -1 { |
152 | curr := u8(sc.current()) |
153 | peek := sc.peek() |
154 | back := sc.peek_back() |
155 | if is_curr_dir_ref(back, curr, peek) { |
156 | continue |
157 | } |
158 | range := sc.ilen - sc.remaining() + volume_len |
159 | if is_slash(curr) && !is_slash(u8(peek)) { |
160 | recent_path = path[..range] |
161 | continue |
162 | } |
163 | if !is_slash(curr) && (peek == -1 || is_slash(u8(peek))) { |
164 | curr_path := path[..range] |
165 | if exists(curr_path) { |
166 | recent_path = curr_path |
167 | continue |
168 | } |
169 | if recent_path.len == 0 { |
170 | break |
171 | } |
172 | return recent_path |
173 | } |
174 | } |
175 | return err |
176 | } |
177 | |
178 | // clean_path returns the "cleaned" version of the given `path` |
179 | // by turning forward slashes into back slashes |
180 | // on a Windows system and eliminating: |
181 | // - references to current directories (.) |
182 | // - redundant separators |
183 | // - the last path separator |
184 | fn clean_path(path string) string { |
185 | if path.len == 0 { |
186 | return os.empty_str |
187 | } |
188 | mut sb := strings.new_builder(path.len) |
189 | mut sc := textscanner.new(path) |
190 | for sc.next() != -1 { |
191 | curr := u8(sc.current()) |
192 | back := sc.peek_back() |
193 | peek := sc.peek() |
194 | // skip current path separator if last byte was a path separator |
195 | if back != -1 && is_slash(u8(back)) && is_slash(curr) { |
196 | continue |
197 | } |
198 | // skip reference to current dir (.) |
199 | if is_curr_dir_ref(back, curr, peek) { |
200 | // skip if the next byte is a path separator |
201 | if peek != -1 && is_slash(u8(peek)) { |
202 | sc.skip_n(1) |
203 | } |
204 | continue |
205 | } |
206 | // turn foward slash into a back slash on a Windows system |
207 | $if windows { |
208 | if curr == os.fslash { |
209 | sb.write_u8(os.bslash) |
210 | continue |
211 | } |
212 | } |
213 | sb.write_u8(u8(sc.current())) |
214 | } |
215 | res := sb.str() |
216 | // eliminate the last path separator |
217 | if res.len > 1 && is_slash(res[res.len - 1]) { |
218 | return res[..res.len - 1] |
219 | } |
220 | return res |
221 | } |
222 | |
223 | // to_slash returns the result of replacing each separator character |
224 | // in path with a slash (`/`). |
225 | pub fn to_slash(path string) string { |
226 | if path_separator == '/' { |
227 | return path |
228 | } |
229 | return path.replace(path_separator, '/') |
230 | } |
231 | |
232 | // from_slash returns the result of replacing each slash (`/`) character |
233 | // is path with a separator character. |
234 | pub fn from_slash(path string) string { |
235 | if path_separator == '/' { |
236 | return path |
237 | } |
238 | return path.replace('/', path_separator) |
239 | } |
240 | |
241 | // win_volume_len returns the length of the |
242 | // Windows volume/drive from the given `path`. |
243 | fn win_volume_len(path string) int { |
244 | $if !windows { |
245 | return 0 |
246 | } |
247 | plen := path.len |
248 | if plen < 2 { |
249 | return 0 |
250 | } |
251 | if has_drive_letter(path) { |
252 | return 2 |
253 | } |
254 | // its UNC path / DOS device path? |
255 | if plen >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) { |
256 | for i := 3; i < plen; i++ { |
257 | if is_slash(path[i]) { |
258 | if i + 1 >= plen || is_slash(path[i + 1]) { |
259 | break |
260 | } |
261 | i++ |
262 | for ; i < plen; i++ { |
263 | if is_slash(path[i]) { |
264 | return i |
265 | } |
266 | } |
267 | return i |
268 | } |
269 | } |
270 | } |
271 | return 0 |
272 | } |
273 | |
274 | fn is_slash(b u8) bool { |
275 | $if windows { |
276 | return b == os.bslash || b == os.fslash |
277 | } |
278 | return b == os.fslash |
279 | } |
280 | |
281 | fn is_unc_path(path string) bool { |
282 | return win_volume_len(path) >= 5 && starts_w_slash_slash(path) |
283 | } |
284 | |
285 | fn has_drive_letter(path string) bool { |
286 | return path.len >= 2 && path[0].is_letter() && path[1] == `:` |
287 | } |
288 | |
289 | fn starts_w_slash_slash(path string) bool { |
290 | return path.len >= 2 && is_slash(path[0]) && is_slash(path[1]) |
291 | } |
292 | |
293 | fn is_drive_rooted(path string) bool { |
294 | return path.len >= 3 && has_drive_letter(path) && is_slash(path[2]) |
295 | } |
296 | |
297 | // is_normal_path returns `true` if the given |
298 | // `path` is NOT a network or Windows device path. |
299 | fn is_normal_path(path string) bool { |
300 | plen := path.len |
301 | if plen == 0 { |
302 | return false |
303 | } |
304 | return (plen == 1 && is_slash(path[0])) || (plen >= 2 && is_slash(path[0]) |
305 | && !is_slash(path[1])) |
306 | } |
307 | |
308 | // is_curr_dir_ref returns `true` if the 3 given integer construct |
309 | // a reference to a current directory (.). |
310 | // NOTE: a negative integer means that no byte is present |
311 | fn is_curr_dir_ref(byte_one int, byte_two int, byte_three int) bool { |
312 | if u8(byte_two) != os.dot { |
313 | return false |
314 | } |
315 | return (byte_one < 0 || is_slash(u8(byte_one))) && (byte_three < 0 || is_slash(u8(byte_three))) |
316 | } |