Source file src/cmd/go/internal/search/search.go

     1  // Copyright 2017 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 search
     6  
     7  import (
     8  	"cmd/go/internal/base"
     9  	"cmd/go/internal/cfg"
    10  	"cmd/go/internal/fsys"
    11  	"fmt"
    12  	"go/build"
    13  	"io/fs"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"regexp"
    18  	"strings"
    19  )
    20  
    21  // A Match represents the result of matching a single package pattern.
    22  type Match struct {
    23  	pattern string   // the pattern itself
    24  	Dirs    []string // if the pattern is local, directories that potentially contain matching packages
    25  	Pkgs    []string // matching packages (import paths)
    26  	Errs    []error  // errors matching the patterns to packages, NOT errors loading those packages
    27  
    28  	// Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching
    29  	// packages could be located but results may be incomplete.
    30  	// If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not
    31  	// match any packages.
    32  }
    33  
    34  // NewMatch returns a Match describing the given pattern,
    35  // without resolving its packages or errors.
    36  func NewMatch(pattern string) *Match {
    37  	return &Match{pattern: pattern}
    38  }
    39  
    40  // Pattern returns the pattern to be matched.
    41  func (m *Match) Pattern() string { return m.pattern }
    42  
    43  // AddError appends a MatchError wrapping err to m.Errs.
    44  func (m *Match) AddError(err error) {
    45  	m.Errs = append(m.Errs, &MatchError{Match: m, Err: err})
    46  }
    47  
    48  // Literal reports whether the pattern is free of wildcards and meta-patterns.
    49  //
    50  // A literal pattern must match at most one package.
    51  func (m *Match) IsLiteral() bool {
    52  	return !strings.Contains(m.pattern, "...") && !m.IsMeta()
    53  }
    54  
    55  // Local reports whether the pattern must be resolved from a specific root or
    56  // directory, such as a filesystem path or a single module.
    57  func (m *Match) IsLocal() bool {
    58  	return build.IsLocalImport(m.pattern) || filepath.IsAbs(m.pattern)
    59  }
    60  
    61  // Meta reports whether the pattern is a “meta-package” keyword that represents
    62  // multiple packages, such as "std", "cmd", or "all".
    63  func (m *Match) IsMeta() bool {
    64  	return IsMetaPackage(m.pattern)
    65  }
    66  
    67  // IsMetaPackage checks if name is a reserved package name that expands to multiple packages.
    68  func IsMetaPackage(name string) bool {
    69  	return name == "std" || name == "cmd" || name == "all"
    70  }
    71  
    72  // A MatchError indicates an error that occurred while attempting to match a
    73  // pattern.
    74  type MatchError struct {
    75  	Match *Match
    76  	Err   error
    77  }
    78  
    79  func (e *MatchError) Error() string {
    80  	if e.Match.IsLiteral() {
    81  		return fmt.Sprintf("%s: %v", e.Match.Pattern(), e.Err)
    82  	}
    83  	return fmt.Sprintf("pattern %s: %v", e.Match.Pattern(), e.Err)
    84  }
    85  
    86  func (e *MatchError) Unwrap() error {
    87  	return e.Err
    88  }
    89  
    90  // MatchPackages sets m.Pkgs to a non-nil slice containing all the packages that
    91  // can be found under the $GOPATH directories and $GOROOT that match the
    92  // pattern. The pattern must be either "all" (all packages), "std" (standard
    93  // packages), "cmd" (standard commands), or a path including "...".
    94  //
    95  // If any errors may have caused the set of packages to be incomplete,
    96  // MatchPackages appends those errors to m.Errs.
    97  func (m *Match) MatchPackages() {
    98  	m.Pkgs = []string{}
    99  	if m.IsLocal() {
   100  		m.AddError(fmt.Errorf("internal error: MatchPackages: %s is not a valid package pattern", m.pattern))
   101  		return
   102  	}
   103  
   104  	if m.IsLiteral() {
   105  		m.Pkgs = []string{m.pattern}
   106  		return
   107  	}
   108  
   109  	match := func(string) bool { return true }
   110  	treeCanMatch := func(string) bool { return true }
   111  	if !m.IsMeta() {
   112  		match = MatchPattern(m.pattern)
   113  		treeCanMatch = TreeCanMatchPattern(m.pattern)
   114  	}
   115  
   116  	have := map[string]bool{
   117  		"builtin": true, // ignore pseudo-package that exists only for documentation
   118  	}
   119  	if !cfg.BuildContext.CgoEnabled {
   120  		have["runtime/cgo"] = true // ignore during walk
   121  	}
   122  
   123  	for _, src := range cfg.BuildContext.SrcDirs() {
   124  		if (m.pattern == "std" || m.pattern == "cmd") && src != cfg.GOROOTsrc {
   125  			continue
   126  		}
   127  		src = filepath.Clean(src) + string(filepath.Separator)
   128  		root := src
   129  		if m.pattern == "cmd" {
   130  			root += "cmd" + string(filepath.Separator)
   131  		}
   132  		err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error {
   133  			if err != nil {
   134  				return err // Likely a permission error, which could interfere with matching.
   135  			}
   136  			if path == src {
   137  				return nil // GOROOT/src and GOPATH/src cannot contain packages.
   138  			}
   139  
   140  			want := true
   141  			// Avoid .foo, _foo, and testdata directory trees.
   142  			_, elem := filepath.Split(path)
   143  			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
   144  				want = false
   145  			}
   146  
   147  			name := filepath.ToSlash(path[len(src):])
   148  			if m.pattern == "std" && (!IsStandardImportPath(name) || name == "cmd") {
   149  				// The name "std" is only the standard library.
   150  				// If the name is cmd, it's the root of the command tree.
   151  				want = false
   152  			}
   153  			if !treeCanMatch(name) {
   154  				want = false
   155  			}
   156  
   157  			if !fi.IsDir() {
   158  				if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.pattern, "...") {
   159  					if target, err := fsys.Stat(path); err == nil && target.IsDir() {
   160  						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
   161  					}
   162  				}
   163  				return nil
   164  			}
   165  			if !want {
   166  				return filepath.SkipDir
   167  			}
   168  
   169  			if have[name] {
   170  				return nil
   171  			}
   172  			have[name] = true
   173  			if !match(name) {
   174  				return nil
   175  			}
   176  			pkg, err := cfg.BuildContext.ImportDir(path, 0)
   177  			if err != nil {
   178  				if _, noGo := err.(*build.NoGoError); noGo {
   179  					// The package does not actually exist, so record neither the package
   180  					// nor the error.
   181  					return nil
   182  				}
   183  				// There was an error importing path, but not matching it,
   184  				// which is all that Match promises to do.
   185  				// Ignore the import error.
   186  			}
   187  
   188  			// If we are expanding "cmd", skip main
   189  			// packages under cmd/vendor. At least as of
   190  			// March, 2017, there is one there for the
   191  			// vendored pprof tool.
   192  			if m.pattern == "cmd" && pkg != nil && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" {
   193  				return nil
   194  			}
   195  
   196  			m.Pkgs = append(m.Pkgs, name)
   197  			return nil
   198  		})
   199  		if err != nil {
   200  			m.AddError(err)
   201  		}
   202  	}
   203  }
   204  
   205  // MatchDirs sets m.Dirs to a non-nil slice containing all directories that
   206  // potentially match a local pattern. The pattern must begin with an absolute
   207  // path, or "./", or "../". On Windows, the pattern may use slash or backslash
   208  // separators or a mix of both.
   209  //
   210  // If any errors may have caused the set of directories to be incomplete,
   211  // MatchDirs appends those errors to m.Errs.
   212  func (m *Match) MatchDirs(modRoots []string) {
   213  	m.Dirs = []string{}
   214  	if !m.IsLocal() {
   215  		m.AddError(fmt.Errorf("internal error: MatchDirs: %s is not a valid filesystem pattern", m.pattern))
   216  		return
   217  	}
   218  
   219  	if m.IsLiteral() {
   220  		m.Dirs = []string{m.pattern}
   221  		return
   222  	}
   223  
   224  	// Clean the path and create a matching predicate.
   225  	// filepath.Clean removes "./" prefixes (and ".\" on Windows). We need to
   226  	// preserve these, since they are meaningful in MatchPattern and in
   227  	// returned import paths.
   228  	cleanPattern := filepath.Clean(m.pattern)
   229  	isLocal := strings.HasPrefix(m.pattern, "./") || (os.PathSeparator == '\\' && strings.HasPrefix(m.pattern, `.\`))
   230  	prefix := ""
   231  	if cleanPattern != "." && isLocal {
   232  		prefix = "./"
   233  		cleanPattern = "." + string(os.PathSeparator) + cleanPattern
   234  	}
   235  	slashPattern := filepath.ToSlash(cleanPattern)
   236  	match := MatchPattern(slashPattern)
   237  
   238  	// Find directory to begin the scan.
   239  	// Could be smarter but this one optimization
   240  	// is enough for now, since ... is usually at the
   241  	// end of a path.
   242  	i := strings.Index(cleanPattern, "...")
   243  	dir, _ := filepath.Split(cleanPattern[:i])
   244  
   245  	// pattern begins with ./ or ../.
   246  	// path.Clean will discard the ./ but not the ../.
   247  	// We need to preserve the ./ for pattern matching
   248  	// and in the returned import paths.
   249  
   250  	if len(modRoots) > 1 {
   251  		abs, err := filepath.Abs(dir)
   252  		if err != nil {
   253  			m.AddError(err)
   254  			return
   255  		}
   256  		var found bool
   257  		for _, modRoot := range modRoots {
   258  			if modRoot != "" && hasFilepathPrefix(abs, modRoot) {
   259  				found = true
   260  			}
   261  		}
   262  		if !found {
   263  			plural := ""
   264  			if len(modRoots) > 1 {
   265  				plural = "s"
   266  			}
   267  			m.AddError(fmt.Errorf("directory %s is outside module root%s (%s)", abs, plural, strings.Join(modRoots, ", ")))
   268  		}
   269  	}
   270  
   271  	err := fsys.Walk(dir, func(path string, fi fs.FileInfo, err error) error {
   272  		if err != nil {
   273  			return err // Likely a permission error, which could interfere with matching.
   274  		}
   275  		if !fi.IsDir() {
   276  			return nil
   277  		}
   278  		top := false
   279  		if path == dir {
   280  			// Walk starts at dir and recurses. For the recursive case,
   281  			// the path is the result of filepath.Join, which calls filepath.Clean.
   282  			// The initial case is not Cleaned, though, so we do this explicitly.
   283  			//
   284  			// This converts a path like "./io/" to "io". Without this step, running
   285  			// "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io
   286  			// package, because prepending the prefix "./" to the unclean path would
   287  			// result in "././io", and match("././io") returns false.
   288  			top = true
   289  			path = filepath.Clean(path)
   290  		}
   291  
   292  		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
   293  		_, elem := filepath.Split(path)
   294  		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
   295  		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
   296  			return filepath.SkipDir
   297  		}
   298  
   299  		if !top && cfg.ModulesEnabled {
   300  			// Ignore other modules found in subdirectories.
   301  			if fi, err := fsys.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() {
   302  				return filepath.SkipDir
   303  			}
   304  		}
   305  
   306  		name := prefix + filepath.ToSlash(path)
   307  		if !match(name) {
   308  			return nil
   309  		}
   310  
   311  		// We keep the directory if we can import it, or if we can't import it
   312  		// due to invalid Go source files. This means that directories containing
   313  		// parse errors will be built (and fail) instead of being silently skipped
   314  		// as not matching the pattern. Go 1.5 and earlier skipped, but that
   315  		// behavior means people miss serious mistakes.
   316  		// See golang.org/issue/11407.
   317  		if p, err := cfg.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) {
   318  			if _, noGo := err.(*build.NoGoError); noGo {
   319  				// The package does not actually exist, so record neither the package
   320  				// nor the error.
   321  				return nil
   322  			}
   323  			// There was an error importing path, but not matching it,
   324  			// which is all that Match promises to do.
   325  			// Ignore the import error.
   326  		}
   327  		m.Dirs = append(m.Dirs, name)
   328  		return nil
   329  	})
   330  	if err != nil {
   331  		m.AddError(err)
   332  	}
   333  }
   334  
   335  // TreeCanMatchPattern(pattern)(name) reports whether
   336  // name or children of name can possibly match pattern.
   337  // Pattern is the same limited glob accepted by matchPattern.
   338  func TreeCanMatchPattern(pattern string) func(name string) bool {
   339  	wildCard := false
   340  	if i := strings.Index(pattern, "..."); i >= 0 {
   341  		wildCard = true
   342  		pattern = pattern[:i]
   343  	}
   344  	return func(name string) bool {
   345  		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
   346  			wildCard && strings.HasPrefix(name, pattern)
   347  	}
   348  }
   349  
   350  // MatchPattern(pattern)(name) reports whether
   351  // name matches pattern. Pattern is a limited glob
   352  // pattern in which '...' means 'any string' and there
   353  // is no other special syntax.
   354  // Unfortunately, there are two special cases. Quoting "go help packages":
   355  //
   356  // First, /... at the end of the pattern can match an empty string,
   357  // so that net/... matches both net and packages in its subdirectories, like net/http.
   358  // Second, any slash-separated pattern element containing a wildcard never
   359  // participates in a match of the "vendor" element in the path of a vendored
   360  // package, so that ./... does not match packages in subdirectories of
   361  // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
   362  // Note, however, that a directory named vendor that itself contains code
   363  // is not a vendored package: cmd/vendor would be a command named vendor,
   364  // and the pattern cmd/... matches it.
   365  func MatchPattern(pattern string) func(name string) bool {
   366  	// Convert pattern to regular expression.
   367  	// The strategy for the trailing /... is to nest it in an explicit ? expression.
   368  	// The strategy for the vendor exclusion is to change the unmatchable
   369  	// vendor strings to a disallowed code point (vendorChar) and to use
   370  	// "(anything but that codepoint)*" as the implementation of the ... wildcard.
   371  	// This is a bit complicated but the obvious alternative,
   372  	// namely a hand-written search like in most shell glob matchers,
   373  	// is too easy to make accidentally exponential.
   374  	// Using package regexp guarantees linear-time matching.
   375  
   376  	const vendorChar = "\x00"
   377  
   378  	if strings.Contains(pattern, vendorChar) {
   379  		return func(name string) bool { return false }
   380  	}
   381  
   382  	re := regexp.QuoteMeta(pattern)
   383  	re = replaceVendor(re, vendorChar)
   384  	switch {
   385  	case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
   386  		re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
   387  	case re == vendorChar+`/\.\.\.`:
   388  		re = `(/vendor|/` + vendorChar + `/\.\.\.)`
   389  	case strings.HasSuffix(re, `/\.\.\.`):
   390  		re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
   391  	}
   392  	re = strings.ReplaceAll(re, `\.\.\.`, `[^`+vendorChar+`]*`)
   393  
   394  	reg := regexp.MustCompile(`^` + re + `$`)
   395  
   396  	return func(name string) bool {
   397  		if strings.Contains(name, vendorChar) {
   398  			return false
   399  		}
   400  		return reg.MatchString(replaceVendor(name, vendorChar))
   401  	}
   402  }
   403  
   404  // replaceVendor returns the result of replacing
   405  // non-trailing vendor path elements in x with repl.
   406  func replaceVendor(x, repl string) string {
   407  	if !strings.Contains(x, "vendor") {
   408  		return x
   409  	}
   410  	elem := strings.Split(x, "/")
   411  	for i := 0; i < len(elem)-1; i++ {
   412  		if elem[i] == "vendor" {
   413  			elem[i] = repl
   414  		}
   415  	}
   416  	return strings.Join(elem, "/")
   417  }
   418  
   419  // WarnUnmatched warns about patterns that didn't match any packages.
   420  func WarnUnmatched(matches []*Match) {
   421  	for _, m := range matches {
   422  		if len(m.Pkgs) == 0 && len(m.Errs) == 0 {
   423  			fmt.Fprintf(os.Stderr, "go: warning: %q matched no packages\n", m.pattern)
   424  		}
   425  	}
   426  }
   427  
   428  // ImportPaths returns the matching paths to use for the given command line.
   429  // It calls ImportPathsQuiet and then WarnUnmatched.
   430  func ImportPaths(patterns, modRoots []string) []*Match {
   431  	matches := ImportPathsQuiet(patterns, modRoots)
   432  	WarnUnmatched(matches)
   433  	return matches
   434  }
   435  
   436  // ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
   437  func ImportPathsQuiet(patterns, modRoots []string) []*Match {
   438  	var out []*Match
   439  	for _, a := range CleanPatterns(patterns) {
   440  		m := NewMatch(a)
   441  		if m.IsLocal() {
   442  			m.MatchDirs(modRoots)
   443  
   444  			// Change the file import path to a regular import path if the package
   445  			// is in GOPATH or GOROOT. We don't report errors here; LoadImport
   446  			// (or something similar) will report them later.
   447  			m.Pkgs = make([]string, len(m.Dirs))
   448  			for i, dir := range m.Dirs {
   449  				absDir := dir
   450  				if !filepath.IsAbs(dir) {
   451  					absDir = filepath.Join(base.Cwd(), dir)
   452  				}
   453  				if bp, _ := cfg.BuildContext.ImportDir(absDir, build.FindOnly); bp.ImportPath != "" && bp.ImportPath != "." {
   454  					m.Pkgs[i] = bp.ImportPath
   455  				} else {
   456  					m.Pkgs[i] = dir
   457  				}
   458  			}
   459  		} else {
   460  			m.MatchPackages()
   461  		}
   462  
   463  		out = append(out, m)
   464  	}
   465  	return out
   466  }
   467  
   468  // CleanPatterns returns the patterns to use for the given command line. It
   469  // canonicalizes the patterns but does not evaluate any matches. For patterns
   470  // that are not local or absolute paths, it preserves text after '@' to avoid
   471  // modifying version queries.
   472  func CleanPatterns(patterns []string) []string {
   473  	if len(patterns) == 0 {
   474  		return []string{"."}
   475  	}
   476  	var out []string
   477  	for _, a := range patterns {
   478  		var p, v string
   479  		if build.IsLocalImport(a) || filepath.IsAbs(a) {
   480  			p = a
   481  		} else if i := strings.IndexByte(a, '@'); i < 0 {
   482  			p = a
   483  		} else {
   484  			p = a[:i]
   485  			v = a[i:]
   486  		}
   487  
   488  		// Arguments may be either file paths or import paths.
   489  		// As a courtesy to Windows developers, rewrite \ to /
   490  		// in arguments that look like import paths.
   491  		// Don't replace slashes in absolute paths.
   492  		if filepath.IsAbs(p) {
   493  			p = filepath.Clean(p)
   494  		} else {
   495  			if filepath.Separator == '\\' {
   496  				p = strings.ReplaceAll(p, `\`, `/`)
   497  			}
   498  
   499  			// Put argument in canonical form, but preserve leading ./.
   500  			if strings.HasPrefix(p, "./") {
   501  				p = "./" + path.Clean(p)
   502  				if p == "./." {
   503  					p = "."
   504  				}
   505  			} else {
   506  				p = path.Clean(p)
   507  			}
   508  		}
   509  
   510  		out = append(out, p+v)
   511  	}
   512  	return out
   513  }
   514  
   515  // hasPathPrefix reports whether the path s begins with the
   516  // elements in prefix.
   517  func hasPathPrefix(s, prefix string) bool {
   518  	switch {
   519  	default:
   520  		return false
   521  	case len(s) == len(prefix):
   522  		return s == prefix
   523  	case len(s) > len(prefix):
   524  		if prefix != "" && prefix[len(prefix)-1] == '/' {
   525  			return strings.HasPrefix(s, prefix)
   526  		}
   527  		return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
   528  	}
   529  }
   530  
   531  // hasFilepathPrefix reports whether the path s begins with the
   532  // elements in prefix.
   533  func hasFilepathPrefix(s, prefix string) bool {
   534  	switch {
   535  	default:
   536  		return false
   537  	case len(s) == len(prefix):
   538  		return s == prefix
   539  	case len(s) > len(prefix):
   540  		if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
   541  			return strings.HasPrefix(s, prefix)
   542  		}
   543  		return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
   544  	}
   545  }
   546  
   547  // IsStandardImportPath reports whether $GOROOT/src/path should be considered
   548  // part of the standard distribution. For historical reasons we allow people to add
   549  // their own code to $GOROOT instead of using $GOPATH, but we assume that
   550  // code will start with a domain name (dot in the first element).
   551  //
   552  // Note that this function is meant to evaluate whether a directory found in GOROOT
   553  // should be treated as part of the standard library. It should not be used to decide
   554  // that a directory found in GOPATH should be rejected: directories in GOPATH
   555  // need not have dots in the first element, and they just take their chances
   556  // with future collisions in the standard library.
   557  func IsStandardImportPath(path string) bool {
   558  	i := strings.Index(path, "/")
   559  	if i < 0 {
   560  		i = len(path)
   561  	}
   562  	elem := path[:i]
   563  	return !strings.Contains(elem, ".")
   564  }
   565  
   566  // IsRelativePath reports whether pattern should be interpreted as a directory
   567  // path relative to the current directory, as opposed to a pattern matching
   568  // import paths.
   569  func IsRelativePath(pattern string) bool {
   570  	return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
   571  }
   572  
   573  // InDir checks whether path is in the file tree rooted at dir.
   574  // If so, InDir returns an equivalent path relative to dir.
   575  // If not, InDir returns an empty string.
   576  // InDir makes some effort to succeed even in the presence of symbolic links.
   577  func InDir(path, dir string) string {
   578  	if rel := inDirLex(path, dir); rel != "" {
   579  		return rel
   580  	}
   581  	xpath, err := filepath.EvalSymlinks(path)
   582  	if err != nil || xpath == path {
   583  		xpath = ""
   584  	} else {
   585  		if rel := inDirLex(xpath, dir); rel != "" {
   586  			return rel
   587  		}
   588  	}
   589  
   590  	xdir, err := filepath.EvalSymlinks(dir)
   591  	if err == nil && xdir != dir {
   592  		if rel := inDirLex(path, xdir); rel != "" {
   593  			return rel
   594  		}
   595  		if xpath != "" {
   596  			if rel := inDirLex(xpath, xdir); rel != "" {
   597  				return rel
   598  			}
   599  		}
   600  	}
   601  	return ""
   602  }
   603  
   604  // inDirLex is like inDir but only checks the lexical form of the file names.
   605  // It does not consider symbolic links.
   606  // TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
   607  // return the suffix. Most uses of str.HasFilePathPrefix should probably
   608  // be calling InDir instead.
   609  func inDirLex(path, dir string) string {
   610  	pv := strings.ToUpper(filepath.VolumeName(path))
   611  	dv := strings.ToUpper(filepath.VolumeName(dir))
   612  	path = path[len(pv):]
   613  	dir = dir[len(dv):]
   614  	switch {
   615  	default:
   616  		return ""
   617  	case pv != dv:
   618  		return ""
   619  	case len(path) == len(dir):
   620  		if path == dir {
   621  			return "."
   622  		}
   623  		return ""
   624  	case dir == "":
   625  		return path
   626  	case len(path) > len(dir):
   627  		if dir[len(dir)-1] == filepath.Separator {
   628  			if path[:len(dir)] == dir {
   629  				return path[len(dir):]
   630  			}
   631  			return ""
   632  		}
   633  		if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
   634  			if len(path) == len(dir)+1 {
   635  				return "."
   636  			}
   637  			return path[len(dir)+1:]
   638  		}
   639  		return ""
   640  	}
   641  }
   642  

View as plain text