Skip to content

format: text and Markdown files can be written in UTF-16 - #83

Merged
donislawdev merged 2 commits into
mainfrom
format/text-encoding
Sep 7, 2026
Merged

format: text and Markdown files can be written in UTF-16#83
donislawdev merged 2 commits into
mainfrom
format/text-encoding

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

txt and md gain two settings, shared in internal/format/textenc: encoding
(utf-8, utf-16le, utf-16be) and bom. Both default to what these formats
already wrote, so a recipe that says nothing gets the same bytes it got before -
pinned by five new golden values, with every one of the 57 existing hashes
untouched.

tfg generate --format txt --size 4kb --set encoding=utf-16le --set bom=true

An odd size is refused, not rounded

Every character takes two bytes in UTF-16, so only an even number of bytes can be
a whole file. Measured on three readers, in three languages, on the same file cut
to an odd length:

reader whole file cut by one byte
Python 3.14.7, strict decode ok rejected
Node v26.8.1, TextDecoder(fatal:true) ok rejected
.NET UnicodeEncoding(throwOnInvalidBytes) ok rejected
all three, lenient no replacement characters one, silently
Get-Content 2 lines 2 lines, looks identical

The last row is the one that decided it: the cut file looks exactly like the
whole one. So half of all sizes become unreachable and the refusal names the
size either side of the one asked for. The registry mechanism for that already
existed - SmallestAccepted walks to a fixed point and already handled PNG
refusing a band above a size it accepts.

Single byte encodings are deliberately absent

The filler vocabulary is ASCII, so a file written as latin-1 is byte for byte the
same file as UTF-8 - compared with cmp. That would be a setting that changes
nothing. They belong with content that is not English, which is a separate change
with its own golden bytes.

Two things this turned up that were not the job

  • TestEveryFormatSurvivesItsReferenceTool skipped the structural check for any
    format declaring no reference tool, so the new checkers would have been dead on
    arrival - with the guard green and reporting both formats as covered. It was
    invisible while the only formats without a reader were also the only ones
    without a checker.
  • A strict decode alone blesses a file with its byte order reversed, because
    0x74 0x00 read the other way is a valid CJK character. What catches it is
    asking what the characters are, and only txt was asking.

txt and md also gain the structural check they never had. The comment saying
they had nothing to check against stopped being true rather than being wrong, and
was rewritten with the reason rather than deleted. The checker is told the
encoding rather than sniffing it, for the reason the CSV dialect is told.

Verification

  • whole suite green, gofmt, vet, preflight --quick all 12 checks
  • nine mutations, all caught
  • 285 of 285 checks on the built binary, 41 of them new
  • 48 generated files decoded strictly by an independent decoder

🤖 Generated with Claude Code

donislawdev and others added 2 commits September 7, 2026 20:22
TXT and MD gain two settings, shared in internal/format/textenc so the two
cannot describe the same thing differently: encoding (utf-8, utf-16le,
utf-16be) and bom. Both default to what these formats already wrote, and five
new golden values pin the encoded paths.

An odd number of bytes is refused in UTF-16 rather than rounded. A UTF-16 file
cut to an odd length is rejected by Python, by V8 and by .NET when each is
asked strictly, and repaired in silence by all three when it is not - so the
file this tool must never write is the one nobody could tell apart by looking.
Half of all sizes become unreachable and the refusal names the reachable size
either side of the one asked for.

The registry mechanism for that already existed: SmallestAccepted walks to a
fixed point and already handled PNG refusing a band above a size it accepts.

Single byte encodings are deliberately not offered. The filler vocabulary is
ASCII, so latin-1 output is byte for byte identical to UTF-8 - a setting that
would change nothing. They belong with content that is not English.

TXT and MD also gain the structural check they never had. The comment saying
they had nothing to be checked against stopped being true rather than being
wrong: a file claiming to be UTF-16LE is a claim somebody else's decoder can
settle. The checker is told the encoding rather than sniffing it, for the
reason the CSV dialect is told.

Two things this found that were not the job:

  - TestEveryFormatSurvivesItsReferenceTool skipped the structural check for
    any format with no reference tool, so the new checkers would have been
    dead on arrival with the guard green and reporting them as covered. It was
    invisible while the only formats without a reader were also the only ones
    without a checker.

  - a strict decode alone blesses a file with its byte order reversed, because
    0x74 0x00 read the other way is a valid CJK character. What catches it is
    asking what the characters are, and only TXT was asking.

Nine mutations, all caught. TestEveryTextFormatIsValidUTF8 keeps its name and
says out loud that its claim is the default path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Width had no caller outside textenc. A fresh export with nobody asking for it
adds to a category this tree already counts, so it goes in as unexported and
the mutation patterns naming it follow the rename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit be665f5 into main Sep 7, 2026
18 checks passed
@donislawdev
donislawdev deleted the format/text-encoding branch September 7, 2026 18:40
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.

1 participant