cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.50.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/comments/comments.go

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