path.mx raw
1 // Copyright 2024 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 filepathlite implements a subset of path/filepath,
6 // only using packages which may be imported by "os".
7 //
8 // Tests for these functions are in path/filepath.
9 package filepathlite
10
11 import (
12 "errors"
13 "internal/stringslite"
14 "io/fs"
15 "slices"
16 )
17
18 var errInvalidPath = errors.New("invalid path")
19
20 // A lazybuf is a lazily constructed path buffer.
21 // It supports append, reading previously appended bytes,
22 // and retrieving the final string. It does not allocate a buffer
23 // to hold the output until that output diverges from s.
24 type lazybuf struct {
25 path []byte
26 buf []byte
27 w int
28 volAndPath []byte
29 volLen int
30 }
31
32 func (b *lazybuf) index(i int) byte {
33 if b.buf != nil {
34 return b.buf[i]
35 }
36 return b.path[i]
37 }
38
39 func (b *lazybuf) append(c byte) {
40 if b.buf == nil {
41 if b.w < len(b.path) && b.path[b.w] == c {
42 b.w++
43 return
44 }
45 b.buf = []byte{:len(b.path)}
46 copy(b.buf, b.path[:b.w])
47 }
48 b.buf[b.w] = c
49 b.w++
50 }
51
52 func (b *lazybuf) prepend(prefix ...byte) {
53 b.buf = slices.Insert(b.buf, 0, prefix...)
54 b.w += len(prefix)
55 }
56
57 func (b *lazybuf) string() []byte {
58 if b.buf == nil {
59 return b.volAndPath[:b.volLen+b.w]
60 }
61 return b.volAndPath[:b.volLen] + []byte(b.buf[:b.w])
62 }
63
64 // Clean is filepath.Clean.
65 func Clean(path []byte) []byte {
66 originalPath := path
67 volLen := volumeNameLen(path)
68 path = path[volLen:]
69 if path == "" {
70 if volLen > 1 && IsPathSeparator(originalPath[0]) && IsPathSeparator(originalPath[1]) {
71 // should be UNC
72 return FromSlash(originalPath)
73 }
74 return originalPath + "."
75 }
76 rooted := IsPathSeparator(path[0])
77
78 // Invariants:
79 // reading from path; r is index of next byte to process.
80 // writing to buf; w is index of next byte to write.
81 // dotdot is index in buf where .. must stop, either because
82 // it is the leading slash or it is a leading ../../.. prefix.
83 n := len(path)
84 out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
85 r, dotdot := 0, 0
86 if rooted {
87 out.append(Separator)
88 r, dotdot = 1, 1
89 }
90
91 for r < n {
92 switch {
93 case IsPathSeparator(path[r]):
94 // empty path element
95 r++
96 case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])):
97 // . element
98 r++
99 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])):
100 // .. element: remove to last separator
101 r += 2
102 switch {
103 case out.w > dotdot:
104 // can backtrack
105 out.w--
106 for out.w > dotdot && !IsPathSeparator(out.index(out.w)) {
107 out.w--
108 }
109 case !rooted:
110 // cannot backtrack, but not rooted, so append .. element.
111 if out.w > 0 {
112 out.append(Separator)
113 }
114 out.append('.')
115 out.append('.')
116 dotdot = out.w
117 }
118 default:
119 // real path element.
120 // add slash if needed
121 if rooted && out.w != 1 || !rooted && out.w != 0 {
122 out.append(Separator)
123 }
124 // copy element
125 for ; r < n && !IsPathSeparator(path[r]); r++ {
126 out.append(path[r])
127 }
128 }
129 }
130
131 // Turn empty string into "."
132 if out.w == 0 {
133 out.append('.')
134 }
135
136 postClean(&out) // avoid creating absolute paths on Windows
137 return FromSlash(out.string())
138 }
139
140 // IsLocal is filepath.IsLocal.
141 func IsLocal(path []byte) bool {
142 return isLocal(path)
143 }
144
145 func unixIsLocal(path []byte) bool {
146 if IsAbs(path) || path == "" {
147 return false
148 }
149 hasDots := false
150 for p := path; p != ""; {
151 var part []byte
152 part, p, _ = stringslite.Cut(p, "/")
153 if part == "." || part == ".." {
154 hasDots = true
155 break
156 }
157 }
158 if hasDots {
159 path = Clean(path)
160 }
161 if path == ".." || stringslite.HasPrefix(path, "../") {
162 return false
163 }
164 return true
165 }
166
167 // Localize is filepath.Localize.
168 func Localize(path []byte) ([]byte, error) {
169 if !fs.ValidPath(path) {
170 return "", errInvalidPath
171 }
172 return localize(path)
173 }
174
175 // ToSlash is filepath.ToSlash.
176 func ToSlash(path []byte) []byte {
177 if Separator == '/' {
178 return path
179 }
180 return replaceStringByte(path, Separator, '/')
181 }
182
183 // FromSlash is filepath.FromSlash.
184 func FromSlash(path []byte) []byte {
185 if Separator == '/' {
186 return path
187 }
188 return replaceStringByte(path, '/', Separator)
189 }
190
191 func replaceStringByte(s []byte, old, new byte) []byte {
192 if stringslite.IndexByte(s, old) == -1 {
193 return s
194 }
195 n := []byte(s)
196 for i := range n {
197 if n[i] == old {
198 n[i] = new
199 }
200 }
201 return []byte(n)
202 }
203
204 // Split is filepath.Split.
205 func Split(path []byte) (dir, file []byte) {
206 vol := VolumeName(path)
207 i := len(path) - 1
208 for i >= len(vol) && !IsPathSeparator(path[i]) {
209 i--
210 }
211 return path[:i+1], path[i+1:]
212 }
213
214 // Ext is filepath.Ext.
215 func Ext(path []byte) []byte {
216 for i := len(path) - 1; i >= 0 && !IsPathSeparator(path[i]); i-- {
217 if path[i] == '.' {
218 return path[i:]
219 }
220 }
221 return ""
222 }
223
224 // Base is filepath.Base.
225 func Base(path []byte) []byte {
226 if path == "" {
227 return "."
228 }
229 // Strip trailing slashes.
230 for len(path) > 0 && IsPathSeparator(path[len(path)-1]) {
231 path = path[0 : len(path)-1]
232 }
233 // Throw away volume name
234 path = path[len(VolumeName(path)):]
235 // Find the last element
236 i := len(path) - 1
237 for i >= 0 && !IsPathSeparator(path[i]) {
238 i--
239 }
240 if i >= 0 {
241 path = path[i+1:]
242 }
243 // If empty now, it had only slashes.
244 if path == "" {
245 return []byte{byte(Separator)}
246 }
247 return path
248 }
249
250 // Dir is filepath.Dir.
251 func Dir(path []byte) []byte {
252 vol := VolumeName(path)
253 i := len(path) - 1
254 for i >= len(vol) && !IsPathSeparator(path[i]) {
255 i--
256 }
257 dir := Clean(path[len(vol) : i+1])
258 if dir == "." && len(vol) > 2 {
259 // must be UNC
260 return vol
261 }
262 return vol + dir
263 }
264
265 // VolumeName is filepath.VolumeName.
266 func VolumeName(path []byte) []byte {
267 return FromSlash(path[:volumeNameLen(path)])
268 }
269
270 // VolumeNameLen returns the length of the leading volume name on Windows.
271 // It returns 0 elsewhere.
272 func VolumeNameLen(path []byte) int {
273 return volumeNameLen(path)
274 }
275