github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/pkg/path/path.go (about) 1 // Copyright 2020 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Copyright 2009 The Go Authors. All rights reserved. 16 // Use of this source code is governed by a BSD-style 17 // license that can be found in the LICENSE file. 18 19 // Package filepath implements utility routines for manipulating filename paths 20 // in a way compatible with the target operating system-defined file paths. 21 // 22 // The filepath package uses either forward slashes or backslashes, 23 // depending on the operating system. To process paths such as URLs 24 // that always use forward slashes regardless of the operating 25 // system, see the path package. 26 package path 27 28 import ( 29 "errors" 30 "strings" 31 ) 32 33 // A lazybuf is a lazily constructed path buffer. 34 // It supports append, reading previously appended bytes, 35 // and retrieving the final string. It does not allocate a buffer 36 // to hold the output until that output diverges. 37 type lazybuf struct { 38 path string 39 buf []byte 40 w int 41 volAndPath string 42 volLen int 43 } 44 45 func (b *lazybuf) index(i int) byte { 46 if b.buf != nil { 47 return b.buf[i] 48 } 49 return b.path[i] 50 } 51 52 func (b *lazybuf) append(c byte) { 53 if b.buf == nil { 54 if b.w < len(b.path) && b.path[b.w] == c { 55 b.w++ 56 return 57 } 58 b.buf = make([]byte, len(b.path)) 59 copy(b.buf, b.path[:b.w]) 60 } 61 b.buf[b.w] = c 62 b.w++ 63 } 64 65 func (b *lazybuf) string() string { 66 if b.buf == nil { 67 return b.volAndPath[:b.volLen+b.w] 68 } 69 return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) 70 } 71 72 // Clean returns the shortest path name equivalent to path 73 // by purely lexical processing. The default value for os is Unix. 74 // It applies the following rules 75 // iteratively until no further processing can be done: 76 // 77 // 1. Replace multiple Separator elements with a single one. 78 // 2. Eliminate each . path name element (the current directory). 79 // 3. Eliminate each inner .. path name element (the parent directory) 80 // along with the non-.. element that precedes it. 81 // 4. Eliminate .. elements that begin a rooted path: 82 // that is, replace "/.." by "/" at the beginning of a path, 83 // assuming Separator is '/'. 84 // 85 // The returned path ends in a slash only if it represents a root directory, 86 // such as "/" on Unix or `C:\` on Windows. 87 // 88 // Finally, any occurrences of slash are replaced by Separator. 89 // 90 // If the result of this process is an empty string, Clean 91 // returns the string ".". 92 // 93 // See also Rob Pike, ``Lexical File Names in Plan 9 or 94 // Getting Dot-Dot Right,'' 95 // https://9p.io/sys/doc/lexnames.html 96 func Clean(path string, os OS) string { 97 return clean(path, getOS(os)) 98 } 99 100 func clean(path string, os os) string { 101 originalPath := path 102 volLen := os.volumeNameLen(path) 103 path = path[volLen:] 104 if path == "" { 105 if volLen > 1 && originalPath[1] != ':' { 106 // should be UNC 107 return fromSlash(originalPath, os) 108 } 109 return originalPath + "." 110 } 111 rooted := os.IsPathSeparator(path[0]) 112 113 // Invariants: 114 // reading from path; r is index of next byte to process. 115 // writing to buf; w is index of next byte to write. 116 // dotdot is index in buf where .. must stop, either because 117 // it is the leading slash or it is a leading ../../.. prefix. 118 n := len(path) 119 out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} 120 r, dotdot := 0, 0 121 if rooted { 122 out.append(os.Separator) 123 r, dotdot = 1, 1 124 } 125 126 for r < n { 127 switch { 128 case os.IsPathSeparator(path[r]): 129 // empty path element 130 r++ 131 case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): 132 // . element 133 r++ 134 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): 135 // .. element: remove to last separator 136 r += 2 137 switch { 138 case out.w > dotdot: 139 // can backtrack 140 out.w-- 141 for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) { 142 out.w-- 143 } 144 case !rooted: 145 // cannot backtrack, but not rooted, so append .. element. 146 if out.w > 0 { 147 out.append(os.Separator) 148 } 149 out.append('.') 150 out.append('.') 151 dotdot = out.w 152 } 153 default: 154 // real path element. 155 // add slash if needed 156 if rooted && out.w != 1 || !rooted && out.w != 0 { 157 out.append(os.Separator) 158 } 159 // copy element 160 for ; r < n && !os.IsPathSeparator(path[r]); r++ { 161 out.append(path[r]) 162 } 163 } 164 } 165 166 // Turn empty string into "." 167 if out.w == 0 { 168 out.append('.') 169 } 170 171 return fromSlash(out.string(), os) 172 } 173 174 // ToSlash returns the result of replacing each separator character 175 // in path with a slash ('/') character. Multiple separators are 176 // replaced by multiple slashes. 177 func ToSlash(path string, os OS) string { 178 return toSlash(path, getOS(os)) 179 } 180 181 func toSlash(path string, os os) string { 182 if os.Separator == '/' { 183 return path 184 } 185 return strings.ReplaceAll(path, string(os.Separator), "/") 186 } 187 188 // FromSlash returns the result of replacing each slash ('/') character 189 // in path with a separator character. Multiple slashes are replaced 190 // by multiple separators. 191 func FromSlash(path string, os OS) string { 192 return fromSlash(path, getOS(os)) 193 } 194 195 func fromSlash(path string, os os) string { 196 if os.Separator == '/' { 197 return path 198 } 199 return strings.ReplaceAll(path, "/", string(os.Separator)) 200 } 201 202 // SplitList splits a list of paths joined by the OS-specific ListSeparator, 203 // usually found in PATH or GOPATH environment variables. 204 // Unlike strings.Split, SplitList returns an empty slice when passed an empty 205 // string. 206 func SplitList(path string, os OS) []string { 207 return getOS(os).splitList(path) 208 } 209 210 // Split splits path immediately following the final slash and returns them as 211 // the list [dir, file], separating it into a directory and file name component. 212 // If there is no slash in path, Split returns an empty dir and file set to 213 // path. The returned values have the property that path = dir+file. 214 // The default value for os is Unix. 215 func Split(path string, os OS) []string { 216 x := getOS(os) 217 vol := volumeName(path, x) 218 i := len(path) - 1 219 for i >= len(vol) && !x.IsPathSeparator(path[i]) { 220 i-- 221 } 222 return []string{path[:i+1], path[i+1:]} 223 } 224 225 // Join joins any number of path elements into a single path, 226 // separating them with an OS specific Separator. Empty elements 227 // are ignored. The result is Cleaned. However, if the argument 228 // list is empty or all its elements are empty, Join returns 229 // an empty string. 230 // On Windows, the result will only be a UNC path if the first 231 // non-empty element is a UNC path. 232 // The default value for os is Unix. 233 func Join(elem []string, os OS) string { 234 return getOS(os).join(elem) 235 } 236 237 // Ext returns the file name extension used by path. 238 // The extension is the suffix beginning at the final dot 239 // in the final element of path; it is empty if there is 240 // no dot. The default value for os is Unix. 241 func Ext(path string, os OS) string { 242 x := getOS(os) 243 for i := len(path) - 1; i >= 0 && !x.IsPathSeparator(path[i]); i-- { 244 if path[i] == '.' { 245 return path[i:] 246 } 247 } 248 return "" 249 } 250 251 // Resolve reports the path of sub relative to dir. If sub is an absolute path, 252 // or if dir is empty, it will return sub. If sub is empty, it will return dir. 253 // Resolve calls Clean on the result. The default value for os is Unix. 254 func Resolve(dir, sub string, os OS) string { 255 x := getOS(os) 256 if x.IsAbs(sub) { 257 return clean(sub, x) 258 } 259 dir = clean(dir, x) 260 return x.join([]string{dir, sub}) 261 } 262 263 // Rel returns a relative path that is lexically equivalent to targpath when 264 // joined to basepath with an intervening separator. That is, 265 // Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself. 266 // On success, the returned path will always be relative to basepath, 267 // even if basepath and targpath share no elements. 268 // An error is returned if targpath can't be made relative to basepath or if 269 // knowing the current working directory would be necessary to compute it. 270 // Rel calls Clean on the result. The default value for os is Unix. 271 func Rel(basepath, targpath string, os OS) (string, error) { 272 x := getOS(os) 273 baseVol := volumeName(basepath, x) 274 targVol := volumeName(targpath, x) 275 base := clean(basepath, x) 276 targ := clean(targpath, x) 277 if x.sameWord(targ, base) { 278 return ".", nil 279 } 280 base = base[len(baseVol):] 281 targ = targ[len(targVol):] 282 if base == "." { 283 base = "" 284 } 285 // Can't use IsAbs - `\a` and `a` are both relative in Windows. 286 baseSlashed := len(base) > 0 && base[0] == x.Separator 287 targSlashed := len(targ) > 0 && targ[0] == x.Separator 288 if baseSlashed != targSlashed || !x.sameWord(baseVol, targVol) { 289 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 290 } 291 // Position base[b0:bi] and targ[t0:ti] at the first differing elements. 292 bl := len(base) 293 tl := len(targ) 294 var b0, bi, t0, ti int 295 for { 296 for bi < bl && base[bi] != x.Separator { 297 bi++ 298 } 299 for ti < tl && targ[ti] != x.Separator { 300 ti++ 301 } 302 if !x.sameWord(targ[t0:ti], base[b0:bi]) { 303 break 304 } 305 if bi < bl { 306 bi++ 307 } 308 if ti < tl { 309 ti++ 310 } 311 b0 = bi 312 t0 = ti 313 } 314 if base[b0:bi] == ".." { 315 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 316 } 317 if b0 != bl { 318 // Base elements left. Must go up before going down. 319 seps := strings.Count(base[b0:bl], string(x.Separator)) 320 size := 2 + seps*3 321 if tl != t0 { 322 size += 1 + tl - t0 323 } 324 buf := make([]byte, size) 325 n := copy(buf, "..") 326 for i := 0; i < seps; i++ { 327 buf[n] = x.Separator 328 copy(buf[n+1:], "..") 329 n += 3 330 } 331 if t0 != tl { 332 buf[n] = x.Separator 333 copy(buf[n+1:], targ[t0:]) 334 } 335 return string(buf), nil 336 } 337 return targ[t0:], nil 338 } 339 340 // Base returns the last element of path. 341 // Trailing path separators are removed before extracting the last element. 342 // If the path is empty, Base returns ".". 343 // If the path consists entirely of separators, Base returns a single separator. 344 // The default value for os is Unix. 345 func Base(path string, os OS) string { 346 x := getOS(os) 347 if path == "" { 348 return "." 349 } 350 // Strip trailing slashes. 351 for len(path) > 0 && x.IsPathSeparator(path[len(path)-1]) { 352 path = path[0 : len(path)-1] 353 } 354 // Throw away volume name 355 path = path[x.volumeNameLen(path):] 356 // Find the last element 357 i := len(path) - 1 358 for i >= 0 && !x.IsPathSeparator(path[i]) { 359 i-- 360 } 361 if i >= 0 { 362 path = path[i+1:] 363 } 364 // If empty now, it had only slashes. 365 if path == "" { 366 return string(x.Separator) 367 } 368 return path 369 } 370 371 // Dir returns all but the last element of path, typically the path's directory. 372 // After dropping the final element, Dir calls Clean on the path and trailing 373 // slashes are removed. 374 // If the path is empty, Dir returns ".". 375 // If the path consists entirely of separators, Dir returns a single separator. 376 // The returned path does not end in a separator unless it is the root directory. 377 // The default value for os is Unix. 378 func Dir(path string, os OS) string { 379 x := getOS(os) 380 vol := volumeName(path, x) 381 i := len(path) - 1 382 for i >= len(vol) && !x.IsPathSeparator(path[i]) { 383 i-- 384 } 385 dir := clean(path[len(vol):i+1], x) 386 if dir == "." && len(vol) > 2 { 387 // must be UNC 388 return vol 389 } 390 return vol + dir 391 } 392 393 // IsAbs reports whether the path is absolute. The default value for os is Unix. 394 // Note that because IsAbs has a default value, it cannot be used as 395 // a validator. 396 func IsAbs(path string, os OS) bool { 397 return getOS(os).IsAbs(path) 398 } 399 400 // VolumeName returns leading volume name. 401 // Given "C:\foo\bar" it returns "C:" on Windows. 402 // Given "\\host\share\foo" it returns "\\host\share". 403 // On other platforms it returns "". 404 // The default value for os is Windows. 405 func VolumeName(path string, os OS) string { 406 return volumeName(path, getOS(os)) 407 } 408 409 func volumeName(path string, os os) string { 410 return path[:os.volumeNameLen(path)] 411 }