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  }