github.com/dorkamotorka/go/src@v0.0.0-20230614113921-187095f0e316/path/filepath/path.go (about)

     1  // Copyright 2009 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 filepath implements utility routines for manipulating filename paths
     6  // in a way compatible with the target operating system-defined file paths.
     7  //
     8  // The filepath package uses either forward slashes or backslashes,
     9  // depending on the operating system. To process paths such as URLs
    10  // that always use forward slashes regardless of the operating
    11  // system, see the [path] package.
    12  package filepath
    13  
    14  import (
    15  	"errors"
    16  	"io/fs"
    17  	"os"
    18  	"runtime"
    19  	"sort"
    20  	"strings"
    21  )
    22  
    23  // A lazybuf is a lazily constructed path buffer.
    24  // It supports append, reading previously appended bytes,
    25  // and retrieving the final string. It does not allocate a buffer
    26  // to hold the output until that output diverges from s.
    27  type lazybuf struct {
    28  	path       string
    29  	buf        []byte
    30  	w          int
    31  	volAndPath string
    32  	volLen     int
    33  }
    34  
    35  func (b *lazybuf) index(i int) byte {
    36  	if b.buf != nil {
    37  		return b.buf[i]
    38  	}
    39  	return b.path[i]
    40  }
    41  
    42  func (b *lazybuf) append(c byte) {
    43  	if b.buf == nil {
    44  		if b.w < len(b.path) && b.path[b.w] == c {
    45  			b.w++
    46  			return
    47  		}
    48  		b.buf = make([]byte, len(b.path))
    49  		copy(b.buf, b.path[:b.w])
    50  	}
    51  	b.buf[b.w] = c
    52  	b.w++
    53  }
    54  
    55  func (b *lazybuf) string() string {
    56  	if b.buf == nil {
    57  		return b.volAndPath[:b.volLen+b.w]
    58  	}
    59  	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
    60  }
    61  
    62  const (
    63  	Separator     = os.PathSeparator
    64  	ListSeparator = os.PathListSeparator
    65  )
    66  
    67  // Clean returns the shortest path name equivalent to path
    68  // by purely lexical processing. It applies the following rules
    69  // iteratively until no further processing can be done:
    70  //
    71  //  1. Replace multiple Separator elements with a single one.
    72  //  2. Eliminate each . path name element (the current directory).
    73  //  3. Eliminate each inner .. path name element (the parent directory)
    74  //     along with the non-.. element that precedes it.
    75  //  4. Eliminate .. elements that begin a rooted path:
    76  //     that is, replace "/.." by "/" at the beginning of a path,
    77  //     assuming Separator is '/'.
    78  //
    79  // The returned path ends in a slash only if it represents a root directory,
    80  // such as "/" on Unix or `C:\` on Windows.
    81  //
    82  // Finally, any occurrences of slash are replaced by Separator.
    83  //
    84  // If the result of this process is an empty string, Clean
    85  // returns the string ".".
    86  //
    87  // On Windows, Clean does not modify the volume name other than to replace
    88  // occurrences of "/" with `\`.
    89  // For example, Clean("//host/share/../x") returns `\\host\share\x`.
    90  //
    91  // See also Rob Pike, “Lexical File Names in Plan 9 or
    92  // Getting Dot-Dot Right,”
    93  // https://9p.io/sys/doc/lexnames.html
    94  func Clean(path string) string {
    95  	originalPath := path
    96  	volLen := volumeNameLen(path)
    97  	path = path[volLen:]
    98  	if path == "" {
    99  		if volLen > 1 && os.IsPathSeparator(originalPath[0]) && os.IsPathSeparator(originalPath[1]) {
   100  			// should be UNC
   101  			return FromSlash(originalPath)
   102  		}
   103  		return originalPath + "."
   104  	}
   105  	rooted := os.IsPathSeparator(path[0])
   106  
   107  	// Invariants:
   108  	//	reading from path; r is index of next byte to process.
   109  	//	writing to buf; w is index of next byte to write.
   110  	//	dotdot is index in buf where .. must stop, either because
   111  	//		it is the leading slash or it is a leading ../../.. prefix.
   112  	n := len(path)
   113  	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
   114  	r, dotdot := 0, 0
   115  	if rooted {
   116  		out.append(Separator)
   117  		r, dotdot = 1, 1
   118  	}
   119  
   120  	for r < n {
   121  		switch {
   122  		case os.IsPathSeparator(path[r]):
   123  			// empty path element
   124  			r++
   125  		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
   126  			// . element
   127  			r++
   128  		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
   129  			// .. element: remove to last separator
   130  			r += 2
   131  			switch {
   132  			case out.w > dotdot:
   133  				// can backtrack
   134  				out.w--
   135  				for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) {
   136  					out.w--
   137  				}
   138  			case !rooted:
   139  				// cannot backtrack, but not rooted, so append .. element.
   140  				if out.w > 0 {
   141  					out.append(Separator)
   142  				}
   143  				out.append('.')
   144  				out.append('.')
   145  				dotdot = out.w
   146  			}
   147  		default:
   148  			// real path element.
   149  			// add slash if needed
   150  			if rooted && out.w != 1 || !rooted && out.w != 0 {
   151  				out.append(Separator)
   152  			}
   153  			// If a ':' appears in the path element at the start of a Windows path,
   154  			// insert a .\ at the beginning to avoid converting relative paths
   155  			// like a/../c: into c:.
   156  			if runtime.GOOS == "windows" && out.w == 0 && out.volLen == 0 && r != 0 {
   157  				for i := r; i < n && !os.IsPathSeparator(path[i]); i++ {
   158  					if path[i] == ':' {
   159  						out.append('.')
   160  						out.append(Separator)
   161  						break
   162  					}
   163  				}
   164  			}
   165  			// copy element
   166  			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
   167  				out.append(path[r])
   168  			}
   169  		}
   170  	}
   171  
   172  	// Turn empty string into "."
   173  	if out.w == 0 {
   174  		out.append('.')
   175  	}
   176  
   177  	return FromSlash(out.string())
   178  }
   179  
   180  // IsLocal reports whether path, using lexical analysis only, has all of these properties:
   181  //
   182  //   - is within the subtree rooted at the directory in which path is evaluated
   183  //   - is not an absolute path
   184  //   - is not empty
   185  //   - on Windows, is not a reserved name such as "NUL"
   186  //
   187  // If IsLocal(path) returns true, then
   188  // Join(base, path) will always produce a path contained within base and
   189  // Clean(path) will always produce an unrooted path with no ".." path elements.
   190  //
   191  // IsLocal is a purely lexical operation.
   192  // In particular, it does not account for the effect of any symbolic links
   193  // that may exist in the filesystem.
   194  func IsLocal(path string) bool {
   195  	return isLocal(path)
   196  }
   197  
   198  func unixIsLocal(path string) bool {
   199  	if IsAbs(path) || path == "" {
   200  		return false
   201  	}
   202  	hasDots := false
   203  	for p := path; p != ""; {
   204  		var part string
   205  		part, p, _ = strings.Cut(p, "/")
   206  		if part == "." || part == ".." {
   207  			hasDots = true
   208  			break
   209  		}
   210  	}
   211  	if hasDots {
   212  		path = Clean(path)
   213  	}
   214  	if path == ".." || strings.HasPrefix(path, "../") {
   215  		return false
   216  	}
   217  	return true
   218  }
   219  
   220  // ToSlash returns the result of replacing each separator character
   221  // in path with a slash ('/') character. Multiple separators are
   222  // replaced by multiple slashes.
   223  func ToSlash(path string) string {
   224  	if Separator == '/' {
   225  		return path
   226  	}
   227  	return strings.ReplaceAll(path, string(Separator), "/")
   228  }
   229  
   230  // FromSlash returns the result of replacing each slash ('/') character
   231  // in path with a separator character. Multiple slashes are replaced
   232  // by multiple separators.
   233  func FromSlash(path string) string {
   234  	if Separator == '/' {
   235  		return path
   236  	}
   237  	return strings.ReplaceAll(path, "/", string(Separator))
   238  }
   239  
   240  // SplitList splits a list of paths joined by the OS-specific ListSeparator,
   241  // usually found in PATH or GOPATH environment variables.
   242  // Unlike strings.Split, SplitList returns an empty slice when passed an empty
   243  // string.
   244  func SplitList(path string) []string {
   245  	return splitList(path)
   246  }
   247  
   248  // Split splits path immediately following the final Separator,
   249  // separating it into a directory and file name component.
   250  // If there is no Separator in path, Split returns an empty dir
   251  // and file set to path.
   252  // The returned values have the property that path = dir+file.
   253  func Split(path string) (dir, file string) {
   254  	vol := VolumeName(path)
   255  	i := len(path) - 1
   256  	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
   257  		i--
   258  	}
   259  	return path[:i+1], path[i+1:]
   260  }
   261  
   262  // Join joins any number of path elements into a single path,
   263  // separating them with an OS specific Separator. Empty elements
   264  // are ignored. The result is Cleaned. However, if the argument
   265  // list is empty or all its elements are empty, Join returns
   266  // an empty string.
   267  // On Windows, the result will only be a UNC path if the first
   268  // non-empty element is a UNC path.
   269  func Join(elem ...string) string {
   270  	return join(elem)
   271  }
   272  
   273  // Ext returns the file name extension used by path.
   274  // The extension is the suffix beginning at the final dot
   275  // in the final element of path; it is empty if there is
   276  // no dot.
   277  func Ext(path string) string {
   278  	for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
   279  		if path[i] == '.' {
   280  			return path[i:]
   281  		}
   282  	}
   283  	return ""
   284  }
   285  
   286  // EvalSymlinks returns the path name after the evaluation of any symbolic
   287  // links.
   288  // If path is relative the result will be relative to the current directory,
   289  // unless one of the components is an absolute symbolic link.
   290  // EvalSymlinks calls Clean on the result.
   291  func EvalSymlinks(path string) (string, error) {
   292  	return evalSymlinks(path)
   293  }
   294  
   295  // Abs returns an absolute representation of path.
   296  // If the path is not absolute it will be joined with the current
   297  // working directory to turn it into an absolute path. The absolute
   298  // path name for a given file is not guaranteed to be unique.
   299  // Abs calls Clean on the result.
   300  func Abs(path string) (string, error) {
   301  	return abs(path)
   302  }
   303  
   304  func unixAbs(path string) (string, error) {
   305  	if IsAbs(path) {
   306  		return Clean(path), nil
   307  	}
   308  	wd, err := os.Getwd()
   309  	if err != nil {
   310  		return "", err
   311  	}
   312  	return Join(wd, path), nil
   313  }
   314  
   315  // Rel returns a relative path that is lexically equivalent to targpath when
   316  // joined to basepath with an intervening separator. That is,
   317  // Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
   318  // On success, the returned path will always be relative to basepath,
   319  // even if basepath and targpath share no elements.
   320  // An error is returned if targpath can't be made relative to basepath or if
   321  // knowing the current working directory would be necessary to compute it.
   322  // Rel calls Clean on the result.
   323  func Rel(basepath, targpath string) (string, error) {
   324  	baseVol := VolumeName(basepath)
   325  	targVol := VolumeName(targpath)
   326  	base := Clean(basepath)
   327  	targ := Clean(targpath)
   328  	if sameWord(targ, base) {
   329  		return ".", nil
   330  	}
   331  	base = base[len(baseVol):]
   332  	targ = targ[len(targVol):]
   333  	if base == "." {
   334  		base = ""
   335  	} else if base == "" && volumeNameLen(baseVol) > 2 /* isUNC */ {
   336  		// Treat any targetpath matching `\\host\share` basepath as absolute path.
   337  		base = string(Separator)
   338  	}
   339  
   340  	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
   341  	baseSlashed := len(base) > 0 && base[0] == Separator
   342  	targSlashed := len(targ) > 0 && targ[0] == Separator
   343  	if baseSlashed != targSlashed || !sameWord(baseVol, targVol) {
   344  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   345  	}
   346  	// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
   347  	bl := len(base)
   348  	tl := len(targ)
   349  	var b0, bi, t0, ti int
   350  	for {
   351  		for bi < bl && base[bi] != Separator {
   352  			bi++
   353  		}
   354  		for ti < tl && targ[ti] != Separator {
   355  			ti++
   356  		}
   357  		if !sameWord(targ[t0:ti], base[b0:bi]) {
   358  			break
   359  		}
   360  		if bi < bl {
   361  			bi++
   362  		}
   363  		if ti < tl {
   364  			ti++
   365  		}
   366  		b0 = bi
   367  		t0 = ti
   368  	}
   369  	if base[b0:bi] == ".." {
   370  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   371  	}
   372  	if b0 != bl {
   373  		// Base elements left. Must go up before going down.
   374  		seps := strings.Count(base[b0:bl], string(Separator))
   375  		size := 2 + seps*3
   376  		if tl != t0 {
   377  			size += 1 + tl - t0
   378  		}
   379  		buf := make([]byte, size)
   380  		n := copy(buf, "..")
   381  		for i := 0; i < seps; i++ {
   382  			buf[n] = Separator
   383  			copy(buf[n+1:], "..")
   384  			n += 3
   385  		}
   386  		if t0 != tl {
   387  			buf[n] = Separator
   388  			copy(buf[n+1:], targ[t0:])
   389  		}
   390  		return string(buf), nil
   391  	}
   392  	return targ[t0:], nil
   393  }
   394  
   395  // SkipDir is used as a return value from WalkFuncs to indicate that
   396  // the directory named in the call is to be skipped. It is not returned
   397  // as an error by any function.
   398  var SkipDir error = fs.SkipDir
   399  
   400  // SkipAll is used as a return value from WalkFuncs to indicate that
   401  // all remaining files and directories are to be skipped. It is not returned
   402  // as an error by any function.
   403  var SkipAll error = fs.SkipAll
   404  
   405  // WalkFunc is the type of the function called by Walk to visit each
   406  // file or directory.
   407  //
   408  // The path argument contains the argument to Walk as a prefix.
   409  // That is, if Walk is called with root argument "dir" and finds a file
   410  // named "a" in that directory, the walk function will be called with
   411  // argument "dir/a".
   412  //
   413  // The directory and file are joined with Join, which may clean the
   414  // directory name: if Walk is called with the root argument "x/../dir"
   415  // and finds a file named "a" in that directory, the walk function will
   416  // be called with argument "dir/a", not "x/../dir/a".
   417  //
   418  // The info argument is the fs.FileInfo for the named path.
   419  //
   420  // The error result returned by the function controls how Walk continues.
   421  // If the function returns the special value SkipDir, Walk skips the
   422  // current directory (path if info.IsDir() is true, otherwise path's
   423  // parent directory). If the function returns the special value SkipAll,
   424  // Walk skips all remaining files and directories. Otherwise, if the function
   425  // returns a non-nil error, Walk stops entirely and returns that error.
   426  //
   427  // The err argument reports an error related to path, signaling that Walk
   428  // will not walk into that directory. The function can decide how to
   429  // handle that error; as described earlier, returning the error will
   430  // cause Walk to stop walking the entire tree.
   431  //
   432  // Walk calls the function with a non-nil err argument in two cases.
   433  //
   434  // First, if an os.Lstat on the root directory or any directory or file
   435  // in the tree fails, Walk calls the function with path set to that
   436  // directory or file's path, info set to nil, and err set to the error
   437  // from os.Lstat.
   438  //
   439  // Second, if a directory's Readdirnames method fails, Walk calls the
   440  // function with path set to the directory's path, info, set to an
   441  // fs.FileInfo describing the directory, and err set to the error from
   442  // Readdirnames.
   443  type WalkFunc func(path string, info fs.FileInfo, err error) error
   444  
   445  var lstat = os.Lstat // for testing
   446  
   447  // walkDir recursively descends path, calling walkDirFn.
   448  func walkDir(path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
   449  	if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
   450  		if err == SkipDir && d.IsDir() {
   451  			// Successfully skipped directory.
   452  			err = nil
   453  		}
   454  		return err
   455  	}
   456  
   457  	dirs, err := readDir(path)
   458  	if err != nil {
   459  		// Second call, to report ReadDir error.
   460  		err = walkDirFn(path, d, err)
   461  		if err != nil {
   462  			if err == SkipDir && d.IsDir() {
   463  				err = nil
   464  			}
   465  			return err
   466  		}
   467  	}
   468  
   469  	for _, d1 := range dirs {
   470  		path1 := Join(path, d1.Name())
   471  		if err := walkDir(path1, d1, walkDirFn); err != nil {
   472  			if err == SkipDir {
   473  				break
   474  			}
   475  			return err
   476  		}
   477  	}
   478  	return nil
   479  }
   480  
   481  // walk recursively descends path, calling walkFn.
   482  func walk(path string, info fs.FileInfo, walkFn WalkFunc) error {
   483  	if !info.IsDir() {
   484  		return walkFn(path, info, nil)
   485  	}
   486  
   487  	names, err := readDirNames(path)
   488  	err1 := walkFn(path, info, err)
   489  	// If err != nil, walk can't walk into this directory.
   490  	// err1 != nil means walkFn want walk to skip this directory or stop walking.
   491  	// Therefore, if one of err and err1 isn't nil, walk will return.
   492  	if err != nil || err1 != nil {
   493  		// The caller's behavior is controlled by the return value, which is decided
   494  		// by walkFn. walkFn may ignore err and return nil.
   495  		// If walkFn returns SkipDir or SkipAll, it will be handled by the caller.
   496  		// So walk should return whatever walkFn returns.
   497  		return err1
   498  	}
   499  
   500  	for _, name := range names {
   501  		filename := Join(path, name)
   502  		fileInfo, err := lstat(filename)
   503  		if err != nil {
   504  			if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
   505  				return err
   506  			}
   507  		} else {
   508  			err = walk(filename, fileInfo, walkFn)
   509  			if err != nil {
   510  				if !fileInfo.IsDir() || err != SkipDir {
   511  					return err
   512  				}
   513  			}
   514  		}
   515  	}
   516  	return nil
   517  }
   518  
   519  // WalkDir walks the file tree rooted at root, calling fn for each file or
   520  // directory in the tree, including root.
   521  //
   522  // All errors that arise visiting files and directories are filtered by fn:
   523  // see the fs.WalkDirFunc documentation for details.
   524  //
   525  // The files are walked in lexical order, which makes the output deterministic
   526  // but requires WalkDir to read an entire directory into memory before proceeding
   527  // to walk that directory.
   528  //
   529  // WalkDir does not follow symbolic links.
   530  //
   531  // WalkDir calls fn with paths that use the separator character appropriate
   532  // for the operating system. This is unlike [io/fs.WalkDir], which always
   533  // uses slash separated paths.
   534  func WalkDir(root string, fn fs.WalkDirFunc) error {
   535  	info, err := os.Lstat(root)
   536  	if err != nil {
   537  		err = fn(root, nil, err)
   538  	} else {
   539  		err = walkDir(root, &statDirEntry{info}, fn)
   540  	}
   541  	if err == SkipDir || err == SkipAll {
   542  		return nil
   543  	}
   544  	return err
   545  }
   546  
   547  type statDirEntry struct {
   548  	info fs.FileInfo
   549  }
   550  
   551  func (d *statDirEntry) Name() string               { return d.info.Name() }
   552  func (d *statDirEntry) IsDir() bool                { return d.info.IsDir() }
   553  func (d *statDirEntry) Type() fs.FileMode          { return d.info.Mode().Type() }
   554  func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
   555  
   556  func (d *statDirEntry) String() string {
   557  	return fs.FormatDirEntry(d)
   558  }
   559  
   560  // Walk walks the file tree rooted at root, calling fn for each file or
   561  // directory in the tree, including root.
   562  //
   563  // All errors that arise visiting files and directories are filtered by fn:
   564  // see the WalkFunc documentation for details.
   565  //
   566  // The files are walked in lexical order, which makes the output deterministic
   567  // but requires Walk to read an entire directory into memory before proceeding
   568  // to walk that directory.
   569  //
   570  // Walk does not follow symbolic links.
   571  //
   572  // Walk is less efficient than WalkDir, introduced in Go 1.16,
   573  // which avoids calling os.Lstat on every visited file or directory.
   574  func Walk(root string, fn WalkFunc) error {
   575  	info, err := os.Lstat(root)
   576  	if err != nil {
   577  		err = fn(root, nil, err)
   578  	} else {
   579  		err = walk(root, info, fn)
   580  	}
   581  	if err == SkipDir || err == SkipAll {
   582  		return nil
   583  	}
   584  	return err
   585  }
   586  
   587  // readDir reads the directory named by dirname and returns
   588  // a sorted list of directory entries.
   589  func readDir(dirname string) ([]fs.DirEntry, error) {
   590  	f, err := os.Open(dirname)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	dirs, err := f.ReadDir(-1)
   595  	f.Close()
   596  	if err != nil {
   597  		return nil, err
   598  	}
   599  	sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
   600  	return dirs, nil
   601  }
   602  
   603  // readDirNames reads the directory named by dirname and returns
   604  // a sorted list of directory entry names.
   605  func readDirNames(dirname string) ([]string, error) {
   606  	f, err := os.Open(dirname)
   607  	if err != nil {
   608  		return nil, err
   609  	}
   610  	names, err := f.Readdirnames(-1)
   611  	f.Close()
   612  	if err != nil {
   613  		return nil, err
   614  	}
   615  	sort.Strings(names)
   616  	return names, nil
   617  }
   618  
   619  // Base returns the last element of path.
   620  // Trailing path separators are removed before extracting the last element.
   621  // If the path is empty, Base returns ".".
   622  // If the path consists entirely of separators, Base returns a single separator.
   623  func Base(path string) string {
   624  	if path == "" {
   625  		return "."
   626  	}
   627  	// Strip trailing slashes.
   628  	for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
   629  		path = path[0 : len(path)-1]
   630  	}
   631  	// Throw away volume name
   632  	path = path[len(VolumeName(path)):]
   633  	// Find the last element
   634  	i := len(path) - 1
   635  	for i >= 0 && !os.IsPathSeparator(path[i]) {
   636  		i--
   637  	}
   638  	if i >= 0 {
   639  		path = path[i+1:]
   640  	}
   641  	// If empty now, it had only slashes.
   642  	if path == "" {
   643  		return string(Separator)
   644  	}
   645  	return path
   646  }
   647  
   648  // Dir returns all but the last element of path, typically the path's directory.
   649  // After dropping the final element, Dir calls Clean on the path and trailing
   650  // slashes are removed.
   651  // If the path is empty, Dir returns ".".
   652  // If the path consists entirely of separators, Dir returns a single separator.
   653  // The returned path does not end in a separator unless it is the root directory.
   654  func Dir(path string) string {
   655  	vol := VolumeName(path)
   656  	i := len(path) - 1
   657  	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
   658  		i--
   659  	}
   660  	dir := Clean(path[len(vol) : i+1])
   661  	if dir == "." && len(vol) > 2 {
   662  		// must be UNC
   663  		return vol
   664  	}
   665  	return vol + dir
   666  }
   667  
   668  // VolumeName returns leading volume name.
   669  // Given "C:\foo\bar" it returns "C:" on Windows.
   670  // Given "\\host\share\foo" it returns "\\host\share".
   671  // On other platforms it returns "".
   672  func VolumeName(path string) string {
   673  	return FromSlash(path[:volumeNameLen(path)])
   674  }