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. 9package filepathlite 10 11import ( 12 "errors" 13 "internal/stringslite" 14 "io/fs" 15 "slices" 16) 17 18var 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. 24type lazybuf struct { 25 path string 26 buf []byte 27 w int 28 volAndPath string 29 volLen int 30} 31 32func (b *lazybuf) index(i int) byte { 33 if b.buf != nil { 34 return b.buf[i] 35 } 36 return b.path[i] 37} 38 39func (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 52func (b *lazybuf) prepend(prefix ...byte) { 53 b.buf = slices.Insert(b.buf, 0, prefix...) 54 b.w += len(prefix) 55} 56 57func (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. 65func 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. 141func IsLocal(path string) bool { 142 return isLocal(path) 143} 144 145func 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. 168func 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. 176func ToSlash(path string) string { 177 if Separator == '/' { 178 return path 179 } 180 return replaceStringByte(path, Separator, '/') 181} 182 183// FromSlash is filepath.ToSlash. 184func FromSlash(path string) string { 185 if Separator == '/' { 186 return path 187 } 188 return replaceStringByte(path, '/', Separator) 189} 190 191func 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. 205func 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. 215func 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. 225func 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. 251func 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. 266func 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. 272func VolumeNameLen(path string) int { 273 return volumeNameLen(path) 274} 275