summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/str/path.go
blob: 83a3d0eb75388f57b5bfbdac17b64e0bcdb5747a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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()
}