cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.57.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/comments/comments.go

336lines · 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 CommentError struct {
88 Err error
89 Line int
90}
91
92func (ce CommentError) Error() string {
93 return ce.Err.Error()
94}
95
96type Invalid struct {
97 Err CommentError
98}
99
100func (i Invalid) String() string {
101 return i.Err.Error()
102}
103
104type Owner struct {
105 Name string
106}
107
108func (o Owner) String() string {
109 return o.Name
110}
111
112type Disable struct {
113 Match string
114}
115
116func (d Disable) String() string {
117 return d.Match
118}
119
120type Snooze struct {
121 Until time.Time
122 Match string
123}
124
125func (s Snooze) String() string {
126 return fmt.Sprintf("%s %s", s.Until.Format(time.RFC3339), s.Match)
127}
128
129type RuleSet struct {
130 Value string
131}
132
133func (r RuleSet) String() string {
134 return r.Value
135}
136
137func parseSnooze(s string) (snz Snooze, err error) {
138 parts := strings.SplitN(s, " ", 2)
139 if len(parts) != 2 {
140 return Snooze{}, fmt.Errorf("invalid snooze comment, expected '$TIME $MATCH' got %q", s)
141 }
142
143 snz = Snooze{Match: parts[1]}
144 snz.Until, err = time.Parse(time.RFC3339, parts[0])
145 if err != nil {
146 snz.Until, err = time.Parse("2006-01-02", parts[0])
147 }
148 if err != nil {
149 return snz, fmt.Errorf("invalid snooze timestamp: %w", err)
150 }
151 return snz, nil
152}
153
154func parseValue(typ Type, s string) (CommentValue, error) {
155 switch typ {
156 case IgnoreFileType, IgnoreLineType, IgnoreBeginType, IgnoreEndType, IgnoreNextLineType:
157 if s != "" {
158 return nil, fmt.Errorf("unexpected comment suffix: %q", s)
159 }
160 case FileOwnerType:
161 if s == "" {
162 return nil, fmt.Errorf("missing %s value", FileOwnerComment)
163 }
164 return Owner{Name: s}, nil
165 case RuleOwnerType:
166 if s == "" {
167 return nil, fmt.Errorf("missing %s value", RuleOwnerComment)
168 }
169 return Owner{Name: s}, nil
170 case FileDisableType:
171 if s == "" {
172 return nil, fmt.Errorf("missing %s value", FileDisableComment)
173 }
174 return Disable{Match: s}, nil
175 case DisableType:
176 if s == "" {
177 return nil, fmt.Errorf("missing %s value", DisableComment)
178 }
179 return Disable{Match: s}, nil
180 case FileSnoozeType:
181 if s == "" {
182 return nil, fmt.Errorf("missing %s value", FileSnoozeComment)
183 }
184 return parseSnooze(s)
185 case SnoozeType:
186 if s == "" {
187 return nil, fmt.Errorf("missing %s value", SnoozeComment)
188 }
189 return parseSnooze(s)
190 case RuleSetType:
191 if s == "" {
192 return nil, fmt.Errorf("missing %s value", RuleSetComment)
193 }
194 return RuleSet{Value: s}, nil
195 case UnknownType, InvalidComment:
196 // pass
197 }
198 return nil, nil
199}
200
201const (
202 needsHash uint8 = iota
203 needsPrefix
204 readsPrefix
205 needsType
206 readsType
207 needsValue
208 readsValue
209)
210
211func parseComment(s string) (parsed []Comment, err error) {
212 var buf strings.Builder
213 var c Comment
214
215 state := needsHash
216 for _, r := range s + "\n" {
217 READRUNE:
218 switch state {
219 case needsHash:
220 if r != '#' {
221 goto NEXT
222 }
223 state = needsPrefix
224 buf.Reset()
225 c.Type = UnknownType
226 c.Value = nil
227 case needsPrefix:
228 if unicode.IsSpace(r) {
229 goto NEXT
230 }
231 state = readsPrefix
232 goto READRUNE
233 case readsPrefix:
234 if unicode.IsLetter(r) {
235 _, _ = buf.WriteRune(r)
236 goto NEXT
237 }
238 if unicode.IsSpace(r) {
239 // Invalid comment prefix, ignore this comment.
240 if buf.String() != Prefix {
241 buf.Reset()
242 state = needsHash
243 goto NEXT
244 }
245 buf.Reset()
246 state = needsType
247 goto NEXT
248 }
249 // Invalid character in the prefix, ignore this comment.
250 state = needsHash
251 case needsType:
252 if r == '#' {
253 state = needsHash
254 goto READRUNE
255 }
256 if unicode.IsSpace(r) {
257 goto NEXT
258 }
259 state = readsType
260 goto READRUNE
261 case readsType:
262 if unicode.IsLetter(r) || r == '/' || r == '-' {
263 _, _ = buf.WriteRune(r)
264 goto NEXT
265 }
266 if unicode.IsSpace(r) || r == '\n' {
267 c.Type = parseType(buf.String())
268 buf.Reset()
269 if c.Type == UnknownType {
270 state = needsHash
271 } else {
272 state = needsValue
273 }
274
275 }
276 case needsValue:
277 if unicode.IsSpace(r) {
278 goto NEXT
279 }
280 state = readsValue
281 goto READRUNE
282 case readsValue:
283 if r == '\n' {
284 goto NEXT
285 }
286 _, _ = buf.WriteRune(r)
287 }
288 NEXT:
289 }
290
291 if c.Type != UnknownType {
292 c.Value, err = parseValue(c.Type, strings.TrimSpace(buf.String()))
293 parsed = append(parsed, c)
294 }
295
296 return parsed, err
297}
298
299func Parse(lineno int, text string) (comments []Comment) {
300 sc := bufio.NewScanner(strings.NewReader(text))
301 var index int
302 for sc.Scan() {
303 line := sc.Text()
304 parsed, err := parseComment(line)
305 if err != nil {
306 comments = append(comments, Comment{
307 Type: InvalidComment,
308 Value: Invalid{Err: CommentError{Line: lineno + index, Err: err}},
309 })
310 continue
311 }
312 comments = append(comments, parsed...)
313 index++
314 }
315 return comments
316}
317
318func Only[T any](src []Comment, typ Type) []T {
319 dst := make([]T, 0, len(src))
320 for _, c := range src {
321 if c.Type != typ {
322 continue
323 }
324 dst = append(dst, c.Value.(T))
325 }
326 return dst
327}
328
329func IsRuleComment(typ Type) bool {
330 // nolint:exhaustive
331 switch typ {
332 case RuleOwnerType, DisableType, SnoozeType, RuleSetType:
333 return true
334 }
335 return false
336}
337