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