1
2
3
4
5
6
7
8
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
21
22
23
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
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
72 return FromSlash(originalPath)
73 }
74 return originalPath + "."
75 }
76 rooted := IsPathSeparator(path[0])
77
78
79
80
81
82
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
95 r++
96 case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])):
97
98 r++
99 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])):
100
101 r += 2
102 switch {
103 case out.w > dotdot:
104
105 out.w--
106 for out.w > dotdot && !IsPathSeparator(out.index(out.w)) {
107 out.w--
108 }
109 case !rooted:
110
111 if out.w > 0 {
112 out.append(Separator)
113 }
114 out.append('.')
115 out.append('.')
116 dotdot = out.w
117 }
118 default:
119
120
121 if rooted && out.w != 1 || !rooted && out.w != 0 {
122 out.append(Separator)
123 }
124
125 for ; r < n && !IsPathSeparator(path[r]); r++ {
126 out.append(path[r])
127 }
128 }
129 }
130
131
132 if out.w == 0 {
133 out.append('.')
134 }
135
136 postClean(&out)
137 return FromSlash(out.string())
138 }
139
140
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
168 func Localize(path string) (string, error) {
169 if !fs.ValidPath(path) {
170 return "", errInvalidPath
171 }
172 return localize(path)
173 }
174
175
176 func ToSlash(path string) string {
177 if Separator == '/' {
178 return path
179 }
180 return replaceStringByte(path, Separator, '/')
181 }
182
183
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
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
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
225 func Base(path string) string {
226 if path == "" {
227 return "."
228 }
229
230 for len(path) > 0 && IsPathSeparator(path[len(path)-1]) {
231 path = path[0 : len(path)-1]
232 }
233
234 path = path[len(VolumeName(path)):]
235
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
244 if path == "" {
245 return string(Separator)
246 }
247 return path
248 }
249
250
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
260 return vol
261 }
262 return vol + dir
263 }
264
265
266 func VolumeName(path string) string {
267 return FromSlash(path[:volumeNameLen(path)])
268 }
269
270
271
272 func VolumeNameLen(path string) int {
273 return volumeNameLen(path)
274 }
275
View as plain text