config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет, файл без него негоден; bot_token стал только ключом доступа и при enabled = false не читается вовсе, а пустой при enabled = true роняет старт - выключенный вход даёт подъём одним входом без единого обращения к Telegram и записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение - отказ разбора файла настроек больше не пересказывает toml — её ParseError несёт в тексте разбираемое значение, и оборванная строка секретного ключа уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
This commit is contained in:
@@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-13 | [Разбор TOML: какое семейство отказов несёт значения из файла](toml-decode-errors.md) | Значения только в `ParseError.Message`, врущее поле `Line`, отказ значением в BurntSushi/toml v1.5.0 |
|
||||
| 2026-08-12 | [PocketBase: умолчания, которые ломают штатный сценарий](pocketbase-defaults.md) | Потолок файла 5 МиБ, тело 32 МиБ, таймаут чтения, суффикс имени, хук правки |
|
||||
| 2026-08-11 | [gRPC-клиент SpeechKit: когда закрытие вообще может отказать](grpc-client-close.md) | Ленивое соединение и два исхода `Close` в grpc v1.74.2 |
|
||||
| 2026-08-11 | [Фреймворк приложения: Svelte, Vue и React на одном экране](spa-framework.md) | Размер собранной статики, цена шага сборки, что у трёх кандидатов одинаково |
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Разбор 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`; при
|
||||
подъёме версии библиотеки их отказ читается как сигнал перечитать эту записку, а
|
||||
не как случайный шум.
|
||||
Reference in New Issue
Block a user