forked from lokesh-balla/doublestar
-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.go
154 lines (140 loc) · 4.52 KB
/
utils.go
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package doublestar
import (
"errors"
"os"
"path"
"path/filepath"
"strings"
"time"
)
// SplitPattern is a utility function. Given a pattern, SplitPattern will
// return two strings: the first string is everything up to the last slash
// (`/`) that appears _before_ any unescaped "meta" characters (ie, `*?[{`).
// The second string is everything after that slash. For example, given the
// pattern:
//
// ../../path/to/meta*/**
// ^----------- split here
//
// SplitPattern returns "../../path/to" and "meta*/**". This is useful for
// initializing os.DirFS() to call Glob() because Glob() will silently fail if
// your pattern includes `/./` or `/../`. For example:
//
// base, pattern := SplitPattern("../../path/to/meta*/**")
// fsys := os.DirFS(base)
// matches, err := Glob(fsys, pattern)
//
// If SplitPattern cannot find somewhere to split the pattern (for example,
// `meta*/**`), it will return "." and the unaltered pattern (`meta*/**` in
// this example).
//
// Of course, it is your responsibility to decide if the returned base path is
// "safe" in the context of your application. Perhaps you could use Match() to
// validate against a list of approved base directories?
func SplitPattern(p string) (base, pattern string) {
base = "."
pattern = p
splitIdx := -1
for i := 0; i < len(p); i++ {
c := p[i]
if c == '\\' {
i++
} else if c == '/' {
splitIdx = i
} else if c == '*' || c == '?' || c == '[' || c == '{' {
break
}
}
if splitIdx == 0 {
return "/", p[1:]
} else if splitIdx > 0 {
return p[:splitIdx], p[splitIdx+1:]
}
return
}
// FilepathGlob returns the names of all files matching pattern or nil if there
// is no matching file. The syntax of pattern is the same as in Match(). The
// pattern may describe hierarchical names such as usr/*/bin/ed.
//
// FilepathGlob ignores file system errors such as I/O errors reading
// directories by default. The only possible returned error is ErrBadPattern,
// reporting that the pattern is malformed.
//
// To enable aborting on I/O errors, the WithFailOnIOErrors option can be
// passed.
//
// Note: FilepathGlob is a convenience function that is meant as a drop-in
// replacement for `path/filepath.Glob()` for users who don't need the
// complication of io/fs. Basically, it:
// - Runs `filepath.Clean()` and `ToSlash()` on the pattern
// - Runs `SplitPattern()` to get a base path and a pattern to Glob
// - Creates an FS object from the base path and `Glob()s` on the pattern
// - Joins the base path with all of the matches from `Glob()`
//
// Returned paths will use the system's path separator, just like
// `filepath.Glob()`.
//
// Note: the returned error doublestar.ErrBadPattern is not equal to
// filepath.ErrBadPattern.
func FilepathGlob(pattern string, opts ...GlobOption) (matches []string, err error) {
pattern = filepath.Clean(pattern)
pattern = filepath.ToSlash(pattern)
base, f := SplitPattern(pattern)
if f == "" || f == "." || f == ".." {
// some special cases to match filepath.Glob behavior
if !ValidatePathPattern(pattern) {
return nil, ErrBadPattern
}
if filepath.Separator != '\\' {
pattern = unescapeMeta(pattern)
}
if _, err = os.Lstat(pattern); err != nil {
g := newGlob(opts...)
if errors.Is(err, os.ErrNotExist) {
return nil, g.handlePatternNotExist(true)
}
return nil, g.forwardErrIfFailOnIOErrors(err)
}
return []string{filepath.FromSlash(pattern)}, nil
}
fs := os.DirFS(base)
if matches, err = Glob(fs, f, opts...); err != nil {
return nil, err
}
for i := range matches {
// use path.Join because we used ToSlash above to ensure our paths are made
// of forward slashes, no matter what the system uses
matches[i] = filepath.FromSlash(path.Join(base, matches[i]))
}
return
}
// Finds the next comma, but ignores any commas that appear inside nested `{}`.
// Assumes that each opening bracket has a corresponding closing bracket.
func indexNextAlt(s string, allowEscaping bool) int {
alts := 1
l := len(s)
for i := 0; i < l; i++ {
if allowEscaping && s[i] == '\\' {
// skip next byte
i++
} else if s[i] == '{' {
alts++
} else if s[i] == '}' {
alts--
} else if s[i] == ',' && alts == 1 {
return i
}
}
return -1
}
var metaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}")
// Unescapes meta characters (*?[]{})
func unescapeMeta(pattern string) string {
return metaReplacer.Replace(pattern)
}
func isOlder(modTime time.Time, maxAge time.Duration) bool {
if maxAge == 0 {
return false
}
return time.Since(modTime) > maxAge
}