cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.51.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/comments/comments.go

325lines · modecode

1package comments
2
3import (
4 "bufio"
5 "fmt"
6 "strings"
7 "time"
8 "unicode"
9)
10
11type Type uint8
12
13const (
14 UnknownType Type = iota
15 InvalidComment
16 IgnoreFileType // ignore/file
17 IgnoreLineType // ignore/line
18 IgnoreBeginType // ignore/begin
19 IgnoreEndType // ignore/end
20 IgnoreNextLineType // ignore/next-line
21 FileOwnerType // file/owner
22 RuleOwnerType // rule/owner
23 FileDisableType // file/disable
24 DisableType // disable
25 FileSnoozeType // file/snooze
26 SnoozeType // snooze
27 RuleSetType // rule/set
28)
29
30var (
31 Prefix = "pint"
32
33 IgnoreFileComment = "ignore/file"
34 IgnoreLineComment = "ignore/line"
35 IgnoreBeginComment = "ignore/begin"
36 IgnoreEndComment = "ignore/end"
37 IgnoreNextLineComment = "ignore/next-line"
38 FileOwnerComment = "file/owner"
39 RuleOwnerComment = "rule/owner"
40 FileDisableComment = "file/disable"
41 DisableComment = "disable"
42 FileSnoozeComment = "file/snooze"
43 SnoozeComment = "snooze"
44 RuleSetComment = "rule/set"
45)
46
47type CommentValue interface {
48 String() string
49}
50
51type Comment struct {
52 Value CommentValue
53 Type Type
54}
55
56func parseType(s string) Type {
57 switch s {
58 case IgnoreFileComment:
59 return IgnoreFileType
60 case IgnoreLineComment:
61 return IgnoreLineType
62 case IgnoreBeginComment:
63 return IgnoreBeginType
64 case IgnoreEndComment:
65 return IgnoreEndType
66 case IgnoreNextLineComment:
67 return IgnoreNextLineType
68 case FileOwnerComment:
69 return FileOwnerType
70 case RuleOwnerComment:
71 return RuleOwnerType
72 case FileDisableComment:
73 return FileDisableType
74 case DisableComment:
75 return DisableType
76 case FileSnoozeComment:
77 return FileSnoozeType
78 case SnoozeComment:
79 return SnoozeType
80 case RuleSetComment:
81 return RuleSetType
82 default:
83 return UnknownType
84 }
85}
86
87type Invalid struct {
88 Err error
89}
90
91func (i Invalid) String() string {
92 return i.Err.Error()
93}
94
95type Owner struct {
96 Name string
97}
98
99func (o Owner) String() string {
100 return o.Name
101}
102
103type Disable struct {
104 Match string
105}
106
107func (d Disable) String() string {
108 return d.Match
109}
110
111type Snooze struct {
112 Until time.Time
113 Match string
114}
115
116func (s Snooze) String() string {
117 return fmt.Sprintf("%s %s", s.Until.Format(time.RFC3339), s.Match)
118}
119
120type RuleSet struct {
121 Value string
122}
123
124func (r RuleSet) String() string {
125 return r.Value
126}
127
128func parseSnooze(s string) (snz Snooze, err error) {
129 parts := strings.SplitN(s, " ", 2)
130 if len(parts) != 2 {
131 return Snooze{}, fmt.Errorf("invalid snooze comment, expected '$TIME $MATCH' got %q", s)
132 }
133
134 snz = Snooze{Match: parts[1]}
135 snz.Until, err = time.Parse(time.RFC3339, parts[0])
136 if err != nil {
137 snz.Until, err = time.Parse("2006-01-02", parts[0])
138 }
139 if err != nil {
140 return snz, fmt.Errorf("invalid snooze timestamp: %w", err)
141 }
142 return snz, nil
143}
144
145func parseValue(typ Type, s string) (CommentValue, error) {
146 switch typ {
147 case IgnoreFileType, IgnoreLineType, IgnoreBeginType, IgnoreEndType, IgnoreNextLineType:
148 if s != "" {
149 return nil, fmt.Errorf("unexpected comment suffix: %q", s)
150 }
151 case FileOwnerType:
152 if s == "" {
153 return nil, fmt.Errorf("missing %s value", FileOwnerComment)
154 }
155 return Owner{Name: s}, nil
156 case RuleOwnerType:
157 if s == "" {
158 return nil, fmt.Errorf("missing %s value", RuleOwnerComment)
159 }
160 return Owner{Name: s}, nil
161 case FileDisableType:
162 if s == "" {
163 return nil, fmt.Errorf("missing %s value", FileDisableComment)
164 }
165 return Disable{Match: s}, nil
166 case DisableType:
167 if s == "" {
168 return nil, fmt.Errorf("missing %s value", DisableComment)
169 }
170 return Disable{Match: s}, nil
171 case FileSnoozeType:
172 if s == "" {
173 return nil, fmt.Errorf("missing %s value", FileSnoozeComment)
174 }
175 return parseSnooze(s)
176 case SnoozeType:
177 if s == "" {
178 return nil, fmt.Errorf("missing %s value", SnoozeComment)
179 }
180 return parseSnooze(s)
181 case RuleSetType:
182 if s == "" {
183 return nil, fmt.Errorf("missing %s value", RuleSetComment)
184 }
185 return RuleSet{Value: s}, nil
186 case UnknownType, InvalidComment:
187 // pass
188 }
189 return nil, nil
190}
191
192const (
193 needsHash uint8 = iota
194 needsPrefix
195 readsPrefix
196 needsType
197 readsType
198 needsValue
199 readsValue
200)
201
202func parseComment(s string) (parsed []Comment, err error) {
203 var buf strings.Builder
204 var c Comment
205
206 state := needsHash
207 for _, r := range s + "\n" {
208 READRUNE:
209 switch state {
210 case needsHash:
211 if r != '#' {
212 goto NEXT
213 }
214 state = needsPrefix
215 buf.Reset()
216 c.Type = UnknownType
217 c.Value = nil
218 case needsPrefix:
219 if unicode.IsSpace(r) {
220 goto NEXT
221 }
222 state = readsPrefix
223 goto READRUNE
224 case readsPrefix:
225 if unicode.IsLetter(r) {
226 _, _ = buf.WriteRune(r)
227 goto NEXT
228 }
229 if unicode.IsSpace(r) {
230 // Invalid comment prefix, ignore this comment.
231 if buf.String() != Prefix {
232 buf.Reset()
233 state = needsHash
234 goto NEXT
235 }
236 buf.Reset()
237 state = needsType
238 goto NEXT
239 }
240 // Invalid character in the prefix, ignore this comment.
241 state = needsHash
242 case needsType:
243 if r == '#' {
244 state = needsHash
245 goto READRUNE
246 }
247 if unicode.IsSpace(r) {
248 goto NEXT
249 }
250 state = readsType
251 goto READRUNE
252 case readsType:
253 if unicode.IsLetter(r) || r == '/' || r == '-' {
254 _, _ = buf.WriteRune(r)
255 goto NEXT
256 }
257 if unicode.IsSpace(r) || r == '\n' {
258 c.Type = parseType(buf.String())
259 buf.Reset()
260 if c.Type == UnknownType {
261 state = needsHash
262 } else {
263 state = needsValue
264 }
265
266 }
267 case needsValue:
268 if unicode.IsSpace(r) {
269 goto NEXT
270 }
271 state = readsValue
272 goto READRUNE
273 case readsValue:
274 if r == '\n' {
275 goto NEXT
276 }
277 _, _ = buf.WriteRune(r)
278 }
279 NEXT:
280 }
281
282 if c.Type != UnknownType {
283 c.Value, err = parseValue(c.Type, strings.TrimSpace(buf.String()))
284 parsed = append(parsed, c)
285 }
286
287 return parsed, err
288}
289
290func Parse(text string) (comments []Comment) {
291 sc := bufio.NewScanner(strings.NewReader(text))
292 for sc.Scan() {
293 line := sc.Text()
294 parsed, err := parseComment(line)
295 if err != nil {
296 comments = append(comments, Comment{
297 Type: InvalidComment,
298 Value: Invalid{Err: err},
299 })
300 continue
301 }
302 comments = append(comments, parsed...)
303 }
304 return comments
305}
306
307func Only[T any](src []Comment, typ Type) []T {
308 dst := make([]T, 0, len(src))
309 for _, c := range src {
310 if c.Type != typ {
311 continue
312 }
313 dst = append(dst, c.Value.(T))
314 }
315 return dst
316}
317
318func IsRuleComment(typ Type) bool {
319 // nolint:exhaustive
320 switch typ {
321 case RuleOwnerType, DisableType, SnoozeType, RuleSetType:
322 return true
323 }
324 return false
325}
326