-
Notifications
You must be signed in to change notification settings - Fork 1
/
rule.go
244 lines (201 loc) · 7.03 KB
/
rule.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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package nogo
import (
"regexp"
"strings"
)
type Rule struct {
// Regexp defines all regexp-rules which have to pass in order
// to pass the rule.
Regexp []*regexp.Regexp
Prefix string
Pattern string
Negate bool
OnlyFolder bool
}
var (
DotGitRule = MustCompileAll("", []byte(".git"))[0]
)
func (r Rule) MatchPath(path string) Result {
var match bool
for _, reg := range r.Regexp {
match = reg.MatchString(path)
// All regexp have to match.
if !match {
return Result{
Found: match,
Rule: r,
}
}
}
return Result{
Found: match,
Rule: r,
}
}
// These bytes won't be in any valid file, so they should be perfectly valid as temporary replacement.
const (
doubleStar = "\000"
singleStar = "\001"
questionMark = "\002"
negatedMatchStart = "\003"
matchStart = "\004"
matchEnd = "\005"
escapedMatchStart = "\006"
escapedMatchEnd = "\007"
)
var (
// findRangeReg matches the replacements of [, [! and ].
// The ? in the regexp enables ungreedy mode.
findRangeReg = regexp.MustCompile(`[` + matchStart + negatedMatchStart + `].*?` + matchEnd)
)
// Compile the pattern into a single regexp.
// skip means that this pattern doesn't contain any rule (e.g. just a comment or empty line).
func Compile(prefix string, pattern string) (skip bool, rule Rule, err error) {
rule = Rule{
Prefix: prefix,
// The original pattern of the source file.
Pattern: pattern,
}
// ignoreFs empty lines.
if len(pattern) == 0 {
return true, Rule{}, nil
}
// ignoreFs lines starting with # as these are comments.
if pattern[0] == '#' {
return true, Rule{}, nil
}
// Unescape \# to #.
if strings.HasPrefix(pattern, "\\#") {
pattern = pattern[1:]
}
// ignoreFs spaces except when the last one is escaped: 'something \ '.
// TODO: actually I am not sure if this is correct but that's what I understand by
// "* Trailing spaces are ignored unless they are quoted with backslash ("\")."
// However I don't think that this is very often used.
if strings.HasSuffix(pattern, "\\ ") {
pattern = strings.TrimSuffix(pattern, "\\ ") + " "
} else {
pattern = strings.TrimRight(pattern, " ")
}
// '!' negates the pattern.
if pattern[0] == '!' {
rule.Negate = true
pattern = pattern[1:]
}
// If any '/' is at the beginning or middle, it is relative to the prefix.
// Else it may be anywhere bellow it and we have to apply a wildcard
if strings.Count(strings.TrimSuffix(pattern, "/"), "/") == 0 {
pattern = "**/" + strings.TrimPrefix(pattern, "/")
} else if prefix != "" {
// In most other cases we have to make sure the prefix ends with a '/'
prefix = strings.TrimSuffix(prefix, "/") + "/"
}
// Replace all special chars with placeholders, then quote the rest.
// After that the special regexp for that special cases can be replaced.
pattern = strings.ReplaceAll(pattern, "**", doubleStar)
pattern = strings.ReplaceAll(pattern, "*", singleStar)
pattern = strings.ReplaceAll(pattern, "?", questionMark)
// Re-Replace escaped replacements.
pattern = strings.ReplaceAll(pattern, `\`+doubleStar, "**")
pattern = strings.ReplaceAll(pattern, `\`+singleStar, "*")
pattern = strings.ReplaceAll(pattern, `\`+questionMark, "?")
pattern = regexp.QuoteMeta(pattern)
// Unescape and transform character matches.
// First replace all by the input escaped brackets to ignore them in the next replaces)
pattern = strings.ReplaceAll(pattern, `\\[`, escapedMatchStart)
pattern = strings.ReplaceAll(pattern, `\\]`, escapedMatchEnd)
// Then do the same with the negated one to ignore its bracket in the next replace.
pattern = strings.ReplaceAll(pattern, `\[!`, negatedMatchStart)
pattern = strings.ReplaceAll(pattern, `\[`, matchStart)
pattern = strings.ReplaceAll(pattern, `\]`, matchEnd)
// Now we can add any new regexp using [ and ] and still
// Do something with the placeholders later.
// If any '/' is at the end, it matches only folders.
// Note, as the input does not show us if it is a folder, the bool
// is set and it has to be checked separately.
if strings.HasSuffix(pattern, "/") {
pattern = strings.TrimSuffix(pattern, "/")
rule.OnlyFolder = true
}
// Check the placeholders:
// '?' matches any char but '/'.
pattern = strings.ReplaceAll(pattern, questionMark, "[^/]?")
// Replace the placeholders:
// A leading "**" followed by a slash means matches in all directories.
if strings.HasPrefix(pattern, doubleStar+"/") {
if prefix == "" {
pattern = "(.*/)?" + strings.TrimPrefix(pattern, doubleStar+"/")
} else {
pattern = "(/.*)?" + strings.TrimPrefix(pattern, doubleStar)
// Also remove a possible '/' from the prefix so that it concatenates correctly with the wildcard
prefix = strings.TrimSuffix(prefix, "/")
}
}
// A trailing "/**" matches everything inside.
if strings.HasSuffix(pattern, "/"+doubleStar) {
pattern = strings.TrimSuffix(pattern, doubleStar) + ".*"
}
// A slash followed by two consecutive asterisks then a slash matches zero or more directories.
pattern = strings.ReplaceAll(pattern, "/"+doubleStar+"/", ".*/")
// '*' matches anything but '/'.
pattern = strings.ReplaceAll(pattern, singleStar, "[^/]*")
// Now replace all still existing doubleStars and all stars by the single star rule.
// TODO: Not sure if that is the correct behavior.
pattern = strings.ReplaceAll(pattern, doubleStar, "[^/]*")
// Add an additional regexp which checks for non-slash on all range patterns.
// As the range should not match slashes, but as Go doesn't support look-ahead,
// I just add a new rule for this.
additionalPattern := findRangeReg.ReplaceAllString(pattern, `[^/]`)
finishPattern := func(p string) error {
// Now replace back the escaped brackets.
p = strings.ReplaceAll(p, escapedMatchStart, `[`)
p = strings.ReplaceAll(p, escapedMatchEnd, `]`)
p = strings.ReplaceAll(p, negatedMatchStart, "[^")
p = strings.ReplaceAll(p, matchStart, "[")
p = strings.ReplaceAll(p, matchEnd, "]")
reg, err := regexp.Compile("^" + regexp.QuoteMeta(prefix) + strings.TrimPrefix(p, "/") + "$")
if err != nil {
return err
}
rule.Regexp = append(rule.Regexp, reg)
return nil
}
// Skip that additional pattern if nothing was replaced.
if additionalPattern != pattern {
err := finishPattern(additionalPattern)
if err != nil {
return false, Rule{}, err
}
}
err = finishPattern(pattern)
if err != nil {
return false, Rule{}, err
}
return false, rule, nil
}
// CompileAll rules in the given data line by line.
// The prefix is added to all rules.
func CompileAll(prefix string, data []byte) ([]Rule, error) {
rules := make([]Rule, 0)
lines := strings.Split(string(data), "\n")
for _, line := range lines {
// Remove \r on windows.
line = strings.TrimSuffix(line, "\r")
skip, rule, err := Compile(prefix, line)
if err != nil {
return nil, err
}
if !skip {
rules = append(rules, rule)
}
}
return rules, nil
}
// MustCompileAll does the same as CompileAll but panics on error.
func MustCompileAll(prefix string, data []byte) []Rule {
rule, err := CompileAll(prefix, data)
if err != nil {
panic(err)
}
return rule
}