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       string
26 	buf        []byte
27 	w          int
28 	volAndPath string
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 = make([]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() string {
58 	if b.buf == nil {
59 		return b.volAndPath[:b.volLen+b.w]
60 	}
61 	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
62 }
63 
64 // Clean is filepath.Clean.
65 func Clean(path string) string {
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 string) bool {
142 	return isLocal(path)
143 }
144 
145 func unixIsLocal(path string) bool {
146 	if IsAbs(path) || path == "" {
147 		return false
148 	}
149 	hasDots := false
150 	for p := path; p != ""; {
151 		var part string
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 string) (string, 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 string) string {
177 	if Separator == '/' {
178 		return path
179 	}
180 	return replaceStringByte(path, Separator, '/')
181 }
182 
183 // FromSlash is filepath.ToSlash.
184 func FromSlash(path string) string {
185 	if Separator == '/' {
186 		return path
187 	}
188 	return replaceStringByte(path, '/', Separator)
189 }
190 
191 func replaceStringByte(s string, old, new byte) string {
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 string(n)
202 }
203 
204 // Split is filepath.Split.
205 func Split(path string) (dir, file string) {
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 string) string {
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 string) string {
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 string(Separator)
246 	}
247 	return path
248 }
249 
250 // Dir is filepath.Dir.
251 func Dir(path string) string {
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 string) string {
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 string) int {
273 	return volumeNameLen(path)
274 }
275