1// Copyright 2018 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
5package str
6
7import (
8	"os"
9	"path/filepath"
10	"runtime"
11	"strings"
12)
13
14// HasPathPrefix reports whether the slash-separated path s
15// begins with the elements in prefix.
16func HasPathPrefix(s, prefix string) bool {
17	if len(s) == len(prefix) {
18		return s == prefix
19	}
20	if prefix == "" {
21		return true
22	}
23	if len(s) > len(prefix) {
24		if prefix[len(prefix)-1] == '/' || s[len(prefix)] == '/' {
25			return s[:len(prefix)] == prefix
26		}
27	}
28	return false
29}
30
31// HasFilePathPrefix reports whether the filesystem path s
32// begins with the elements in prefix.
33//
34// HasFilePathPrefix is case-sensitive (except for volume names) even if the
35// filesystem is not, does not apply Unicode normalization even if the
36// filesystem does, and assumes that all path separators are canonicalized to
37// filepath.Separator (as returned by filepath.Clean).
38func HasFilePathPrefix(s, prefix string) bool {
39	sv := filepath.VolumeName(s)
40	pv := filepath.VolumeName(prefix)
41
42	// Strip the volume from both paths before canonicalizing sv and pv:
43	// it's unlikely that strings.ToUpper will change the length of the string,
44	// but doesn't seem impossible.
45	s = s[len(sv):]
46	prefix = prefix[len(pv):]
47
48	// Always treat Windows volume names as case-insensitive, even though
49	// we don't treat the rest of the path as such.
50	//
51	// TODO(bcmills): Why do we care about case only for the volume name? It's
52	// been this way since https://go.dev/cl/11316, but I don't understand why
53	// that problem doesn't apply to case differences in the entire path.
54	if sv != pv {
55		sv = strings.ToUpper(sv)
56		pv = strings.ToUpper(pv)
57	}
58
59	switch {
60	default:
61		return false
62	case sv != pv:
63		return false
64	case len(s) == len(prefix):
65		return s == prefix
66	case prefix == "":
67		return true
68	case len(s) > len(prefix):
69		if prefix[len(prefix)-1] == filepath.Separator {
70			return strings.HasPrefix(s, prefix)
71		}
72		return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
73	}
74}
75
76// TrimFilePathPrefix returns s without the leading path elements in prefix,
77// such that joining the string to prefix produces s.
78//
79// If s does not start with prefix (HasFilePathPrefix with the same arguments
80// returns false), TrimFilePathPrefix returns s. If s equals prefix,
81// TrimFilePathPrefix returns "".
82func TrimFilePathPrefix(s, prefix string) string {
83	if prefix == "" {
84		// Trimming the empty string from a path should join to produce that path.
85		// (Trim("/tmp/foo", "") should give "/tmp/foo", not "tmp/foo".)
86		return s
87	}
88	if !HasFilePathPrefix(s, prefix) {
89		return s
90	}
91
92	trimmed := s[len(prefix):]
93	if len(trimmed) > 0 && os.IsPathSeparator(trimmed[0]) {
94		if runtime.GOOS == "windows" && prefix == filepath.VolumeName(prefix) && len(prefix) == 2 && prefix[1] == ':' {
95			// Joining a relative path to a bare Windows drive letter produces a path
96			// relative to the working directory on that drive, but the original path
97			// was absolute, not relative. Keep the leading path separator so that it
98			// remains absolute when joined to prefix.
99		} else {
100			// Prefix ends in a regular path element, so strip the path separator that
101			// follows it.
102			trimmed = trimmed[1:]
103		}
104	}
105	return trimmed
106}
107
108// WithFilePathSeparator returns s with a trailing path separator, or the empty
109// string if s is empty.
110func WithFilePathSeparator(s string) string {
111	if s == "" || os.IsPathSeparator(s[len(s)-1]) {
112		return s
113	}
114	return s + string(filepath.Separator)
115}
116
117// QuoteGlob returns s with all Glob metacharacters quoted.
118// We don't try to handle backslash here, as that can appear in a
119// file path on Windows.
120func QuoteGlob(s string) string {
121	if !strings.ContainsAny(s, `*?[]`) {
122		return s
123	}
124	var sb strings.Builder
125	for _, c := range s {
126		switch c {
127		case '*', '?', '[', ']':
128			sb.WriteByte('\\')
129		}
130		sb.WriteRune(c)
131	}
132	return sb.String()
133}
134