Skip to content

fix: TextFiles/FileStringPersister read/write UTF-8 explicitly, not the locale - #106

Merged
thorwhalen merged 1 commit into
masterfrom
fix-97-textfiles-utf8-encoding
Sep 22, 2026
Merged

thorwhalen merged 1 commit into
masterfrom
fix-97-textfiles-utf8-encoding

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Summary

FileStringReader/FileStringPersister opened text files with encoding=None, which Python resolves to locale.getpreferredencoding() at call time -- so the bytes a store writes/reads depend on the process's environment ($LANG etc). Consequences, per #97: a write of any non-ASCII character can raise on an ASCII-resolving locale (a container or cron/systemd job with LANG unset, an Alpine/musl image); a round trip under different locales can silently corrupt; and a store synced between machines with different locales can become unreadable to the machine that didn't write it.

Defaults encoding to "utf-8" explicitly -- the exact fix suggested by the issue's author (the repo maintainer) in the issue body. A store is a serialization boundary; one whose format depends on an ambient environment variable isn't really specified.

Also adds a docstring note to FileBytesPersister.__init__ about the delete_func default (OS trash) being a surprising default for a store of derived/generated data, per a smaller separate observation in the same issue -- docs only, no behavior change.

Closes #97

Review

Independent sub-agent review (per crowsnest policy) confirmed:

  • The fix applies end-to-end without dropping other inherited open() kwargs (buffering, errors, newline, closefd, opener).
  • The pre-fix failure mode reproduces under a true ASCII locale (PYTHONUTF8=0 PYTHONCOERCECLOCALE=0, since most tested LANG/LC_ALL values are coerced to UTF-8 by PEP 538/540 regardless of what they say) -- UnicodeEncodeError writing non-ASCII text.
  • Reading a pre-existing non-UTF-8 file (e.g. written as latin-1) now fails cleanly with UnicodeDecodeError rather than silently -- an accepted, informative tradeoff.
  • No sibling text-mode class in filesys.py was left with the same bug (only FileStringReader/FileStringPersister open in text mode; FileBytesReader/FileBytesPersister are binary and ignore encoding).

Dependents

~23 on-box fleet packages import TextFiles/FileStringPersister. This box's locale is already UTF-8, so their test suites can't observe a difference (the change is behaviorally identical here); the whole point of the bug is that it's locale-dependent, so no on-box test run can validate the fix's actual purpose beyond "doesn't break anything on a UTF-8 box," which is confirmed. Full dol suite green.

Test plan

  • pytest --doctest-modules dol/filesys.py -> 7 passed
  • pytest (full suite) -> 592 passed, 3 skipped

🤖 Generated with Claude Code

https://claude.ai/code/session_011HSBVhDjRU4apSLcRkavv9

…he locale

FileStringReader/FileStringPersister opened text files with
encoding=None, which Python resolves to locale.getpreferredencoding()
at call time -- so the bytes a store writes/reads depend on the
process's environment ($LANG etc). Consequences: a write of any
non-ASCII character can raise on an ASCII-resolving locale (a
container or cron/systemd job with LANG unset, an Alpine/musl image),
a round trip under different locales can silently corrupt, and a store
synced between machines with different locales can become unreadable
to the machine that didn't write it.

Default encoding to "utf-8" explicitly, matching the exact fix
suggested in the issue by its author. A store is a serialization
boundary; one whose format depends on an ambient environment variable
isn't really specified.

Also adds a docstring note to FileBytesPersister.__init__ about the
delete_func default (OS trash) being a surprising default for a store
of derived/generated data (every deletion leaves a full untracked copy
outside the store, and the trash is keyed by basename so same-named
keys from different stores can collide there) -- docs only, no
behavior change.

Reviewed by an independent sub-agent before merge (per crowsnest
policy): confirmed the fix applies end-to-end without dropping other
inherited open() kwargs, confirmed the pre-fix failure mode
(UnicodeEncodeError under a true ASCII locale, forced via
PYTHONUTF8=0/PYTHONCOERCECLOCALE=0 since most tested locale strings
are coerced to UTF-8 by PEP 538/540 regardless), confirmed reading a
pre-existing non-UTF-8 file now fails cleanly (UnicodeDecodeError, not
silent corruption) rather than silently, and confirmed no sibling
text-mode class in filesys.py was left with the same bug.

Closes #97

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thorwhalen
thorwhalen merged commit 6e71c71 into master Sep 22, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the fix-97-textfiles-utf8-encoding branch September 22, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TextFiles writes in the locale encoding, so a store's bytes depend on $LANG

1 participant