1package doublestar
2
3import (
4 "errors"
5 "os"
6 "path"
7 "path/filepath"
8 "strings"
9)
10
11// SplitPattern is a utility function. Given a pattern, SplitPattern will
12// return two strings: the first string is everything up to the last slash
13// (`/`) that appears _before_ any unescaped "meta" characters (ie, `*?[{`).
14// The second string is everything after that slash. For example, given the
15// pattern:
16//
17// ../../path/to/meta*/**
18// ^----------- split here
19//
20// SplitPattern returns "../../path/to" and "meta*/**". This is useful for
21// initializing os.DirFS() to call Glob() because Glob() will silently fail if
22// your pattern includes `/./` or `/../`. For example:
23//
24// base, pattern := SplitPattern("../../path/to/meta*/**")
25// fsys := os.DirFS(base)
26// matches, err := Glob(fsys, pattern)
27//
28// If SplitPattern cannot find somewhere to split the pattern (for example,
29// `meta*/**`), it will return "." and the unaltered pattern (`meta*/**` in
30// this example).
31//
32// Note that SplitPattern will also unescape any meta characters in the
33// returned base string, so that it can be passed straight to os.DirFS().
34//
35// Of course, it is your responsibility to decide if the returned base path is
36// "safe" in the context of your application. Perhaps you could use Match() to
37// validate against a list of approved base directories?
38//
39func SplitPattern(p string) (base, pattern string) {
40 base = "."
41 pattern = p
42
43 splitIdx := -1
44 for i := 0; i < len(p); i++ {
45 c := p[i]
46 if c == '\\' {
47 i++
48 } else if c == '/' {
49 splitIdx = i
50 } else if c == '*' || c == '?' || c == '[' || c == '{' {
51 break
52 }
53 }
54
55 if splitIdx == 0 {
56 return "/", p[1:]
57 } else if splitIdx > 0 {
58 return unescapeMeta(p[:splitIdx]), p[splitIdx+1:]
59 }
60
61 return
62}
63
64// FilepathGlob returns the names of all files matching pattern or nil if there
65// is no matching file. The syntax of pattern is the same as in Match(). The
66// pattern may describe hierarchical names such as usr/*/bin/ed.
67//
68// FilepathGlob ignores file system errors such as I/O errors reading
69// directories by default. The only possible returned error is ErrBadPattern,
70// reporting that the pattern is malformed.
71//
72// To enable aborting on I/O errors, the WithFailOnIOErrors option can be
73// passed.
74//
75// Note: FilepathGlob is a convenience function that is meant as a drop-in
76// replacement for `path/filepath.Glob()` for users who don't need the
77// complication of io/fs. Basically, it:
78// - Runs `filepath.Clean()` and `ToSlash()` on the pattern
79// - Runs `SplitPattern()` to get a base path and a pattern to Glob
80// - Creates an FS object from the base path and `Glob()s` on the pattern
81// - Joins the base path with all of the matches from `Glob()`
82//
83// Returned paths will use the system's path separator, just like
84// `filepath.Glob()`.
85//
86// Note: the returned error doublestar.ErrBadPattern is not equal to
87// filepath.ErrBadPattern.
88//
89func FilepathGlob(pattern string, opts ...GlobOption) (matches []string, err error) {
90 if pattern == "" {
91 // special case to match filepath.Glob behavior
92 g := newGlob(opts...)
93 if g.failOnIOErrors {
94 // match doublestar.Glob behavior here
95 return nil, os.ErrInvalid
96 }
97 return nil, nil
98 }
99
100 pattern = filepath.Clean(pattern)
101 pattern = filepath.ToSlash(pattern)
102 base, f := SplitPattern(pattern)
103 if f == "" || f == "." || f == ".." {
104 // some special cases to match filepath.Glob behavior
105 if !ValidatePathPattern(pattern) {
106 return nil, ErrBadPattern
107 }
108
109 if filepath.Separator != '\\' {
110 pattern = unescapeMeta(pattern)
111 }
112
113 if _, err = os.Lstat(pattern); err != nil {
114 g := newGlob(opts...)
115 if errors.Is(err, os.ErrNotExist) {
116 return nil, g.handlePatternNotExist(true)
117 }
118 return nil, g.forwardErrIfFailOnIOErrors(err)
119 }
120 return []string{filepath.FromSlash(pattern)}, nil
121 }
122
123 fs := os.DirFS(base)
124 if matches, err = Glob(fs, f, opts...); err != nil {
125 return nil, err
126 }
127 for i := range matches {
128 // use path.Join because we used ToSlash above to ensure our paths are made
129 // of forward slashes, no matter what the system uses
130 matches[i] = filepath.FromSlash(path.Join(base, matches[i]))
131 }
132 return
133}
134
135// Finds the next comma, but ignores any commas that appear inside nested `{}`.
136// Assumes that each opening bracket has a corresponding closing bracket.
137func indexNextAlt(s string, allowEscaping bool) int {
138 alts := 1
139 l := len(s)
140 for i := 0; i < l; i++ {
141 if allowEscaping && s[i] == '\\' {
142 // skip next byte
143 i++
144 } else if s[i] == '{' {
145 alts++
146 } else if s[i] == '}' {
147 alts--
148 } else if s[i] == ',' && alts == 1 {
149 return i
150 }
151 }
152 return -1
153}
154
155var metaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}")
156
157// Unescapes meta characters (*?[]{})
158func unescapeMeta(pattern string) string {
159 return metaReplacer.Replace(pattern)
160}