1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6 "bytes"
7 "errors"
8 "fmt"
9 "io"
10 "math"
11 "regexp"
12 "strconv"
13 "strings"
14
15 "go.yaml.in/yaml/v3"
16)
17
18// MaxFileBytes is the largest document [ReadYAML] accepts: 1 MiB. The check
19// comes before any parsing, so that a hostile file costs nothing more than
20// its length; a file this size can still expand through aliases (see
21// [MaxNodes]).
22//
23// TODO(verify with recording): GitHub's own limit and message.
24const MaxFileBytes = 1 << 20
25
26// MaxNodes is the number of YAML nodes a document may expand to, counting
27// every use of an alias. The reference expands aliases without a limit, so a
28// small document of nested aliases can allocate gigabytes.
29const MaxNodes = 200_000
30
31type rkind int
32
33const (
34 rLiteral rkind = iota
35 rSeq
36 rMap
37)
38
39// rnode is a YAML node after alias resolution: the tree the template reader
40// walks.
41type rnode struct {
42 kind rkind
43 pos Pos
44 lit Token // rLiteral: a *String, *Number, *Boolean or *Null
45 items []*rnode
46 keys []*rnode // rMap: the keys, rLiteral nodes with string tokens
47 vals []*rnode // rMap: the values, parallel to keys
48 // quoted and block describe how a string scalar was written; they only
49 // matter to the position of expressions inside it.
50 quoted bool
51 block bool
52 // badKey is set on a key that is a collection.
53 badKey bool
54}
55
56// yamlResult is the outcome of reading a YAML document.
57type yamlResult struct {
58 root *rnode // nil for an empty document
59 errs []*Error
60}
61
62type yamlBuilder struct {
63 file string
64 nodes int
65 errs []*Error
66 stack map[*yaml.Node]bool
67 over bool
68}
69
70// readYAMLDocument parses content with yaml.v3 and builds the tree. Syntax
71// errors are returned in errs, with root nil.
72func readYAMLDocument(file string, content []byte) (res yamlResult) {
73 defer func() {
74 if r := recover(); r != nil {
75 res = yamlResult{errs: []*Error{{File: file, Raw: fmt.Sprintf("%v", r)}}}
76 }
77 }()
78 dec := yaml.NewDecoder(bytes.NewReader(content))
79 var doc yaml.Node
80 err := dec.Decode(&doc)
81 if errors.Is(err, io.EOF) {
82 return yamlResult{}
83 }
84 if err != nil {
85 return yamlResult{errs: []*Error{yamlError(file, err)}}
86 }
87 var second yaml.Node
88 if err := dec.Decode(&second); err == nil {
89 e := &Error{File: file, Raw: "Source contains multiple documents; please use YAML.parseAllDocuments()"}
90 e.Pos = Pos{second.Line, second.Column}
91 e.fileOnly = true
92 e.Raw += fmt.Sprintf(" at line %d, column %d", second.Line, second.Column)
93 return yamlResult{errs: []*Error{e}}
94 } else if !errors.Is(err, io.EOF) {
95 return yamlResult{errs: []*Error{yamlError(file, err)}}
96 }
97 if len(doc.Content) == 0 {
98 return yamlResult{}
99 }
100 b := &yamlBuilder{file: file, stack: map[*yaml.Node]bool{}}
101 root := b.build(doc.Content[0], false)
102 if b.over {
103 return yamlResult{errs: b.errs}
104 }
105 return yamlResult{root: root, errs: b.errs}
106}
107
108var yamlLineRE = regexp.MustCompile(`^(?:yaml: )?(?:unmarshal errors:\n\s*)?(?:line (\d+): )?`)
109
110// yamlError converts an error of yaml.v3 ("yaml: line 3: did not find ...")
111// to an Error. The wording is yaml.v3's, not the JavaScript library's, and
112// only the line is known.
113func yamlError(file string, err error) *Error {
114 msg := err.Error()
115 e := &Error{File: file, fileOnly: true}
116 if m := yamlLineRE.FindStringSubmatch(msg); m != nil {
117 if m[1] != "" {
118 e.Pos.Line, _ = strconv.Atoi(m[1])
119 }
120 msg = msg[len(m[0]):]
121 }
122 e.Raw = msg
123 if e.Pos.Line > 0 {
124 e.Raw += fmt.Sprintf(" at line %d", e.Pos.Line)
125 }
126 return e
127}
128
129func (b *yamlBuilder) count() bool {
130 b.nodes++
131 if b.nodes > MaxNodes {
132 if !b.over {
133 b.over = true
134 b.errs = append(b.errs, &Error{File: b.file, Raw: fmt.Sprintf("The document is too large: it has more than %d nodes once aliases are expanded", MaxNodes)})
135 }
136 return false
137 }
138 return true
139}
140
141func (b *yamlBuilder) build(n *yaml.Node, asKey bool) *rnode {
142 if b.over || !b.count() {
143 return &rnode{kind: rLiteral, lit: NewNull(Origin{})}
144 }
145 pos := Pos{n.Line, n.Column}
146 o := Origin{File: b.file, Pos: pos}
147 switch n.Kind {
148 case yaml.AliasNode:
149 target := n.Alias
150 if target == nil || b.stack[target] {
151 b.errs = append(b.errs, &Error{File: b.file, Pos: pos, Raw: fmt.Sprintf("Circular reference through the YAML alias '*%s'", n.Value)})
152 return &rnode{kind: rLiteral, pos: pos, lit: NewNull(o)}
153 }
154 b.stack[target] = true
155 r := b.build(target, asKey)
156 delete(b.stack, target)
157 return r
158 case yaml.SequenceNode:
159 if asKey {
160 return &rnode{kind: rLiteral, pos: pos, lit: NewString(o, ""), badKey: true}
161 }
162 r := &rnode{kind: rSeq, pos: pos}
163 b.stack[n] = true
164 for _, c := range n.Content {
165 r.items = append(r.items, b.build(c, false))
166 }
167 delete(b.stack, n)
168 return r
169 case yaml.MappingNode:
170 if asKey {
171 return &rnode{kind: rLiteral, pos: pos, lit: NewString(o, ""), badKey: true}
172 }
173 r := &rnode{kind: rMap, pos: pos}
174 b.stack[n] = true
175 for i := 0; i+1 < len(n.Content); i += 2 {
176 r.keys = append(r.keys, b.build(n.Content[i], true))
177 r.vals = append(r.vals, b.build(n.Content[i+1], false))
178 }
179 delete(b.stack, n)
180 return r
181 case yaml.ScalarNode:
182 r := &rnode{kind: rLiteral, pos: pos}
183 if asKey {
184 // Keys are always strings, whatever they look like.
185 r.lit = NewString(o, n.Value)
186 } else {
187 r.lit = resolveScalar(o, n)
188 }
189 r.quoted = n.Style&(yaml.DoubleQuotedStyle|yaml.SingleQuotedStyle) != 0
190 r.block = n.Style&(yaml.LiteralStyle|yaml.FoldedStyle) != 0
191 return r
192 }
193 return &rnode{kind: rLiteral, pos: pos, lit: NewNull(o)}
194}
195
196// resolveScalar gives a scalar its type the way the YAML 1.2 core schema of
197// the JavaScript yaml library does: only plain scalars are resolved, and an
198// explicit !!tag overrides the guess.
199func resolveScalar(o Origin, n *yaml.Node) Token {
200 if n.Style&yaml.TaggedStyle != 0 {
201 switch n.ShortTag() {
202 case "!!str":
203 return NewString(o, n.Value)
204 case "!!null", "!!bool", "!!int", "!!float":
205 if t := resolvePlain(o, n.Value); t.Kind() != KindString {
206 return t
207 }
208 if n.ShortTag() == "!!null" && n.Value == "" {
209 return NewNull(o)
210 }
211 }
212 return NewString(o, n.Value)
213 }
214 if n.Style&(yaml.DoubleQuotedStyle|yaml.SingleQuotedStyle|yaml.LiteralStyle|yaml.FoldedStyle) != 0 {
215 return NewString(o, n.Value)
216 }
217 return resolvePlain(o, n.Value)
218}
219
220var (
221 reInt = regexp.MustCompile(`^[-+]?[0-9]+$`)
222 reOct = regexp.MustCompile(`^0o[0-7]+$`)
223 reHex = regexp.MustCompile(`^0x[0-9a-fA-F]+$`)
224 reFloat = regexp.MustCompile(`^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?$`)
225 reInf = regexp.MustCompile(`^[-+]?\.(?:inf|Inf|INF)$`)
226 reNaN = regexp.MustCompile(`^\.(?:nan|NaN|NAN)$`)
227)
228
229func resolvePlain(o Origin, s string) Token {
230 switch s {
231 case "", "~", "null", "Null", "NULL":
232 return NewNull(o)
233 case "true", "True", "TRUE":
234 return NewBoolean(o, true)
235 case "false", "False", "FALSE":
236 return NewBoolean(o, false)
237 }
238 switch {
239 case reInt.MatchString(s):
240 f, _ := strconv.ParseFloat(s, 64)
241 return NewNumber(o, f)
242 case reOct.MatchString(s):
243 return NewNumber(o, parseBase(s[2:], 8))
244 case reHex.MatchString(s):
245 return NewNumber(o, parseBase(s[2:], 16))
246 case reFloat.MatchString(s):
247 f, _ := strconv.ParseFloat(s, 64)
248 return NewNumber(o, f)
249 case reInf.MatchString(s):
250 if s[0] == '-' {
251 return NewNumber(o, math.Inf(-1))
252 }
253 return NewNumber(o, math.Inf(1))
254 case reNaN.MatchString(s):
255 return NewNumber(o, math.NaN())
256 }
257 return NewString(o, s)
258}
259
260// parseBase converts digits in base 8 or 16 to a float64 without overflowing,
261// like JavaScript's parseInt.
262func parseBase(digits string, base int) float64 {
263 var f float64
264 for _, c := range strings.ToLower(digits) {
265 d := int(c - '0')
266 if c >= 'a' {
267 d = int(c-'a') + 10
268 }
269 f = f*float64(base) + float64(d)
270 }
271 return f
272}