- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет, файл без него негоден; bot_token стал только ключом доступа и при enabled = false не читается вовсе, а пустой при enabled = true роняет старт - выключенный вход даёт подъём одним входом без единого обращения к Telegram и записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение - отказ разбора файла настроек больше не пересказывает toml — её ParseError несёт в тексте разбираемое значение, и оборванная строка секретного ключа уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
56 lines
4.4 KiB
Markdown
56 lines
4.4 KiB
Markdown
# Разбор 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`; при
|
||
подъёме версии библиотеки их отказ читается как сигнал перечитать эту записку, а
|
||
не как случайный шум.
|