v / vlib / os
Raw file | 316 loc (298 sloc) | 7.53 KB | Latest commit hash 3e4cfc734
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 (
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.
22pub 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.
35pub 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]
61pub 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`.
134pub 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
184fn 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 (`/`).
225pub 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.
234pub 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`.
243fn 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
274fn is_slash(b u8) bool {
275 $if windows {
276 return b == os.bslash || b == os.fslash
277 }
278 return b == os.fslash
279}
280
281fn is_unc_path(path string) bool {
282 return win_volume_len(path) >= 5 && starts_w_slash_slash(path)
283}
284
285fn has_drive_letter(path string) bool {
286 return path.len >= 2 && path[0].is_letter() && path[1] == `:`
287}
288
289fn starts_w_slash_slash(path string) bool {
290 return path.len >= 2 && is_slash(path[0]) && is_slash(path[1])
291}
292
293fn 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.
299fn 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
311fn 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}