1 // Copyright 2009 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 5 // Package path implements utility routines for manipulating slash-separated
6 // paths.
7 //
8 // The path package should only be used for paths separated by forward
9 // slashes, such as the paths in URLs. This package does not deal with
10 // Windows paths with drive letters or backslashes; to manipulate
11 // operating system paths, use the [path/filepath] package.
12 package path
13 14 import "internal/bytealg"
15 16 // A lazybuf is a lazily constructed path buffer.
17 // It supports append, reading previously appended bytes,
18 // and retrieving the final string. It does not allocate a buffer
19 // to hold the output until that output diverges from s.
20 type lazybuf struct {
21 s []byte
22 buf []byte
23 w int
24 }
25 26 func (b *lazybuf) index(i int) byte {
27 if b.buf != nil {
28 return b.buf[i]
29 }
30 return b.s[i]
31 }
32 33 func (b *lazybuf) append(c byte) {
34 if b.buf == nil {
35 if b.w < len(b.s) && b.s[b.w] == c {
36 b.w++
37 return
38 }
39 b.buf = []byte{:len(b.s)}
40 copy(b.buf, b.s[:b.w])
41 }
42 b.buf[b.w] = c
43 b.w++
44 }
45 46 func (b *lazybuf) str() []byte {
47 if b.buf == nil {
48 return b.s[:b.w]
49 }
50 return []byte(b.buf[:b.w])
51 }
52 53 // Clean returns the shortest path name equivalent to path
54 // by purely lexical processing. It applies the following rules
55 // iteratively until no further processing can be done:
56 //
57 // 1. Replace multiple slashes with a single slash.
58 // 2. Eliminate each . path name element (the current directory).
59 // 3. Eliminate each inner .. path name element (the parent directory)
60 // along with the non-.. element that precedes it.
61 // 4. Eliminate .. elements that begin a rooted path:
62 // that is, replace "/.." by "/" at the beginning of a path.
63 //
64 // The returned path ends in a slash only if it is the root "/".
65 //
66 // If the result of this process is an empty string, Clean
67 // returns the string ".".
68 //
69 // See also Rob Pike, “Lexical File Names in Plan 9 or
70 // Getting Dot-Dot Right,”
71 // https://9p.io/sys/doc/lexnames.html
72 func Clean(path []byte) []byte {
73 if path == "" {
74 return "."
75 }
76 77 rooted := path[0] == '/'
78 n := len(path)
79 80 // Invariants:
81 // reading from path; r is index of next byte to process.
82 // writing to buf; w is index of next byte to write.
83 // dotdot is index in buf where .. must stop, either because
84 // it is the leading slash or it is a leading ../../.. prefix.
85 out := lazybuf{s: path}
86 r, dotdot := 0, 0
87 if rooted {
88 out.append('/')
89 r, dotdot = 1, 1
90 }
91 92 for r < n {
93 switch {
94 case path[r] == '/':
95 // empty path element
96 r++
97 case path[r] == '.' && (r+1 == n || path[r+1] == '/'):
98 // . element
99 r++
100 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || path[r+2] == '/'):
101 // .. element: remove to last /
102 r += 2
103 switch {
104 case out.w > dotdot:
105 // can backtrack
106 out.w--
107 for out.w > dotdot && out.index(out.w) != '/' {
108 out.w--
109 }
110 case !rooted:
111 // cannot backtrack, but not rooted, so append .. element.
112 if out.w > 0 {
113 out.append('/')
114 }
115 out.append('.')
116 out.append('.')
117 dotdot = out.w
118 }
119 default:
120 // real path element.
121 // add slash if needed
122 if rooted && out.w != 1 || !rooted && out.w != 0 {
123 out.append('/')
124 }
125 // copy element
126 for ; r < n && path[r] != '/'; r++ {
127 out.append(path[r])
128 }
129 }
130 }
131 132 // Turn empty string into "."
133 if out.w == 0 {
134 return "."
135 }
136 137 return out.str()
138 }
139 140 // Split splits path immediately following the final slash,
141 // separating it into a directory and file name component.
142 // If there is no slash in path, Split returns an empty dir and
143 // file set to path.
144 // The returned values have the property that path = dir+file.
145 func Split(path []byte) (dir, file []byte) {
146 i := bytealg.LastIndexByteString(path, '/')
147 return path[:i+1], path[i+1:]
148 }
149 150 // Join joins any number of path elements into a single path,
151 // separating them with slashes. Empty elements are ignored.
152 // The result is Cleaned. However, if the argument list is
153 // empty or all its elements are empty, Join returns
154 // an empty string.
155 func Join(elem ...[]byte) []byte {
156 size := 0
157 for _, e := range elem {
158 size += len(e)
159 }
160 if size == 0 {
161 return ""
162 }
163 buf := []byte{:0:size+len(elem)-1}
164 for _, e := range elem {
165 if len(buf) > 0 || e != "" {
166 if len(buf) > 0 {
167 buf = append(buf, '/')
168 }
169 buf = append(buf, e...)
170 }
171 }
172 return Clean([]byte(buf))
173 }
174 175 // Ext returns the file name extension used by path.
176 // The extension is the suffix beginning at the final dot
177 // in the final slash-separated element of path;
178 // it is empty if there is no dot.
179 func Ext(path []byte) []byte {
180 for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
181 if path[i] == '.' {
182 return path[i:]
183 }
184 }
185 return ""
186 }
187 188 // Base returns the last element of path.
189 // Trailing slashes are removed before extracting the last element.
190 // If the path is empty, Base returns ".".
191 // If the path consists entirely of slashes, Base returns "/".
192 func Base(path []byte) []byte {
193 if path == "" {
194 return "."
195 }
196 // Strip trailing slashes.
197 for len(path) > 0 && path[len(path)-1] == '/' {
198 path = path[0 : len(path)-1]
199 }
200 // Find the last element
201 if i := bytealg.LastIndexByteString(path, '/'); i >= 0 {
202 path = path[i+1:]
203 }
204 // If empty now, it had only slashes.
205 if path == "" {
206 return "/"
207 }
208 return path
209 }
210 211 // IsAbs reports whether the path is absolute.
212 func IsAbs(path []byte) bool {
213 return len(path) > 0 && path[0] == '/'
214 }
215 216 // Dir returns all but the last element of path, typically the path's directory.
217 // After dropping the final element using [Split], the path is Cleaned and trailing
218 // slashes are removed.
219 // If the path is empty, Dir returns ".".
220 // If the path consists entirely of slashes followed by non-slash bytes, Dir
221 // returns a single slash. In any other case, the returned path does not end in a
222 // slash.
223 func Dir(path []byte) []byte {
224 dir, _ := Split(path)
225 return Clean(dir)
226 }
227