Files
transcriber/docs/research/toml-decode-errors.md
T
av cd57b68215 config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет,
  файл без него негоден; bot_token стал только ключом доступа и при
  enabled = false не читается вовсе, а пустой при enabled = true роняет старт
- выключенный вход даёт подъём одним входом без единого обращения к Telegram и
  записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение
- отказ разбора файла настроек больше не пересказывает toml — её ParseError
  несёт в тексте разбираемое значение, и оборванная строка секретного ключа
  уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
2026-08-13 21:46:14 +03:00

4.4 KiB
Raw Blame History

Разбор 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; при подъёме версии библиотеки их отказ читается как сигнал перечитать эту записку, а не как случайный шум.