blob: 83a3d0eb75388f57b5bfbdac17b64e0bcdb5747a [file] [log] [blame]
// 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()
}