path.mx raw

   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       []byte
  26  	buf        []byte
  27  	w          int
  28  	volAndPath []byte
  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 = []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() []byte {
  58  	if b.buf == nil {
  59  		return b.volAndPath[:b.volLen+b.w]
  60  	}
  61  	return b.volAndPath[:b.volLen] + []byte(b.buf[:b.w])
  62  }
  63  
  64  // Clean is filepath.Clean.
  65  func Clean(path []byte) []byte {
  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 []byte) bool {
 142  	return isLocal(path)
 143  }
 144  
 145  func unixIsLocal(path []byte) bool {
 146  	if IsAbs(path) || path == "" {
 147  		return false
 148  	}
 149  	hasDots := false
 150  	for p := path; p != ""; {
 151  		var part []byte
 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 []byte) ([]byte, 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 []byte) []byte {
 177  	if Separator == '/' {
 178  		return path
 179  	}
 180  	return replaceStringByte(path, Separator, '/')
 181  }
 182  
 183  // FromSlash is filepath.FromSlash.
 184  func FromSlash(path []byte) []byte {
 185  	if Separator == '/' {
 186  		return path
 187  	}
 188  	return replaceStringByte(path, '/', Separator)
 189  }
 190  
 191  func replaceStringByte(s []byte, old, new byte) []byte {
 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 []byte(n)
 202  }
 203  
 204  // Split is filepath.Split.
 205  func Split(path []byte) (dir, file []byte) {
 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 []byte) []byte {
 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 []byte) []byte {
 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 []byte{byte(Separator)}
 246  	}
 247  	return path
 248  }
 249  
 250  // Dir is filepath.Dir.
 251  func Dir(path []byte) []byte {
 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 []byte) []byte {
 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 []byte) int {
 273  	return volumeNameLen(path)
 274  }
 275