go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/system/filesystem/filesystem.go (about)

     1  // Copyright 2017 The LUCI 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  package filesystem
    16  
    17  import (
    18  	"io"
    19  	"io/ioutil"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime"
    23  	"sort"
    24  	"strings"
    25  	"syscall"
    26  	"time"
    27  
    28  	"go.chromium.org/luci/common/data/sortby"
    29  	"go.chromium.org/luci/common/errors"
    30  )
    31  
    32  // IsNotExist calls os.IsNotExist on the unwrapped err.
    33  func IsNotExist(err error) bool { return os.IsNotExist(errors.Unwrap(err)) }
    34  
    35  // MakeDirs is a convenience wrapper around os.MkdirAll that applies a 0755
    36  // mask to all created directories.
    37  func MakeDirs(path string) error {
    38  	if err := os.MkdirAll(path, 0755); err != nil {
    39  		return errors.Annotate(err, "").Err()
    40  	}
    41  	return nil
    42  }
    43  
    44  // AbsPath is a convenience wrapper around filepath.Abs that accepts a string
    45  // pointer, base, and updates it on successful resolution.
    46  func AbsPath(base *string) error {
    47  	v, err := filepath.Abs(*base)
    48  	if err != nil {
    49  		return errors.Annotate(err, "unable to resolve absolute path").
    50  			InternalReason("base(%q)", *base).Err()
    51  	}
    52  	*base = v
    53  	return nil
    54  }
    55  
    56  // Touch creates a new, empty file at the specified path.
    57  //
    58  // If when is zero-value, time.Now will be used.
    59  func Touch(path string, when time.Time, mode os.FileMode) error {
    60  	// Try and create a file at the target path.
    61  	fd, err := os.OpenFile(path, (os.O_CREATE | os.O_RDWR | os.O_EXCL), mode)
    62  	if err == nil {
    63  		if err := fd.Close(); err != nil {
    64  			return errors.Annotate(err, "failed to close new file").Err()
    65  		}
    66  		if when.IsZero() {
    67  			// If "now" was specified, and we created a new file, then its times will
    68  			// be now by default.
    69  			return nil
    70  		}
    71  	}
    72  
    73  	// Couldn't create a new file. Either it exists already, it is a directory,
    74  	// or there was an OS-level failure. Since we can't really distinguish
    75  	// between these cases, try Chtimes (update timestamp) and error
    76  	// if this fails.
    77  	if when.IsZero() {
    78  		when = time.Now()
    79  	}
    80  	if err := os.Chtimes(path, when, when); err != nil {
    81  		return errors.Annotate(err, "failed to Chtimes").InternalReason("path(%q)", path).Err()
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  // RemoveAll is a fork of os.RemoveAll that attempts to deal with read only
    88  // files and directories by modifying permissions as necessary.
    89  //
    90  // If the specified path does not exist, RemoveAll will return nil.
    91  //
    92  // Note that RemoveAll will not modify permissions on parent directory of the
    93  // provided path, even if it is read only and preventing deletion of the path on
    94  // POSIX system.
    95  //
    96  // Copied from
    97  // https://go.googlesource.com/go/+/b86e76681366447798c94abb959bb60875bcc856/src/os/path.go#63
    98  func RemoveAll(path string) error {
    99  	const isWin = runtime.GOOS == "windows"
   100  	// Simple case: try removing as if it was a file or empty directory.
   101  	var err error
   102  	if isWin {
   103  		// In theory this call should not be necessary. os.Remove() already
   104  		// tries to remove the FILE_ATTRIBUTE_READONLY attribute at
   105  		// https://go.googlesource.com/go/+blame/go1.13/src/os/file_windows.go#296.
   106  		// In practice this doesn't work in one case, when it is a symlink that
   107  		// points to a missing file. In this case, ErrNotExist is returned, but
   108  		// the function call is still needed for the os.Remove() to work below.
   109  		err = MakePathUserWritable(path, nil)
   110  	}
   111  	if err == nil || IsNotExist(err) {
   112  		// On Windows, invalid symlink is treated as not exist error, but need to
   113  		// remove that.
   114  		err = os.Remove(path)
   115  	}
   116  	if err == nil || IsNotExist(err) {
   117  		return nil
   118  	}
   119  
   120  	// Otherwise, is this a directory we need to recurse into?
   121  	dir, serr := os.Lstat(path)
   122  	if serr != nil {
   123  		if serr, ok := serr.(*os.PathError); ok && (IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
   124  			return nil
   125  		}
   126  		return serr
   127  	}
   128  	if !dir.IsDir() {
   129  		// Not a directory; return the error from Remove.
   130  		return err
   131  	}
   132  	// Directory.
   133  	if !isWin {
   134  		// On POSIX systems, the directory must have write access for its files to
   135  		// be deleted. Best effort attempt to make it writable.
   136  		_ = MakePathUserWritable(path, dir)
   137  	}
   138  	fd, err := os.Open(path)
   139  	if err != nil {
   140  		if IsNotExist(err) {
   141  			// Race. It was deleted between the Lstat and Open.
   142  			// Return nil per RemoveAll's docs.
   143  			return nil
   144  		}
   145  		return err
   146  	}
   147  	// Remove contents & return first error.
   148  	err = nil
   149  	for {
   150  		if err == nil && (runtime.GOOS == "plan9" || runtime.GOOS == "nacl") {
   151  			// Reset read offset after removing directory entries.
   152  			// See golang.org/issue/22572.
   153  			fd.Seek(0, 0)
   154  		}
   155  		names, err1 := fd.Readdirnames(100)
   156  		for _, name := range names {
   157  			err1 := RemoveAll(path + string(os.PathSeparator) + name)
   158  			if err == nil {
   159  				err = err1
   160  			}
   161  		}
   162  		if err1 == io.EOF {
   163  			break
   164  		}
   165  		// If Readdirnames returned an error, use it.
   166  		if err == nil {
   167  			err = err1
   168  		}
   169  		if len(names) == 0 {
   170  			break
   171  		}
   172  	}
   173  	// Close directory, because windows won't remove opened directory.
   174  	fd.Close()
   175  	// Remove directory.
   176  	err1 := os.Remove(path)
   177  	if err1 == nil || IsNotExist(err1) {
   178  		return nil
   179  	}
   180  	if err == nil {
   181  		err = err1
   182  	}
   183  	return err
   184  }
   185  
   186  // RenamingRemoveAll opportunistically renames a path first, and then removes it.
   187  //
   188  // The advantage over RemoveAll is, if renaming succeeds, lower chance of
   189  // interference from other writers/readers of the filesystem.
   190  // If renaming fails, removes the original path via RemoveAll.
   191  //
   192  // If renameToDir is given, a new temp directory will be created in it.
   193  // Else, a new temp directory is placed within the path's parent dir.
   194  // After this, a file/dir represented by the path is moved into the temp dir.
   195  //
   196  // In case of any failures during the temp dir creation or the move,
   197  // default to RemoveAll of path in place.
   198  //
   199  // Returned renamedToPath is the renamed path if renaming succeeded and ""
   200  // otherwise.
   201  // Returned error is the one from RemoveAll call.
   202  func RenamingRemoveAll(path, renameToDir string) (renamedToPath string, err error) {
   203  	pathParentDir, pathFileOrDir := filepath.Split(filepath.Clean(path))
   204  	if renameToDir == "" {
   205  		renameToDir = pathParentDir
   206  	}
   207  	renameToDir, err = ioutil.TempDir(renameToDir, ".trash-")
   208  	if err != nil {
   209  		err = RemoveAll(path)
   210  		return
   211  	}
   212  
   213  	renamedToPath = filepath.Join(renameToDir, pathFileOrDir)
   214  	if err = os.Rename(path, renamedToPath); err != nil {
   215  		// delete temp dir we just created and ignore errors -- there is not much we can do.
   216  		_ = os.Remove(renameToDir)
   217  		renamedToPath = ""
   218  		err = RemoveAll(path)
   219  		return
   220  	}
   221  	err = RemoveAll(renamedToPath)
   222  	return
   223  }
   224  
   225  // MakeReadOnly recursively iterates through all of the files and directories
   226  // starting at path and marks them read-only.
   227  func MakeReadOnly(path string, filter func(string) bool) error {
   228  	return recursiveChmod(path, filter, func(mode os.FileMode) os.FileMode {
   229  		return mode & (^os.FileMode(0222))
   230  	})
   231  }
   232  
   233  // MakePathUserWritable updates the filesystem metadata on a single file or
   234  // directory to make it user-writable.
   235  //
   236  // fi is optional. If nil, os.Stat will be called on path. Otherwise, fi will
   237  // be regarded as the results of calling os.Stat on path. This is provided as
   238  // an optimization, since some filesystem operations automatically yield a
   239  // FileInfo.
   240  func MakePathUserWritable(path string, fi os.FileInfo) error {
   241  	if fi == nil {
   242  		var err error
   243  		if fi, err = os.Stat(path); err != nil {
   244  			return errors.Annotate(err, "failed to Stat path").InternalReason("path(%q)", path).Err()
   245  		}
   246  	}
   247  
   248  	// Make user-writable, if it's not already.
   249  	mode := fi.Mode()
   250  	if (mode & 0200) == 0 {
   251  		mode |= 0200
   252  		if err := os.Chmod(path, mode); err != nil {
   253  			return errors.Annotate(err, "could not Chmod path").InternalReason("mode(%#o)/path(%q)", mode, path).Err()
   254  		}
   255  	}
   256  	return nil
   257  }
   258  
   259  func recursiveChmod(path string, filter func(string) bool, chmod func(mode os.FileMode) os.FileMode) error {
   260  	if filter == nil {
   261  		filter = func(string) bool { return true }
   262  	}
   263  
   264  	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
   265  		if err != nil {
   266  			return errors.Annotate(err, "").Err()
   267  		}
   268  
   269  		mode := info.Mode()
   270  		if (mode.IsRegular() || mode.IsDir()) && filter(path) {
   271  			if newMode := chmod(mode); newMode != mode {
   272  				if err := os.Chmod(path, newMode); err != nil {
   273  					return errors.Annotate(err, "failed to Chmod").InternalReason("path(%q)", path).Err()
   274  				}
   275  			}
   276  		}
   277  		return nil
   278  	})
   279  	if err != nil {
   280  		return errors.Annotate(err, "").Err()
   281  	}
   282  	return nil
   283  }
   284  
   285  // Copy makes a copy of the file.
   286  func Copy(outfile, infile string, mode os.FileMode) (err error) {
   287  	in, err := os.Open(infile)
   288  	if err != nil {
   289  		return err
   290  	}
   291  	defer func() {
   292  		if cerr := in.Close(); err == nil {
   293  			err = cerr
   294  		}
   295  	}()
   296  
   297  	out, err := os.OpenFile(outfile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	defer func() {
   302  		if cerr := out.Close(); err == nil {
   303  			err = cerr
   304  		}
   305  	}()
   306  
   307  	_, err = io.Copy(out, in)
   308  	return err
   309  }
   310  
   311  // ReadableCopy makes a copy of the file that is readable by everyone.
   312  func ReadableCopy(outfile, infile string) error {
   313  	istat, err := os.Stat(infile)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	return Copy(outfile, infile, addReadMode(istat.Mode()))
   319  }
   320  
   321  func hardlinkWithFallback(outfile, infile string) error {
   322  	if err := os.Link(infile, outfile); err == nil {
   323  		return nil
   324  	}
   325  
   326  	return ReadableCopy(outfile, infile)
   327  }
   328  
   329  // HardlinkRecursively efficiently copies a file or directory from src to dst.
   330  //
   331  // `src` may be a file, directory, or a symlink to a file or directory.
   332  // All symlinks are replaced with their targets, so the resulting
   333  // directory structure in `dst` will never have any symlinks.
   334  //
   335  // To increase speed, HardlinkRecursively hardlinks individual files into the
   336  // (newly created) directory structure if possible.
   337  func HardlinkRecursively(src, dst string) error {
   338  	src, stat, err := ResolveSymlink(src)
   339  	if err != nil {
   340  		return errors.Annotate(err, "failed to call ResolveSymlink(%s)", src).Err()
   341  	}
   342  
   343  	if stat.Mode().IsRegular() {
   344  		return hardlinkWithFallback(dst, src)
   345  	}
   346  
   347  	if !stat.Mode().IsDir() {
   348  		return errors.Reason("%s is not a directory: %v", src, stat).Err()
   349  	}
   350  
   351  	if err := os.MkdirAll(dst, 0775); err != nil {
   352  		return errors.Annotate(err, "failed to call MkdirAll for %s", dst).Err()
   353  	}
   354  
   355  	file, err := os.Open(src)
   356  	if err != nil {
   357  		return errors.Annotate(err, "failed to Open %s", src).Err()
   358  	}
   359  	defer file.Close()
   360  
   361  	for {
   362  		names, err := file.Readdirnames(100)
   363  		if err == io.EOF {
   364  			break
   365  		}
   366  		if err != nil {
   367  			return errors.Annotate(err, "failed to call Readdirnames for %s", src).Err()
   368  		}
   369  
   370  		for _, name := range names {
   371  			if err := HardlinkRecursively(filepath.Join(src, name), filepath.Join(dst, name)); err != nil {
   372  				return errors.Annotate(err, "failed to call HardlinkRecursively(%s, %s)", filepath.Join(src, name), filepath.Join(dst, name)).Err()
   373  			}
   374  
   375  		}
   376  	}
   377  
   378  	return nil
   379  }
   380  
   381  // CreateDirectories creates the directory structure needed by the given list of files.
   382  func CreateDirectories(baseDirectory string, files []string) error {
   383  	dirs := make([]string, len(files))
   384  	for i, file := range files {
   385  		if filepath.IsAbs(file) {
   386  			return errors.Reason("file should be relative path: %s", file).Err()
   387  		}
   388  		dirs[i] = filepath.Dir(file)
   389  	}
   390  
   391  	sort.Strings(dirs)
   392  
   393  	for i, dir := range dirs {
   394  		if dir == "" {
   395  			continue
   396  		}
   397  		if i+1 < len(dirs) && filepath.HasPrefix(dirs[i+1], dir) {
   398  			continue
   399  		}
   400  		dir = filepath.Join(baseDirectory, dir)
   401  
   402  		if err := os.MkdirAll(dir, 0755); err != nil {
   403  			return errors.Annotate(err, "failed to create directory for %s", dir).Err()
   404  		}
   405  	}
   406  
   407  	return nil
   408  }
   409  
   410  // IsEmptyDir returns whether |dir| is empty or not.
   411  // This returns error if |dir| is not directory, or find some error during checking.
   412  func IsEmptyDir(dir string) (bool, error) {
   413  	d, err := os.Open(dir)
   414  	if err != nil {
   415  		return false, errors.Annotate(err, "failed to Open(%s)", dir).Err()
   416  	}
   417  	defer d.Close()
   418  
   419  	names, err := d.Readdirnames(1)
   420  	if len(names) > 0 || err == io.EOF {
   421  		return len(names) == 0, nil
   422  	}
   423  
   424  	return false, errors.Annotate(err, "failed to call Readdirnames(1) for %s", dir).Err()
   425  }
   426  
   427  // IsDir to see whether |path| is a directory.
   428  // This is just a thin wrapper around os.Stat(...).
   429  // If this returns True, |path| is a directory.
   430  // If this returns False with nil err, |path| is not a directory.
   431  // If this returns non-nil error, failed to determine |path| is a drectory.
   432  func IsDir(path string) (bool, error) {
   433  	stat, err := os.Stat(path)
   434  	if err != nil {
   435  		if os.IsNotExist(err) {
   436  			return false, nil
   437  		}
   438  		return false, err
   439  	}
   440  	return stat.IsDir(), nil
   441  }
   442  
   443  // GetFreeSpace returns the number of free bytes.
   444  //
   445  // On POSIX platforms, this returns the free space as visible by the current
   446  // user. The returned value is what is usable, and it can be lower than the
   447  // actual free disk space. For example on linux there's by default a 5% that is
   448  // reserved to the root user.
   449  func GetFreeSpace(path string) (uint64, error) {
   450  	return getFreeSpace(path)
   451  }
   452  
   453  // findPathSeparators finds the index of all PathSeparators in `path` which
   454  // don't split the Volume.
   455  //
   456  // This function is only defined for clean, absolute, paths which end with
   457  // a path separator.
   458  //
   459  // For unix paths, the first returned index will always be 0.
   460  func findPathSeparators(path string) []int {
   461  	offset := len(filepath.VolumeName(path))
   462  	path = path[offset:]
   463  
   464  	ret := make([]int, 0, 10) // 10 is a guess, could be more or less.
   465  	for {
   466  		idx := strings.IndexByte(path, os.PathSeparator)
   467  		if idx == -1 {
   468  			break
   469  		}
   470  		ret = append(ret, offset+idx)
   471  		offset += idx + 1
   472  		path = path[idx+1:]
   473  	}
   474  
   475  	return ret
   476  }
   477  
   478  // ErrRootSentinel is wrapped and then returned from GetCommonAncestor when it
   479  // encounters one of the provided rootSentinels.
   480  var ErrRootSentinel = errors.New("hit root sentinel")
   481  
   482  // GetCommonAncestor returns the smallest path which is the ancestor of all
   483  // provided paths (which must actually exist on the filesystem).
   484  //
   485  // All paths here are converted to absolute paths before calculating the
   486  // ancestor, and the returned path will also be an absolute path. Note that this
   487  // doesn't do anything special with symlinks; the caller can resolve them if
   488  // necessary.
   489  //
   490  // This function works correctly on case-insensitive filesystems, or on
   491  // filesystems with a mix of case-sensitive and case-insensitive paths, but you
   492  // can get some wild filesystems out there, so this will probably break on
   493  // exotic setups. Note that the case of the path you get back will be derived
   494  // from the shortest input path (after making them absolute); this function
   495  // makes no attempt to "canonicalize" the case of any paths (but may do so
   496  // accidentally, depending on the operating system). This function does not
   497  // attempt to resolve symlinks.
   498  //
   499  // If a given path points to a file, the file's containing directory will be
   500  // considered instead (i.e. GetCommonAncestor("a/b.ext") will return the
   501  // absolute path of "a").
   502  //
   503  // `rootSentinels` is a list of sub paths to look for to stop walking up the
   504  // directory hierarchy. A typical value would be something like
   505  // []string{".git"}. If one of these is found, this function returns "" with
   506  // a wrapped ErrRootSentinel. Use errors.Is to identify this.
   507  //
   508  // Returns an error if any of the provided paths does not exist.
   509  // If successful, will return a path ending with PathSeparator.
   510  //
   511  // If no paths are prodvided, returns ("", nil)
   512  func GetCommonAncestor(paths []string, rootSentinels []string) (string, error) {
   513  	if len(paths) == 0 {
   514  		return "", nil
   515  	}
   516  
   517  	const sep = string(os.PathSeparator)
   518  
   519  	type cleanPath struct {
   520  		// The cleaned, absolute path to a directory which exists.
   521  		path string
   522  		// Indexes in `path` for each os.PathSeparator which is a valid split point.
   523  		// Note that UNC roots on windows may 'skip' PathSeparators (i.e. slashes[0]
   524  		// may contain multiple PathSeparators).
   525  		slashes []int
   526  	}
   527  	cleanPaths := make([]cleanPath, len(paths))
   528  
   529  	var commonVolume *string
   530  
   531  	// Clean all the paths, make them absolute.
   532  	// Find all the slashes in the paths.
   533  	//
   534  	// Note that a UNC path like '\\host\share\something' would have its first
   535  	// slash at index 12.
   536  	// A Unix path like '/host/share/something' would have its first
   537  	// slash at index 0.
   538  	for i, path := range paths {
   539  		if err := AbsPath(&path); err != nil {
   540  			return "", err
   541  		}
   542  		vol := filepath.VolumeName(path)
   543  
   544  		if commonVolume != nil {
   545  			// note: we check this first to allow testing; otherwise we would need to
   546  			// run tests on a machine with multiple volumes.
   547  			if vol != *commonVolume {
   548  				return "", errors.Reason("provided paths originate on different volumes: path[0]:%q, path[%d]:%q", *commonVolume, i, vol).Err()
   549  			}
   550  		} else {
   551  			commonVolume = &vol
   552  		}
   553  
   554  		fi, err := os.Lstat(path)
   555  		if err != nil {
   556  			return "", errors.Annotate(err, "reading path[%d]: %q", i, path).Err()
   557  		}
   558  		if !fi.IsDir() {
   559  			path = filepath.Dir(path)
   560  			fi, err := os.Lstat(path)
   561  			if err != nil {
   562  				// given that we know that the original `path` exists, this SHOULD be
   563  				// impossible, but FUSE exists so... idk.
   564  				return "", errors.Annotate(err, "reading Dir(path[%d]): %q", i, path).Err()
   565  			}
   566  			if !fi.IsDir() {
   567  				// this SHOULD ALSO be impossible...
   568  				return "", errors.Annotate(err, "path %q could not be resolved to parent dir", path).Err()
   569  			}
   570  		}
   571  
   572  		if !strings.HasSuffix(path, sep) {
   573  			path = path + sep
   574  		}
   575  		cleanPaths[i] = cleanPath{
   576  			path:    path,
   577  			slashes: findPathSeparators(path),
   578  		}
   579  	}
   580  
   581  	// sort by length and then alphabetically
   582  	sort.Slice(cleanPaths, sortby.Chain{
   583  		func(i, j int) bool { return len(cleanPaths[i].path) < len(cleanPaths[j].path) },
   584  		func(i, j int) bool { return cleanPaths[i].path < cleanPaths[j].path },
   585  	}.Use)
   586  
   587  	candidate := cleanPaths[0]
   588  
   589  	// We want to see if all the slashes in all other candidates line up with the
   590  	// slashes in `candidate`.
   591  	//
   592  	// We are already making some lexical assumptions about the paths here; If we
   593  	// wanted to discard lexical assumptions we would need to do all permutations
   594  	// of SameFile checks for every directory combination in all paths, and pick
   595  	// the lowest one which matched (due to the possibility of e.g. bind mounts).
   596  	//
   597  	// However, ain't nobody got time for that.
   598  	slashesMatch := func(whichSlash int) bool {
   599  		for _, other := range cleanPaths[1:] {
   600  			// whichSlash+1 because we want to include everything up to, and
   601  			// including, whichSlash.
   602  			for i, slashIdx := range candidate.slashes[:whichSlash+1] {
   603  				if other.slashes[i] != slashIdx {
   604  					return false
   605  				}
   606  			}
   607  		}
   608  		return true
   609  	}
   610  
   611  	// for each slash in the candidate, see if all cleanPaths at this slash return
   612  	// true from os.SameFile vs candidate.
   613  	//
   614  	// Calling this function implies that all other paths have already been
   615  	// verified with slashesMatch.
   616  	//
   617  	// The first check would avoid comparing "/long/f" vs "/s/long"; Although they
   618  	// both have a slash at 7, the prefix leading to that doesn't match. See
   619  	// comment on slashesMatch.
   620  	trySlash := func(curPath string) (bool, error) {
   621  		var curFi os.FileInfo
   622  		for _, other := range cleanPaths[1:] {
   623  			otherPath := other.path[:len(curPath)]
   624  			if otherPath == curPath {
   625  				// exact match, try the next one
   626  				continue
   627  			}
   628  
   629  			// ok, try SameFile
   630  			if curFi == nil {
   631  				var err error
   632  				if curFi, err = os.Lstat(curPath); err != nil {
   633  					return false, err
   634  				}
   635  			}
   636  			otherFi, err := os.Lstat(otherPath)
   637  			if err != nil {
   638  				return false, err
   639  			}
   640  			if !os.SameFile(curFi, otherFi) {
   641  				return false, nil
   642  			}
   643  		}
   644  		return true, nil
   645  	}
   646  
   647  	for whichSlash := len(candidate.slashes) - 1; whichSlash >= 0; whichSlash-- {
   648  		// curPath includes trailing slash
   649  		curPath := candidate.path[:candidate.slashes[whichSlash]+1]
   650  
   651  		// check if it's out of bounds
   652  		for _, sentinel := range rootSentinels {
   653  			sentinelPath := filepath.Join(curPath, sentinel)
   654  			if _, err := os.Lstat(sentinelPath); err == nil {
   655  				return "", errors.Annotate(ErrRootSentinel, "%q", sentinelPath).Err()
   656  			} else if !os.IsNotExist(err) {
   657  				return "", errors.Annotate(err, "failed to read root sentinel %q", sentinelPath).Err()
   658  			}
   659  		}
   660  
   661  		// Check to see if all other candidates have the same slash structure; i.e.
   662  		// the first `whichSlash` number of slashes line up across all paths. This
   663  		// avoids doing stats to see if "/a/path/" and "/path/a/" are the same file.
   664  		if !slashesMatch(whichSlash) {
   665  			continue
   666  		}
   667  
   668  		// all slashes match, let's see if the targeted files are the same (either
   669  		// lexically equivalent or are os.SameFile)
   670  		ok, err := trySlash(curPath)
   671  		if err != nil {
   672  			return "", err
   673  		}
   674  		if ok {
   675  			return curPath, nil
   676  		}
   677  	}
   678  
   679  	// Should never get here:
   680  	//   * We are on unix and so whichSlash==0 is always the path "/". Either we
   681  	//     found the root sentinel above, or returned "/" successfully.
   682  	//   * We are on !unix and so whichSlash==0 is always a Volume (e.g. "C:\\").
   683  	//     However we already checked when resolving `paths` at the top that all
   684  	//     the cleanPaths share the SAME volume. Thus we should have either found
   685  	//     the root sentinel or returned this same volume (i.e. `commonVolume`).
   686  	panic("impossible")
   687  }