Source file src/internal/filepathlite/path.go

     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  

View as plain text