github.com/evanw/esbuild@v0.21.4/internal/fs/filepath.go (about)

     1  // Code in this file has been forked from the "filepath" module in the Go
     2  // source code to work around bugs with the WebAssembly build target. More
     3  // information about why here: https://github.com/golang/go/issues/43768.
     4  
     5  ////////////////////////////////////////////////////////////////////////////////
     6  
     7  // Copyright (c) 2009 The Go Authors. All rights reserved.
     8  //
     9  // Redistribution and use in source and binary forms, with or without
    10  // modification, are permitted provided that the following conditions are
    11  // met:
    12  //
    13  //    * Redistributions of source code must retain the above copyright
    14  // notice, this list of conditions and the following disclaimer.
    15  //    * Redistributions in binary form must reproduce the above
    16  // copyright notice, this list of conditions and the following disclaimer
    17  // in the documentation and/or other materials provided with the
    18  // distribution.
    19  //    * Neither the name of Google Inc. nor the names of its
    20  // contributors may be used to endorse or promote products derived from
    21  // this software without specific prior written permission.
    22  //
    23  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    24  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    25  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    26  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    27  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    28  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    29  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    30  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    31  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    32  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    33  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    34  
    35  package fs
    36  
    37  import (
    38  	"errors"
    39  	"os"
    40  	"strings"
    41  	"syscall"
    42  )
    43  
    44  type goFilepath struct {
    45  	cwd           string
    46  	isWindows     bool
    47  	pathSeparator byte
    48  }
    49  
    50  func isSlash(c uint8) bool {
    51  	return c == '\\' || c == '/'
    52  }
    53  
    54  // reservedNames lists reserved Windows names. Search for PRN in
    55  // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
    56  // for details.
    57  var reservedNames = []string{
    58  	"CON", "PRN", "AUX", "NUL",
    59  	"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
    60  	"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
    61  }
    62  
    63  // isReservedName returns true, if path is Windows reserved name.
    64  // See reservedNames for the full list.
    65  func isReservedName(path string) bool {
    66  	if len(path) == 0 {
    67  		return false
    68  	}
    69  	for _, reserved := range reservedNames {
    70  		if strings.EqualFold(path, reserved) {
    71  			return true
    72  		}
    73  	}
    74  	return false
    75  }
    76  
    77  // IsAbs reports whether the path is absolute.
    78  func (fp goFilepath) isAbs(path string) bool {
    79  	if !fp.isWindows {
    80  		return strings.HasPrefix(path, "/")
    81  	}
    82  	if isReservedName(path) {
    83  		return true
    84  	}
    85  	l := fp.volumeNameLen(path)
    86  	if l == 0 {
    87  		return false
    88  	}
    89  	path = path[l:]
    90  	if path == "" {
    91  		return false
    92  	}
    93  	return isSlash(path[0])
    94  }
    95  
    96  // Abs returns an absolute representation of path.
    97  // If the path is not absolute it will be joined with the current
    98  // working directory to turn it into an absolute path. The absolute
    99  // path name for a given file is not guaranteed to be unique.
   100  // Abs calls Clean on the result.
   101  func (fp goFilepath) abs(path string) (string, error) {
   102  	if fp.isAbs(path) {
   103  		return fp.clean(path), nil
   104  	}
   105  	return fp.join([]string{fp.cwd, path}), nil
   106  }
   107  
   108  // IsPathSeparator reports whether c is a directory separator character.
   109  func (fp goFilepath) isPathSeparator(c uint8) bool {
   110  	return c == '/' || (fp.isWindows && c == '\\')
   111  }
   112  
   113  // volumeNameLen returns length of the leading volume name on Windows.
   114  // It returns 0 elsewhere.
   115  func (fp goFilepath) volumeNameLen(path string) int {
   116  	if !fp.isWindows {
   117  		return 0
   118  	}
   119  	if len(path) < 2 {
   120  		return 0
   121  	}
   122  	// with drive letter
   123  	c := path[0]
   124  	if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
   125  		return 2
   126  	}
   127  	// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
   128  	if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
   129  		!isSlash(path[2]) && path[2] != '.' {
   130  		// first, leading `\\` and next shouldn't be `\`. its server name.
   131  		for n := 3; n < l-1; n++ {
   132  			// second, next '\' shouldn't be repeated.
   133  			if isSlash(path[n]) {
   134  				n++
   135  				// third, following something characters. its share name.
   136  				if !isSlash(path[n]) {
   137  					if path[n] == '.' {
   138  						break
   139  					}
   140  					for ; n < l; n++ {
   141  						if isSlash(path[n]) {
   142  							break
   143  						}
   144  					}
   145  					return n
   146  				}
   147  				break
   148  			}
   149  		}
   150  	}
   151  	return 0
   152  }
   153  
   154  // EvalSymlinks returns the path name after the evaluation of any symbolic
   155  // links.
   156  // If path is relative the result will be relative to the current directory,
   157  // unless one of the components is an absolute symbolic link.
   158  // EvalSymlinks calls Clean on the result.
   159  func (fp goFilepath) evalSymlinks(path string) (string, error) {
   160  	volLen := fp.volumeNameLen(path)
   161  	pathSeparator := string(fp.pathSeparator)
   162  
   163  	if volLen < len(path) && fp.isPathSeparator(path[volLen]) {
   164  		volLen++
   165  	}
   166  	vol := path[:volLen]
   167  	dest := vol
   168  	linksWalked := 0
   169  	for start, end := volLen, volLen; start < len(path); start = end {
   170  		for start < len(path) && fp.isPathSeparator(path[start]) {
   171  			start++
   172  		}
   173  		end = start
   174  		for end < len(path) && !fp.isPathSeparator(path[end]) {
   175  			end++
   176  		}
   177  
   178  		// On Windows, "." can be a symlink.
   179  		// We look it up, and use the value if it is absolute.
   180  		// If not, we just return ".".
   181  		isWindowsDot := fp.isWindows && path[fp.volumeNameLen(path):] == "."
   182  
   183  		// The next path component is in path[start:end].
   184  		if end == start {
   185  			// No more path components.
   186  			break
   187  		} else if path[start:end] == "." && !isWindowsDot {
   188  			// Ignore path component ".".
   189  			continue
   190  		} else if path[start:end] == ".." {
   191  			// Back up to previous component if possible.
   192  			// Note that volLen includes any leading slash.
   193  
   194  			// Set r to the index of the last slash in dest,
   195  			// after the volume.
   196  			var r int
   197  			for r = len(dest) - 1; r >= volLen; r-- {
   198  				if fp.isPathSeparator(dest[r]) {
   199  					break
   200  				}
   201  			}
   202  			if r < volLen || dest[r+1:] == ".." {
   203  				// Either path has no slashes
   204  				// (it's empty or just "C:")
   205  				// or it ends in a ".." we had to keep.
   206  				// Either way, keep this "..".
   207  				if len(dest) > volLen {
   208  					dest += pathSeparator
   209  				}
   210  				dest += ".."
   211  			} else {
   212  				// Discard everything since the last slash.
   213  				dest = dest[:r]
   214  			}
   215  			continue
   216  		}
   217  
   218  		// Ordinary path component. Add it to result.
   219  
   220  		if len(dest) > fp.volumeNameLen(dest) && !fp.isPathSeparator(dest[len(dest)-1]) {
   221  			dest += pathSeparator
   222  		}
   223  
   224  		dest += path[start:end]
   225  
   226  		// Resolve symlink.
   227  
   228  		fi, err := os.Lstat(dest)
   229  		if err != nil {
   230  			return "", err
   231  		}
   232  
   233  		if fi.Mode()&os.ModeSymlink == 0 {
   234  			if !fi.Mode().IsDir() && end < len(path) {
   235  				return "", syscall.ENOTDIR
   236  			}
   237  			continue
   238  		}
   239  
   240  		// Found symlink.
   241  
   242  		linksWalked++
   243  		if linksWalked > 255 {
   244  			return "", errors.New("EvalSymlinks: too many links")
   245  		}
   246  
   247  		link, err := os.Readlink(dest)
   248  		if err != nil {
   249  			return "", err
   250  		}
   251  
   252  		if isWindowsDot && !fp.isAbs(link) {
   253  			// On Windows, if "." is a relative symlink,
   254  			// just return ".".
   255  			break
   256  		}
   257  
   258  		path = link + path[end:]
   259  
   260  		v := fp.volumeNameLen(link)
   261  		if v > 0 {
   262  			// Symlink to drive name is an absolute path.
   263  			if v < len(link) && fp.isPathSeparator(link[v]) {
   264  				v++
   265  			}
   266  			vol = link[:v]
   267  			dest = vol
   268  			end = len(vol)
   269  		} else if len(link) > 0 && fp.isPathSeparator(link[0]) {
   270  			// Symlink to absolute path.
   271  			dest = link[:1]
   272  			end = 1
   273  		} else {
   274  			// Symlink to relative path; replace last
   275  			// path component in dest.
   276  			var r int
   277  			for r = len(dest) - 1; r >= volLen; r-- {
   278  				if fp.isPathSeparator(dest[r]) {
   279  					break
   280  				}
   281  			}
   282  			if r < volLen {
   283  				dest = vol
   284  			} else {
   285  				dest = dest[:r]
   286  			}
   287  			end = 0
   288  		}
   289  	}
   290  	return fp.clean(dest), nil
   291  }
   292  
   293  // A lazybuf is a lazily constructed path buffer.
   294  // It supports append, reading previously appended bytes,
   295  // and retrieving the final string. It does not allocate a buffer
   296  // to hold the output until that output diverges from s.
   297  type lazybuf struct {
   298  	path       string
   299  	volAndPath string
   300  	buf        []byte
   301  	w          int
   302  	volLen     int
   303  }
   304  
   305  func (b *lazybuf) index(i int) byte {
   306  	if b.buf != nil {
   307  		return b.buf[i]
   308  	}
   309  	return b.path[i]
   310  }
   311  
   312  func (b *lazybuf) append(c byte) {
   313  	if b.buf == nil {
   314  		if b.w < len(b.path) && b.path[b.w] == c {
   315  			b.w++
   316  			return
   317  		}
   318  		b.buf = make([]byte, len(b.path))
   319  		copy(b.buf, b.path[:b.w])
   320  	}
   321  	b.buf[b.w] = c
   322  	b.w++
   323  }
   324  
   325  func (b *lazybuf) string() string {
   326  	if b.buf == nil {
   327  		return b.volAndPath[:b.volLen+b.w]
   328  	}
   329  	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
   330  }
   331  
   332  // FromSlash returns the result of replacing each slash ('/') character
   333  // in path with a separator character. Multiple slashes are replaced
   334  // by multiple separators.
   335  func (fp goFilepath) fromSlash(path string) string {
   336  	if !fp.isWindows {
   337  		return path
   338  	}
   339  	return strings.ReplaceAll(path, "/", "\\")
   340  }
   341  
   342  // Clean returns the shortest path name equivalent to path
   343  // by purely lexical processing. It applies the following rules
   344  // iteratively until no further processing can be done:
   345  //
   346  //  1. Replace multiple Separator elements with a single one.
   347  //  2. Eliminate each . path name element (the current directory).
   348  //  3. Eliminate each inner .. path name element (the parent directory)
   349  //     along with the non-.. element that precedes it.
   350  //  4. Eliminate .. elements that begin a rooted path:
   351  //     that is, replace "/.." by "/" at the beginning of a path,
   352  //     assuming Separator is '/'.
   353  //
   354  // The returned path ends in a slash only if it represents a root directory,
   355  // such as "/" on Unix or `C:\` on Windows.
   356  //
   357  // Finally, any occurrences of slash are replaced by Separator.
   358  //
   359  // If the result of this process is an empty string, Clean
   360  // returns the string ".".
   361  //
   362  // See also Rob Pike, "Lexical File Names in Plan 9 or
   363  // Getting Dot-Dot Right,"
   364  // https://9p.io/sys/doc/lexnames.html
   365  func (fp goFilepath) clean(path string) string {
   366  	originalPath := path
   367  	volLen := fp.volumeNameLen(path)
   368  	path = path[volLen:]
   369  	if path == "" {
   370  		if volLen > 1 && originalPath[1] != ':' {
   371  			// should be UNC
   372  			return fp.fromSlash(originalPath)
   373  		}
   374  		return originalPath + "."
   375  	}
   376  	rooted := fp.isPathSeparator(path[0])
   377  
   378  	// Invariants:
   379  	//	reading from path; r is index of next byte to process.
   380  	//	writing to buf; w is index of next byte to write.
   381  	//	dotdot is index in buf where .. must stop, either because
   382  	//		it is the leading slash or it is a leading ../../.. prefix.
   383  	n := len(path)
   384  	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
   385  	r, dotdot := 0, 0
   386  	if rooted {
   387  		out.append(fp.pathSeparator)
   388  		r, dotdot = 1, 1
   389  	}
   390  
   391  	for r < n {
   392  		switch {
   393  		case fp.isPathSeparator(path[r]):
   394  			// empty path element
   395  			r++
   396  		case path[r] == '.' && (r+1 == n || fp.isPathSeparator(path[r+1])):
   397  			// . element
   398  			r++
   399  		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || fp.isPathSeparator(path[r+2])):
   400  			// .. element: remove to last separator
   401  			r += 2
   402  			switch {
   403  			case out.w > dotdot:
   404  				// can backtrack
   405  				out.w--
   406  				for out.w > dotdot && !fp.isPathSeparator(out.index(out.w)) {
   407  					out.w--
   408  				}
   409  			case !rooted:
   410  				// cannot backtrack, but not rooted, so append .. element.
   411  				if out.w > 0 {
   412  					out.append(fp.pathSeparator)
   413  				}
   414  				out.append('.')
   415  				out.append('.')
   416  				dotdot = out.w
   417  			}
   418  		default:
   419  			// real path element.
   420  			// add slash if needed
   421  			if rooted && out.w != 1 || !rooted && out.w != 0 {
   422  				out.append(fp.pathSeparator)
   423  			}
   424  			// copy element
   425  			for ; r < n && !fp.isPathSeparator(path[r]); r++ {
   426  				out.append(path[r])
   427  			}
   428  		}
   429  	}
   430  
   431  	// Turn empty string into "."
   432  	if out.w == 0 {
   433  		out.append('.')
   434  	}
   435  
   436  	return fp.fromSlash(out.string())
   437  }
   438  
   439  // VolumeName returns leading volume name.
   440  // Given "C:\foo\bar" it returns "C:" on Windows.
   441  // Given "\\host\share\foo" it returns "\\host\share".
   442  // On other platforms it returns "".
   443  func (fp goFilepath) volumeName(path string) string {
   444  	return path[:fp.volumeNameLen(path)]
   445  }
   446  
   447  // Base returns the last element of path.
   448  // Trailing path separators are removed before extracting the last element.
   449  // If the path is empty, Base returns ".".
   450  // If the path consists entirely of separators, Base returns a single separator.
   451  func (fp goFilepath) base(path string) string {
   452  	if path == "" {
   453  		return "."
   454  	}
   455  	// Strip trailing slashes.
   456  	for len(path) > 0 && fp.isPathSeparator(path[len(path)-1]) {
   457  		path = path[0 : len(path)-1]
   458  	}
   459  	// Throw away volume name
   460  	path = path[len(fp.volumeName(path)):]
   461  	// Find the last element
   462  	i := len(path) - 1
   463  	for i >= 0 && !fp.isPathSeparator(path[i]) {
   464  		i--
   465  	}
   466  	if i >= 0 {
   467  		path = path[i+1:]
   468  	}
   469  	// If empty now, it had only slashes.
   470  	if path == "" {
   471  		return string(fp.pathSeparator)
   472  	}
   473  	return path
   474  }
   475  
   476  // Dir returns all but the last element of path, typically the path's directory.
   477  // After dropping the final element, Dir calls Clean on the path and trailing
   478  // slashes are removed.
   479  // If the path is empty, Dir returns ".".
   480  // If the path consists entirely of separators, Dir returns a single separator.
   481  // The returned path does not end in a separator unless it is the root directory.
   482  func (fp goFilepath) dir(path string) string {
   483  	vol := fp.volumeName(path)
   484  	i := len(path) - 1
   485  	for i >= len(vol) && !fp.isPathSeparator(path[i]) {
   486  		i--
   487  	}
   488  	dir := fp.clean(path[len(vol) : i+1])
   489  	if dir == "." && len(vol) > 2 {
   490  		// must be UNC
   491  		return vol
   492  	}
   493  	return vol + dir
   494  }
   495  
   496  // Ext returns the file name extension used by path.
   497  // The extension is the suffix beginning at the final dot
   498  // in the final element of path; it is empty if there is
   499  // no dot.
   500  func (fp goFilepath) ext(path string) string {
   501  	for i := len(path) - 1; i >= 0 && !fp.isPathSeparator(path[i]); i-- {
   502  		if path[i] == '.' {
   503  			return path[i:]
   504  		}
   505  	}
   506  	return ""
   507  }
   508  
   509  // Join joins any number of path elements into a single path,
   510  // separating them with an OS specific Separator. Empty elements
   511  // are ignored. The result is Cleaned. However, if the argument
   512  // list is empty or all its elements are empty, Join returns
   513  // an empty string.
   514  // On Windows, the result will only be a UNC path if the first
   515  // non-empty element is a UNC path.
   516  func (fp goFilepath) join(elem []string) string {
   517  	for i, e := range elem {
   518  		if e != "" {
   519  			if fp.isWindows {
   520  				return fp.joinNonEmpty(elem[i:])
   521  			}
   522  			return fp.clean(strings.Join(elem[i:], string(fp.pathSeparator)))
   523  		}
   524  	}
   525  	return ""
   526  }
   527  
   528  // joinNonEmpty is like join, but it assumes that the first element is non-empty.
   529  func (fp goFilepath) joinNonEmpty(elem []string) string {
   530  	if len(elem[0]) == 2 && elem[0][1] == ':' {
   531  		// First element is drive letter without terminating slash.
   532  		// Keep path relative to current directory on that drive.
   533  		// Skip empty elements.
   534  		i := 1
   535  		for ; i < len(elem); i++ {
   536  			if elem[i] != "" {
   537  				break
   538  			}
   539  		}
   540  		return fp.clean(elem[0] + strings.Join(elem[i:], string(fp.pathSeparator)))
   541  	}
   542  	// The following logic prevents Join from inadvertently creating a
   543  	// UNC path on Windows. Unless the first element is a UNC path, Join
   544  	// shouldn't create a UNC path. See golang.org/issue/9167.
   545  	p := fp.clean(strings.Join(elem, string(fp.pathSeparator)))
   546  	if !fp.isUNC(p) {
   547  		return p
   548  	}
   549  	// p == UNC only allowed when the first element is a UNC path.
   550  	head := fp.clean(elem[0])
   551  	if fp.isUNC(head) {
   552  		return p
   553  	}
   554  	// head + tail == UNC, but joining two non-UNC paths should not result
   555  	// in a UNC path. Undo creation of UNC path.
   556  	tail := fp.clean(strings.Join(elem[1:], string(fp.pathSeparator)))
   557  	if head[len(head)-1] == fp.pathSeparator {
   558  		return head + tail
   559  	}
   560  	return head + string(fp.pathSeparator) + tail
   561  }
   562  
   563  // isUNC reports whether path is a UNC path.
   564  func (fp goFilepath) isUNC(path string) bool {
   565  	return fp.volumeNameLen(path) > 2
   566  }
   567  
   568  // Rel returns a relative path that is lexically equivalent to targpath when
   569  // joined to basepath with an intervening separator. That is,
   570  // Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
   571  // On success, the returned path will always be relative to basepath,
   572  // even if basepath and targpath share no elements.
   573  // An error is returned if targpath can't be made relative to basepath or if
   574  // knowing the current working directory would be necessary to compute it.
   575  // Rel calls Clean on the result.
   576  func (fp goFilepath) rel(basepath, targpath string) (string, error) {
   577  	baseVol := fp.volumeName(basepath)
   578  	targVol := fp.volumeName(targpath)
   579  	base := fp.clean(basepath)
   580  	targ := fp.clean(targpath)
   581  	if fp.sameWord(targ, base) {
   582  		return ".", nil
   583  	}
   584  	base = base[len(baseVol):]
   585  	targ = targ[len(targVol):]
   586  	if base == "." {
   587  		base = ""
   588  	}
   589  	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
   590  	baseSlashed := len(base) > 0 && base[0] == fp.pathSeparator
   591  	targSlashed := len(targ) > 0 && targ[0] == fp.pathSeparator
   592  	if baseSlashed != targSlashed || !fp.sameWord(baseVol, targVol) {
   593  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   594  	}
   595  	// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
   596  	bl := len(base)
   597  	tl := len(targ)
   598  	var b0, bi, t0, ti int
   599  	for {
   600  		for bi < bl && base[bi] != fp.pathSeparator {
   601  			bi++
   602  		}
   603  		for ti < tl && targ[ti] != fp.pathSeparator {
   604  			ti++
   605  		}
   606  		if !fp.sameWord(targ[t0:ti], base[b0:bi]) {
   607  			break
   608  		}
   609  		if bi < bl {
   610  			bi++
   611  		}
   612  		if ti < tl {
   613  			ti++
   614  		}
   615  		b0 = bi
   616  		t0 = ti
   617  	}
   618  	if base[b0:bi] == ".." {
   619  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   620  	}
   621  	if b0 != bl {
   622  		// Base elements left. Must go up before going down.
   623  		seps := strings.Count(base[b0:bl], string(fp.pathSeparator))
   624  		size := 2 + seps*3
   625  		if tl != t0 {
   626  			size += 1 + tl - t0
   627  		}
   628  		buf := make([]byte, size)
   629  		n := copy(buf, "..")
   630  		for i := 0; i < seps; i++ {
   631  			buf[n] = fp.pathSeparator
   632  			copy(buf[n+1:], "..")
   633  			n += 3
   634  		}
   635  		if t0 != tl {
   636  			buf[n] = fp.pathSeparator
   637  			copy(buf[n+1:], targ[t0:])
   638  		}
   639  		return string(buf), nil
   640  	}
   641  	return targ[t0:], nil
   642  }
   643  
   644  func (fp goFilepath) sameWord(a, b string) bool {
   645  	if !fp.isWindows {
   646  		return a == b
   647  	}
   648  	return strings.EqualFold(a, b)
   649  }