1 package toml
2 3 import (
4 "fmt"
5 "strings"
6 )
7 8 // ParseError is returned when there is an error parsing the TOML syntax such as
9 // invalid syntax, duplicate keys, etc.
10 //
11 // In addition to the error message itself, you can also print detailed location
12 // information with context by using [ErrorWithPosition]:
13 //
14 // toml: error: Key 'fruit' was already created and cannot be used as an array.
15 //
16 // At line 4, column 2-7:
17 //
18 // 2 | fruit = []
19 // 3 |
20 // 4 | [[fruit]] # Not allowed
21 // ^^^^^
22 //
23 // [ErrorWithUsage] can be used to print the above with some more detailed usage
24 // guidance:
25 //
26 // toml: error: newlines not allowed within inline tables
27 //
28 // At line 1, column 18:
29 //
30 // 1 | x = [{ key = 42 #
31 // ^
32 //
33 // Error help:
34 //
35 // Inline tables must always be on a single line:
36 //
37 // table = {key = 42, second = 43}
38 //
39 // It is invalid to split them over multiple lines like so:
40 //
41 // # INVALID
42 // table = {
43 // key = 42,
44 // second = 43
45 // }
46 //
47 // Use regular for this:
48 //
49 // [table]
50 // key = 42
51 // second = 43
52 type ParseError struct {
53 Message string // Short technical message.
54 Usage string // Longer message with usage guidance; may be blank.
55 Position Position // Position of the error
56 LastKey string // Last parsed key, may be blank.
57 58 // Line the error occurred.
59 //
60 // Deprecated: use [Position].
61 Line int
62 63 err error
64 input string
65 }
66 67 // Position of an error.
68 type Position struct {
69 Line int // Line number, starting at 1.
70 Col int // Error column, starting at 1.
71 Start int // Start of error, as byte offset starting at 0.
72 Len int // Length of the error in bytes.
73 }
74 75 func (p Position) withCol(tomlFile string) Position {
76 var (
77 pos int
78 lines = strings.Split(tomlFile, "\n")
79 )
80 for i := range lines {
81 ll := len(lines[i]) + 1 // +1 for the removed newline
82 if pos+ll >= p.Start {
83 p.Col = p.Start - pos + 1
84 if p.Col < 1 { // Should never happen, but just in case.
85 p.Col = 1
86 }
87 break
88 }
89 pos += ll
90 }
91 return p
92 }
93 94 func (pe ParseError) Error() string {
95 if pe.LastKey == "" {
96 return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, pe.Message)
97 }
98 return fmt.Sprintf("toml: line %d (last key %q): %s",
99 pe.Position.Line, pe.LastKey, pe.Message)
100 }
101 102 // ErrorWithPosition returns the error with detailed location context.
103 //
104 // See the documentation on [ParseError].
105 func (pe ParseError) ErrorWithPosition() string {
106 if pe.input == "" { // Should never happen, but just in case.
107 return pe.Error()
108 }
109 110 // TODO: don't show control characters as literals? This may not show up
111 // well everywhere.
112 113 var (
114 lines = strings.Split(pe.input, "\n")
115 b = new(strings.Builder)
116 )
117 if pe.Position.Len == 1 {
118 fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
119 pe.Message, pe.Position.Line, pe.Position.Col)
120 } else {
121 fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
122 pe.Message, pe.Position.Line, pe.Position.Col, pe.Position.Col+pe.Position.Len-1)
123 }
124 if pe.Position.Line > 2 {
125 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3]))
126 }
127 if pe.Position.Line > 1 {
128 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2]))
129 }
130 131 /// Expand tabs, so that the ^^^s are at the correct position, but leave
132 /// "column 10-13" intact. Adjusting this to the visual column would be
133 /// better, but we don't know the tabsize of the user in their editor, which
134 /// can be 8, 4, 2, or something else. We can't know. So leaving it as the
135 /// character index is probably the "most correct".
136 expanded := expandTab(lines[pe.Position.Line-1])
137 diff := len(expanded) - len(lines[pe.Position.Line-1])
138 139 fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded)
140 fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", pe.Position.Col-1+diff), strings.Repeat("^", pe.Position.Len))
141 return b.String()
142 }
143 144 // ErrorWithUsage returns the error with detailed location context and usage
145 // guidance.
146 //
147 // See the documentation on [ParseError].
148 func (pe ParseError) ErrorWithUsage() string {
149 m := pe.ErrorWithPosition()
150 if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
151 lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
152 for i := range lines {
153 if lines[i] != "" {
154 lines[i] = " " + lines[i]
155 }
156 }
157 return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
158 }
159 return m
160 }
161 162 func expandTab(s string) string {
163 var (
164 b strings.Builder
165 l int
166 fill = func(n int) string {
167 b := make([]byte, n)
168 for i := range b {
169 b[i] = ' '
170 }
171 return string(b)
172 }
173 )
174 b.Grow(len(s))
175 for _, r := range s {
176 switch r {
177 case '\t':
178 tw := 8 - l%8
179 b.WriteString(fill(tw))
180 l += tw
181 default:
182 b.WriteRune(r)
183 l += 1
184 }
185 }
186 return b.String()
187 }
188 189 type (
190 errLexControl struct{ r rune }
191 errLexEscape struct{ r rune }
192 errLexUTF8 struct{ b byte }
193 errParseDate struct{ v string }
194 errLexInlineTableNL struct{}
195 errLexStringNL struct{}
196 errParseRange struct {
197 i any // int or float
198 size string // "int64", "uint16", etc.
199 }
200 errUnsafeFloat struct {
201 i interface{} // float32 or float64
202 size string // "float32" or "float64"
203 }
204 errParseDuration struct{ d string }
205 )
206 207 func (e errLexControl) Error() string {
208 return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
209 }
210 func (e errLexControl) Usage() string { return "" }
211 212 func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
213 func (e errLexEscape) Usage() string { return usageEscape }
214 func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
215 func (e errLexUTF8) Usage() string { return "" }
216 func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) }
217 func (e errParseDate) Usage() string { return usageDate }
218 func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
219 func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
220 func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
221 func (e errLexStringNL) Usage() string { return usageStringNewline }
222 func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
223 func (e errParseRange) Usage() string { return usageIntOverflow }
224 func (e errUnsafeFloat) Error() string {
225 return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size)
226 }
227 func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat }
228 func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
229 func (e errParseDuration) Usage() string { return usageDuration }
230 231 const usageEscape = `
232 A '\' inside a "-delimited string is interpreted as an escape character.
233 234 The following escape sequences are supported:
235 \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
236 237 To prevent a '\' from being recognized as an escape character, use either:
238 239 - a ' or '''-delimited string; escape characters aren't processed in them; or
240 - write two backslashes to get a single backslash: '\\'.
241 242 If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
243 instead of '\' will usually also work: "C:/Users/martin".
244 `
245 246 const usageInlineNewline = `
247 Inline tables must always be on a single line:
248 249 table = {key = 42, second = 43}
250 251 It is invalid to split them over multiple lines like so:
252 253 # INVALID
254 table = {
255 key = 42,
256 second = 43
257 }
258 259 Use regular for this:
260 261 [table]
262 key = 42
263 second = 43
264 `
265 266 const usageStringNewline = `
267 Strings must always be on a single line, and cannot span more than one line:
268 269 # INVALID
270 string = "Hello,
271 world!"
272 273 Instead use """ or ''' to split strings over multiple lines:
274 275 string = """Hello,
276 world!"""
277 `
278 279 const usageIntOverflow = `
280 This number is too large; this may be an error in the TOML, but it can also be a
281 bug in the program that uses too small of an integer.
282 283 The maximum and minimum values are:
284 285 size │ lowest │ highest
286 ───────┼────────────────┼──────────────
287 int8 │ -128 │ 127
288 int16 │ -32,768 │ 32,767
289 int32 │ -2,147,483,648 │ 2,147,483,647
290 int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
291 uint8 │ 0 │ 255
292 uint16 │ 0 │ 65,535
293 uint32 │ 0 │ 4,294,967,295
294 uint64 │ 0 │ 1.8 × 10¹⁸
295 296 int refers to int32 on 32-bit systems and int64 on 64-bit systems.
297 `
298 299 const usageUnsafeFloat = `
300 This number is outside of the "safe" range for floating point numbers; whole
301 (non-fractional) numbers outside the below range can not always be represented
302 accurately in a float, leading to some loss of accuracy.
303 304 Explicitly mark a number as a fractional unit by adding ".0", which will incur
305 some loss of accuracy; for example:
306 307 f = 2_000_000_000.0
308 309 Accuracy ranges:
310 311 float32 = 16,777,215
312 float64 = 9,007,199,254,740,991
313 `
314 315 const usageDuration = `
316 A duration must be as "number<unit>", without any spaces. Valid units are:
317 318 ns nanoseconds (billionth of a second)
319 us, µs microseconds (millionth of a second)
320 ms milliseconds (thousands of a second)
321 s seconds
322 m minutes
323 h hours
324 325 You can combine multiple units; for example "5m10s" for 5 minutes and 10
326 seconds.
327 `
328 329 const usageDate = `
330 A TOML datetime must be in one of the following formats:
331 332 2006-01-02T15:04:05Z07:00 Date and time, with timezone.
333 2006-01-02T15:04:05 Date and time, but without timezone.
334 2006-01-02 Date without a time or timezone.
335 15:04:05 Just a time, without any timezone.
336 337 Seconds may optionally have a fraction, up to nanosecond precision:
338 339 15:04:05.123
340 15:04:05.856018510
341 `
342 343 // TOML 1.1:
344 // The seconds part in times is optional, and may be omitted:
345 // 2006-01-02T15:04Z07:00
346 // 2006-01-02T15:04
347 // 15:04
348