diff options
Diffstat (limited to 'src/cmd/go/internal/str')
-rw-r--r-- | src/cmd/go/internal/str/path.go | 133 | ||||
-rw-r--r-- | src/cmd/go/internal/str/str.go | 113 | ||||
-rw-r--r-- | src/cmd/go/internal/str/str_test.go | 185 |
3 files changed, 431 insertions, 0 deletions
diff --git a/src/cmd/go/internal/str/path.go b/src/cmd/go/internal/str/path.go new file mode 100644 index 0000000..83a3d0e --- /dev/null +++ b/src/cmd/go/internal/str/path.go @@ -0,0 +1,133 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package str + +import ( + "os" + "path/filepath" + "runtime" + "strings" +) + +// HasPathPrefix reports whether the slash-separated path s +// begins with the elements in prefix. +func HasPathPrefix(s, prefix string) bool { + if len(s) == len(prefix) { + return s == prefix + } + if prefix == "" { + return true + } + if len(s) > len(prefix) { + if prefix[len(prefix)-1] == '/' || s[len(prefix)] == '/' { + return s[:len(prefix)] == prefix + } + } + return false +} + +// 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 := 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 + case sv != pv: + return false + case len(s) == len(prefix): + return s == prefix + case prefix == "": + return true + case len(s) > len(prefix): + if prefix[len(prefix)-1] == filepath.Separator { + return strings.HasPrefix(s, prefix) + } + return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == 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 && 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 +} + +// WithFilePathSeparator returns s with a trailing path separator, or the empty +// string if s is empty. +func WithFilePathSeparator(s string) string { + if s == "" || os.IsPathSeparator(s[len(s)-1]) { + return s + } + return s + string(filepath.Separator) +} + +// QuoteGlob returns s with all Glob metacharacters quoted. +// We don't try to handle backslash here, as that can appear in a +// file path on Windows. +func QuoteGlob(s string) string { + if !strings.ContainsAny(s, `*?[]`) { + return s + } + var sb strings.Builder + for _, c := range s { + switch c { + case '*', '?', '[', ']': + sb.WriteByte('\\') + } + sb.WriteRune(c) + } + return sb.String() +} diff --git a/src/cmd/go/internal/str/str.go b/src/cmd/go/internal/str/str.go new file mode 100644 index 0000000..af7c699 --- /dev/null +++ b/src/cmd/go/internal/str/str.go @@ -0,0 +1,113 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package str provides string manipulation utilities. +package str + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// StringList flattens its arguments into a single []string. +// Each argument in args must have type string or []string. +func StringList(args ...any) []string { + var x []string + for _, arg := range args { + switch arg := arg.(type) { + case []string: + x = append(x, arg...) + case string: + x = append(x, arg) + default: + panic("stringList: invalid argument of type " + fmt.Sprintf("%T", arg)) + } + } + return x +} + +// ToFold returns a string with the property that +// +// strings.EqualFold(s, t) iff ToFold(s) == ToFold(t) +// +// This lets us test a large set of strings for fold-equivalent +// duplicates without making a quadratic number of calls +// to EqualFold. Note that strings.ToUpper and strings.ToLower +// do not have the desired property in some corner cases. +func ToFold(s string) string { + // Fast path: all ASCII, no upper case. + // Most paths look like this already. + for i := 0; i < len(s); i++ { + c := s[i] + if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' { + goto Slow + } + } + return s + +Slow: + var b strings.Builder + for _, r := range s { + // SimpleFold(x) cycles to the next equivalent rune > x + // or wraps around to smaller values. Iterate until it wraps, + // and we've found the minimum value. + for { + r0 := r + r = unicode.SimpleFold(r0) + if r <= r0 { + break + } + } + // Exception to allow fast path above: A-Z => a-z + if 'A' <= r && r <= 'Z' { + r += 'a' - 'A' + } + b.WriteRune(r) + } + return b.String() +} + +// FoldDup reports a pair of strings from the list that are +// equal according to strings.EqualFold. +// It returns "", "" if there are no such strings. +func FoldDup(list []string) (string, string) { + clash := map[string]string{} + for _, s := range list { + fold := ToFold(s) + if t := clash[fold]; t != "" { + if s > t { + s, t = t, s + } + return s, t + } + clash[fold] = s + } + return "", "" +} + +// Contains reports whether x contains s. +func Contains(x []string, s string) bool { + for _, t := range x { + if t == s { + return true + } + } + return false +} + +// Uniq removes consecutive duplicate strings from ss. +func Uniq(ss *[]string) { + if len(*ss) <= 1 { + return + } + uniq := (*ss)[:1] + for _, s := range *ss { + if s != uniq[len(uniq)-1] { + uniq = append(uniq, s) + } + } + *ss = uniq +} diff --git a/src/cmd/go/internal/str/str_test.go b/src/cmd/go/internal/str/str_test.go new file mode 100644 index 0000000..7c19877 --- /dev/null +++ b/src/cmd/go/internal/str/str_test.go @@ -0,0 +1,185 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package str + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +var foldDupTests = []struct { + list []string + f1, f2 string +}{ + {StringList("math/rand", "math/big"), "", ""}, + {StringList("math", "strings"), "", ""}, + {StringList("strings"), "", ""}, + {StringList("strings", "strings"), "strings", "strings"}, + {StringList("Rand", "rand", "math", "math/rand", "math/Rand"), "Rand", "rand"}, +} + +func TestFoldDup(t *testing.T) { + for _, tt := range foldDupTests { + f1, f2 := FoldDup(tt.list) + if f1 != tt.f1 || f2 != tt.f2 { + t.Errorf("foldDup(%q) = %q, %q, want %q, %q", tt.list, f1, f2, tt.f1, tt.f2) + } + } +} + +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") + } + + 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"}, + {"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) + } + } + } +} + +func TestTrimFilePathPrefixWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skipf("test requires Windows file paths") + } + 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`}, + + //UNC path + {`\\host\share\foo`, `\\host\share`, `foo`}, + {`\\host\share\foo`, `\\host\share\`, `foo`}, + {`\\host\share\foo`, `\\host\share\foo`, ``}, + {`\\host\share\foo\bar`, `\\host\share\foo`, `bar`}, + {`\\host\share\foo\bar`, `\\host\share\foo\`, `bar`}, + // if prefix is not s's prefix, return s + {`\\host\share\foo`, `\\host\share\bar`, `\\host\share\foo`}, + {`\\host\share\foo`, `\\host\share\foo\bar`, `\\host\share\foo`}, + // if either host or share name is different, return s + {`\\host\share\foo`, ``, `\\host\share\foo`}, + {`\\host\share\foo`, `\foo`, `\\host\share\foo`}, + {`\\host\share\foo`, `\\host\other\`, `\\host\share\foo`}, + {`\\host\share\foo`, `\\other\share\`, `\\host\share\foo`}, + {`\\host\share\foo`, `\\host\`, `\\host\share\foo`}, + {`\\host\share\foo`, `\share\`, `\\host\share\foo`}, + + // 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) + } + } + } +} |