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