1 package toml
2 3 import (
4 "fmt"
5 "strconv"
6 "strings"
7 8 "github.com/pelletier/go-toml/v2/internal/danger"
9 "github.com/pelletier/go-toml/v2/unstable"
10 )
11 12 // DecodeError represents an error encountered during the parsing or decoding
13 // of a TOML document.
14 //
15 // In addition to the error message, it contains the position in the document
16 // where it happened, as well as a human-readable representation that shows
17 // where the error occurred in the document.
18 type DecodeError struct {
19 message string
20 line int
21 column int
22 key Key
23 24 human string
25 }
26 27 // StrictMissingError occurs in a TOML document that does not have a
28 // corresponding field in the target value. It contains all the missing fields
29 // in Errors.
30 //
31 // Emitted by Decoder when DisallowUnknownFields() was called.
32 type StrictMissingError struct {
33 // One error per field that could not be found.
34 Errors []DecodeError
35 }
36 37 // Error returns the canonical string for this error.
38 func (s *StrictMissingError) Error() string {
39 return "strict mode: fields in the document are missing in the target struct"
40 }
41 42 // String returns a human readable description of all errors.
43 func (s *StrictMissingError) String() string {
44 var buf strings.Builder
45 46 for i, e := range s.Errors {
47 if i > 0 {
48 buf.WriteString("\n---\n")
49 }
50 51 buf.WriteString(e.String())
52 }
53 54 return buf.String()
55 }
56 57 type Key []string
58 59 // Error returns the error message contained in the DecodeError.
60 func (e *DecodeError) Error() string {
61 return "toml: " + e.message
62 }
63 64 // String returns the human-readable contextualized error. This string is multi-line.
65 func (e *DecodeError) String() string {
66 return e.human
67 }
68 69 // Position returns the (line, column) pair indicating where the error
70 // occurred in the document. Positions are 1-indexed.
71 func (e *DecodeError) Position() (row int, column int) {
72 return e.line, e.column
73 }
74 75 // Key that was being processed when the error occurred. The key is present only
76 // if this DecodeError is part of a StrictMissingError.
77 func (e *DecodeError) Key() Key {
78 return e.key
79 }
80 81 // decodeErrorFromHighlight creates a DecodeError referencing a highlighted
82 // range of bytes from document.
83 //
84 // highlight needs to be a sub-slice of document, or this function panics.
85 //
86 // The function copies all bytes used in DecodeError, so that document and
87 // highlight can be freely deallocated.
88 //
89 //nolint:funlen
90 func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
91 offset := danger.SubsliceOffset(document, de.Highlight)
92 93 errMessage := de.Error()
94 errLine, errColumn := positionAtEnd(document[:offset])
95 before, after := linesOfContext(document, de.Highlight, offset, 3)
96 97 var buf strings.Builder
98 99 maxLine := errLine + len(after) - 1
100 lineColumnWidth := len(strconv.Itoa(maxLine))
101 102 // Write the lines of context strictly before the error.
103 for i := len(before) - 1; i > 0; i-- {
104 line := errLine - i
105 buf.WriteString(formatLineNumber(line, lineColumnWidth))
106 buf.WriteString("|")
107 108 if len(before[i]) > 0 {
109 buf.WriteString(" ")
110 buf.Write(before[i])
111 }
112 113 buf.WriteRune('\n')
114 }
115 116 // Write the document line that contains the error.
117 118 buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
119 buf.WriteString("| ")
120 121 if len(before) > 0 {
122 buf.Write(before[0])
123 }
124 125 buf.Write(de.Highlight)
126 127 if len(after) > 0 {
128 buf.Write(after[0])
129 }
130 131 buf.WriteRune('\n')
132 133 // Write the line with the error message itself (so it does not have a line
134 // number).
135 136 buf.WriteString(strings.Repeat(" ", lineColumnWidth))
137 buf.WriteString("| ")
138 139 if len(before) > 0 {
140 buf.WriteString(strings.Repeat(" ", len(before[0])))
141 }
142 143 buf.WriteString(strings.Repeat("~", len(de.Highlight)))
144 145 if len(errMessage) > 0 {
146 buf.WriteString(" ")
147 buf.WriteString(errMessage)
148 }
149 150 // Write the lines of context strictly after the error.
151 152 for i := 1; i < len(after); i++ {
153 buf.WriteRune('\n')
154 line := errLine + i
155 buf.WriteString(formatLineNumber(line, lineColumnWidth))
156 buf.WriteString("|")
157 158 if len(after[i]) > 0 {
159 buf.WriteString(" ")
160 buf.Write(after[i])
161 }
162 }
163 164 return &DecodeError{
165 message: errMessage,
166 line: errLine,
167 column: errColumn,
168 key: de.Key,
169 human: buf.String(),
170 }
171 }
172 173 func formatLineNumber(line int, width int) string {
174 format := "%" + strconv.Itoa(width) + "d"
175 176 return fmt.Sprintf(format, line)
177 }
178 179 func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
180 return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
181 }
182 183 func beforeLines(document []byte, offset int, linesAround int) [][]byte {
184 var beforeLines [][]byte
185 186 // Walk the document backward from the highlight to find previous lines
187 // of context.
188 rest := document[:offset]
189 backward:
190 for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
191 switch {
192 case rest[o] == '\n':
193 // handle individual lines
194 beforeLines = append(beforeLines, rest[o+1:])
195 rest = rest[:o]
196 o = len(rest) - 1
197 case o == 0:
198 // add the first line only if it's non-empty
199 beforeLines = append(beforeLines, rest)
200 201 break backward
202 default:
203 o--
204 }
205 }
206 207 return beforeLines
208 }
209 210 func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
211 var afterLines [][]byte
212 213 // Walk the document forward from the highlight to find the following
214 // lines of context.
215 rest := document[offset+len(highlight):]
216 forward:
217 for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
218 switch {
219 case rest[o] == '\n':
220 // handle individual lines
221 afterLines = append(afterLines, rest[:o])
222 rest = rest[o+1:]
223 o = 0
224 225 case o == len(rest)-1:
226 // add last line only if it's non-empty
227 afterLines = append(afterLines, rest)
228 229 break forward
230 default:
231 o++
232 }
233 }
234 235 return afterLines
236 }
237 238 func positionAtEnd(b []byte) (row int, column int) {
239 row = 1
240 column = 1
241 242 for _, c := range b {
243 if c == '\n' {
244 row++
245 column = 1
246 } else {
247 column++
248 }
249 }
250 251 return
252 }
253