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