"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/fsys"
+ "cmd/go/internal/str"
"cmd/internal/pkgpattern"
"fmt"
"go/build"
}
var found bool
for _, modRoot := range modRoots {
- if modRoot != "" && hasFilepathPrefix(abs, modRoot) {
+ if modRoot != "" && str.HasFilePathPrefix(abs, modRoot) {
found = true
}
}
return out
}
-// hasFilepathPrefix reports whether the path s begins with the
-// elements in prefix.
-func hasFilepathPrefix(s, prefix string) bool {
- switch {
- default:
- return false
- case len(s) == len(prefix):
- return s == prefix
- case len(s) > len(prefix):
- if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
- return strings.HasPrefix(s, prefix)
- }
- return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
- }
-}
-
// IsStandardImportPath reports whether $GOROOT/src/path should be considered
// part of the standard distribution. For historical reasons we allow people to add
// their own code to $GOROOT instead of using $GOPATH, but we assume that
// If not, InDir returns an empty string.
// InDir makes some effort to succeed even in the presence of symbolic links.
func InDir(path, dir string) string {
- if rel := inDirLex(path, dir); rel != "" {
+ // inDirLex reports whether path is lexically in dir,
+ // without considering symbolic or hard links.
+ inDirLex := func(path, dir string) (string, bool) {
+ if dir == "" {
+ return path, true
+ }
+ rel := str.TrimFilePathPrefix(path, dir)
+ if rel == path {
+ return "", false
+ }
+ if rel == "" {
+ return ".", true
+ }
+ return rel, true
+ }
+
+ if rel, ok := inDirLex(path, dir); ok {
return rel
}
xpath, err := filepath.EvalSymlinks(path)
if err != nil || xpath == path {
xpath = ""
} else {
- if rel := inDirLex(xpath, dir); rel != "" {
+ if rel, ok := inDirLex(xpath, dir); ok {
return rel
}
}
xdir, err := filepath.EvalSymlinks(dir)
if err == nil && xdir != dir {
- if rel := inDirLex(path, xdir); rel != "" {
+ if rel, ok := inDirLex(path, xdir); ok {
return rel
}
if xpath != "" {
- if rel := inDirLex(xpath, xdir); rel != "" {
+ if rel, ok := inDirLex(xpath, xdir); ok {
return rel
}
}
}
return ""
}
-
-// inDirLex is like inDir but only checks the lexical form of the file names.
-// It does not consider symbolic links.
-// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
-// return the suffix. Most uses of str.HasFilePathPrefix should probably
-// be calling InDir instead.
-func inDirLex(path, dir string) string {
- pv := strings.ToUpper(filepath.VolumeName(path))
- dv := strings.ToUpper(filepath.VolumeName(dir))
- path = path[len(pv):]
- dir = dir[len(dv):]
- switch {
- default:
- return ""
- case pv != dv:
- return ""
- case len(path) == len(dir):
- if path == dir {
- return "."
- }
- return ""
- case dir == "":
- return path
- case len(path) > len(dir):
- if dir[len(dir)-1] == filepath.Separator {
- if path[:len(dir)] == dir {
- return path[len(dir):]
- }
- return ""
- }
- if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
- if len(path) == len(dir)+1 {
- return "."
- }
- return path[len(dir)+1:]
- }
- return ""
- }
-}
package str
import (
+ "os"
"path/filepath"
+ "runtime"
"strings"
)
// HasFilePathPrefix reports whether the filesystem path s
// begins with the elements in prefix.
+//
+// HasFilePathPrefix is case-sensitive (except for volume names) even if the
+// filesystem is not, does not apply Unicode normalization even if the
+// filesystem does, and assumes that all path separators are canonicalized to
+// filepath.Separator (as returned by filepath.Clean).
func HasFilePathPrefix(s, prefix string) bool {
- sv := strings.ToUpper(filepath.VolumeName(s))
- pv := strings.ToUpper(filepath.VolumeName(prefix))
+ sv := filepath.VolumeName(s)
+ pv := filepath.VolumeName(prefix)
+
+ // Strip the volume from both paths before canonicalizing sv and pv:
+ // it's unlikely that strings.ToUpper will change the length of the string,
+ // but doesn't seem impossible.
s = s[len(sv):]
prefix = prefix[len(pv):]
+
+ // Always treat Windows volume names as case-insensitive, even though
+ // we don't treat the rest of the path as such.
+ //
+ // TODO(bcmills): Why do we care about case only for the volume name? It's
+ // been this way since https://go.dev/cl/11316, but I don't understand why
+ // that problem doesn't apply to case differences in the entire path.
+ if sv != pv {
+ sv = strings.ToUpper(sv)
+ pv = strings.ToUpper(pv)
+ }
+
switch {
default:
return false
}
}
-// TrimFilePathPrefix returns s without the leading path elements in prefix.
+// TrimFilePathPrefix returns s without the leading path elements in prefix,
+// such that joining the string to prefix produces s.
+//
// If s does not start with prefix (HasFilePathPrefix with the same arguments
// returns false), TrimFilePathPrefix returns s. If s equals prefix,
// TrimFilePathPrefix returns "".
func TrimFilePathPrefix(s, prefix string) string {
+ if prefix == "" {
+ // Trimming the empty string from a path should join to produce that path.
+ // (Trim("/tmp/foo", "") should give "/tmp/foo", not "tmp/foo".)
+ return s
+ }
if !HasFilePathPrefix(s, prefix) {
return s
}
+
trimmed := s[len(prefix):]
- if len(trimmed) == 0 || trimmed[0] != filepath.Separator {
- // Prefix either is equal to s, or ends with a separator
- // (for example, if it is exactly "/").
- return trimmed
+ if len(trimmed) > 0 && os.IsPathSeparator(trimmed[0]) {
+ if runtime.GOOS == "windows" && prefix == filepath.VolumeName(prefix) && len(prefix) == 2 && prefix[1] == ':' {
+ // Joining a relative path to a bare Windows drive letter produces a path
+ // relative to the working directory on that drive, but the original path
+ // was absolute, not relative. Keep the leading path separator so that it
+ // remains absolute when joined to prefix.
+ } else {
+ // Prefix ends in a regular path element, so strip the path separator that
+ // follows it.
+ trimmed = trimmed[1:]
+ }
}
- return trimmed[1:]
+ return trimmed
}
// QuoteGlob returns s with all Glob metacharacters quoted.
import (
"os"
+ "path/filepath"
"runtime"
+ "strings"
"testing"
)
}
}
-type trimFilePathPrefixTest struct {
- s, prefix, want string
+func TestHasPathPrefix(t *testing.T) {
+ type testCase struct {
+ s, prefix string
+ want bool
+ }
+ for _, tt := range []testCase{
+ {"", "", true},
+ {"", "/", false},
+ {"foo", "", true},
+ {"foo", "/", false},
+ {"foo", "foo", true},
+ {"foo", "foo/", false},
+ {"foo", "/foo", false},
+ {"foo/bar", "", true},
+ {"foo/bar", "foo", true},
+ {"foo/bar", "foo/", true},
+ {"foo/bar", "/foo", false},
+ {"foo/bar", "foo/bar", true},
+ {"foo/bar", "foo/bar/", false},
+ {"foo/bar", "/foo/bar", false},
+ } {
+ got := HasPathPrefix(tt.s, tt.prefix)
+ if got != tt.want {
+ t.Errorf("HasPathPrefix(%q, %q) = %v; want %v", tt.s, tt.prefix, got, tt.want)
+ }
+ }
}
func TestTrimFilePathPrefixSlash(t *testing.T) {
if os.PathSeparator != '/' {
t.Skipf("test requires slash-separated file paths")
}
- tests := []trimFilePathPrefixTest{
- {"/foo", "", "foo"},
+
+ type testCase struct {
+ s, prefix, want string
+ }
+ for _, tt := range []testCase{
+ {"/", "", "/"},
+ {"/", "/", ""},
+ {"/foo", "", "/foo"},
{"/foo", "/", "foo"},
{"/foo", "/foo", ""},
{"/foo/bar", "/foo", "bar"},
{"/foo/bar", "/foo/", "bar"},
+ {"/foo/", "/", "foo/"},
+ {"/foo/", "/foo", ""},
+ {"/foo/", "/foo/", ""},
+
// if prefix is not s's prefix, return s
+ {"", "/", ""},
{"/foo", "/bar", "/foo"},
{"/foo", "/foo/bar", "/foo"},
- }
-
- for _, tt := range tests {
- if got := TrimFilePathPrefix(tt.s, tt.prefix); got != tt.want {
+ {"foo", "/foo", "foo"},
+ {"/foo", "foo", "/foo"},
+ {"/foo", "/foo/", "/foo"},
+ } {
+ got := TrimFilePathPrefix(tt.s, tt.prefix)
+ if got == tt.want {
+ t.Logf("TrimFilePathPrefix(%q, %q) = %q", tt.s, tt.prefix, got)
+ } else {
t.Errorf("TrimFilePathPrefix(%q, %q) = %q, want %q", tt.s, tt.prefix, got, tt.want)
}
+
+ if HasFilePathPrefix(tt.s, tt.prefix) {
+ joined := filepath.Join(tt.prefix, got)
+ if clean := filepath.Clean(tt.s); joined != clean {
+ t.Errorf("filepath.Join(%q, %q) = %q, want %q", tt.prefix, got, joined, clean)
+ }
+ }
}
}
if runtime.GOOS != "windows" {
t.Skipf("test requires Windows file paths")
}
- tests := []trimFilePathPrefixTest{
- {`C:\foo`, `C:`, `foo`},
+ type testCase struct {
+ s, prefix, want string
+ }
+ for _, tt := range []testCase{
+ {`\`, ``, `\`},
+ {`\`, `\`, ``},
+ {`C:`, `C:`, ``},
+ {`C:\`, `C:`, `\`},
+ {`C:\`, `C:\`, ``},
+ {`C:\foo`, ``, `C:\foo`},
+ {`C:\foo`, `C:`, `\foo`},
{`C:\foo`, `C:\`, `foo`},
{`C:\foo`, `C:\foo`, ``},
+ {`C:\foo\`, `C:\foo`, ``},
{`C:\foo\bar`, `C:\foo`, `bar`},
{`C:\foo\bar`, `C:\foo\`, `bar`},
// if prefix is not s's prefix, return s
{`C:\foo`, `C:\bar`, `C:\foo`},
{`C:\foo`, `C:\foo\bar`, `C:\foo`},
+ {`C:`, `C:\`, `C:`},
// if volumes are different, return s
+ {`C:`, ``, `C:`},
+ {`C:\`, ``, `C:\`},
{`C:\foo`, ``, `C:\foo`},
{`C:\foo`, `\foo`, `C:\foo`},
{`C:\foo`, `D:\foo`, `C:\foo`},
{`\\host\share\foo`, `\\other\share\`, `\\host\share\foo`},
{`\\host\share\foo`, `\\host\`, `\\host\share\foo`},
{`\\host\share\foo`, `\share\`, `\\host\share\foo`},
- }
- for _, tt := range tests {
- if got := TrimFilePathPrefix(tt.s, tt.prefix); got != tt.want {
- t.Errorf("TrimFilePathPrefix(%q, %q) = %q, want %q", tt.s, tt.prefix, got, tt.want)
+ // only volume names are case-insensitive
+ {`C:\foo`, `c:`, `\foo`},
+ {`C:\foo`, `c:\foo`, ``},
+ {`c:\foo`, `C:`, `\foo`},
+ {`c:\foo`, `C:\foo`, ``},
+ {`C:\foo`, `C:\Foo`, `C:\foo`},
+ {`\\Host\Share\foo`, `\\host\share`, `foo`},
+ {`\\Host\Share\foo`, `\\host\share\foo`, ``},
+ {`\\host\share\foo`, `\\Host\Share`, `foo`},
+ {`\\host\share\foo`, `\\Host\Share\foo`, ``},
+ {`\\Host\Share\foo`, `\\Host\Share\Foo`, `\\Host\Share\foo`},
+ } {
+ got := TrimFilePathPrefix(tt.s, tt.prefix)
+ if got == tt.want {
+ t.Logf("TrimFilePathPrefix(%#q, %#q) = %#q", tt.s, tt.prefix, got)
+ } else {
+ t.Errorf("TrimFilePathPrefix(%#q, %#q) = %#q, want %#q", tt.s, tt.prefix, got, tt.want)
+ }
+
+ if HasFilePathPrefix(tt.s, tt.prefix) {
+ // Although TrimFilePathPrefix is only case-insensitive in the volume name,
+ // what we care about in testing Join is that absolute paths remain
+ // absolute and relative paths remaining relative — there is no harm in
+ // over-normalizing letters in the comparison, so we use EqualFold.
+ joined := filepath.Join(tt.prefix, got)
+ if clean := filepath.Clean(tt.s); !strings.EqualFold(joined, clean) {
+ t.Errorf("filepath.Join(%#q, %#q) = %#q, want %#q", tt.prefix, got, joined, clean)
+ }
}
}
}