summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/str
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
commitccd992355df7192993c666236047820244914598 (patch)
treef00fea65147227b7743083c6148396f74cd66935 /src/cmd/go/internal/str
parentInitial commit. (diff)
downloadgolang-1.21-ccd992355df7192993c666236047820244914598.tar.xz
golang-1.21-ccd992355df7192993c666236047820244914598.zip
Adding upstream version 1.21.8.upstream/1.21.8
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/go/internal/str')
-rw-r--r--src/cmd/go/internal/str/path.go133
-rw-r--r--src/cmd/go/internal/str/str.go113
-rw-r--r--src/cmd/go/internal/str/str_test.go185
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)
+ }
+ }
+ }
+}