# Разбор TOML: какое семейство отказов несёт значения из файла

Отвечает на вопрос, возникший по ходу задачи `telegram-enabled-flag`: можно ли
пересказывать отказ библиотеки разбора в журнал, если в файле настроек лежат
секреты. Наблюдение понадобилось потому, что ревью дизайна назвало этот путь
утечкой, а чинить его без разреза пришлось бы выбрасыванием всего текста отказа —
то есть платой разборчивостью на каждой опечатке.

## Как снималось

Не замером, а **чтением исходников** зависимости, зафиксированной в `go.mod`:
`github.com/BurntSushi/toml` версии **v1.5.0**. Смотрел `error.go`, `parse.go`,
`decode.go`, `meta.go`, `lex.go` в кэше модулей. Дополнительно прогонял
`toml.Decode` на правдоподобных опечатках — в каталоге вне репозитория, чтобы не
править код проекта.

## Что выяснилось

- **Значения из файла несёт ровно одно семейство отказов — `toml.ParseError`.**
  Его поле `Message` собирается из разбираемого куска: `Invalid float value: %q`
  (`parse.go:341`), `invalid duration: %q`, `%v is out of range`, `Invalid
  integer %q`. Туда же лексер отдаёт свои отказы через `panicItemf`
  (`parse.go:134`).
- **Прочие отказы декодера значений не содержат вовсе.** Их строит `md.e`
  (`decode.go:577`) и `md.badtype` — из имён ключей, имён типов (`%T` через
  `fmtType`) и длин. Обойдены все места: `decode.go:282,288,297,329,348,385,388,399,428,437,467,487,518,552,561`.
- **`LastKey` секрета нести не может.** Текущий ключ присваивается только после
  `itemKeyEnd`, то есть после `=` (`parse.go:200`), а лексер ключа до `=` не
  доходит (`lex.go:481-501`). Посторонняя строка со значением ключом не станет.
- **Поле `Line` у `ParseError` врёт, а `Position.Line` — нет.** `panicErr` и
  `panicItemf` кладут в устаревшее поле `Line` значение `it.pos.Len`, то есть
  **длину**, а не номер строки (`parse.go:97,106`). Брать надо `Position.Line`.
- **`ParseError` возвращается значением, не указателем** (`decode.go:564`,
  `parse.go` целиком), поэтому `errors.As` берёт целью `toml.ParseError`, а не
  `*toml.ParseError`. `Unwrap` у типа нет.
- **Ветка без последнего ключа достижима обычной опечаткой.** Незакрытая скобка
  секции даёт `LastKey=""`:

  ```
  вход "[telegram\nenabled = true\n"
  → LastKey="" err=toml: line 2: expected '.' or ']' to end table name, but got '\n' instead
  ```

## Что из этого следует для кода

Разрез по семейству отказа: `ParseError` пересобирается своими словами — путь,
строка, столбец, последний ключ, — а его `Message` не берётся; прочие отказы
проходят как есть. Так инвариант «Секрет не покидает конфиг» держится, а
несовпадение типов по-прежнему называет ключ и типы.

**Наблюдение привязано к версии.** Версия, переложившая значение в другое
семейство или сменившая возврат на указатель, вернёт утечку молча. Держат это
проверки поломанного файла настроек в `internal/config/config_test.go`; при
подъёме версии библиотеки их отказ читается как сигнал перечитать эту записку, а
не как случайный шум.
