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