Repository navigation
A form can be written, not only read - #19
Merged
Merged
Conversation
This is the writing half of the package: FieldType, Flags, Dict, Check and CheckAll say what a field has to be in the file for Read to read back the field that was asked for. It is what BentoPDF's form-creator needs, and the fleet had every other piece of it already. It shares the reader's flag constants rather than restating them. The bits are the one thing a second copy gets subtly wrong -- bit 22 and bit 25 are the two the tables make easy to miscount -- and a writer that disagreed with the reader would produce a form this package itself reads as something else, which no test of either half alone could see. A flag that does not belong to the kind is dropped rather than written. Multiline is bit 13 of a text field and NoToggleToOff is bit 15 of a button, so writing the caller's flags blindly turns one reused struct into a checkbox that cannot be unticked. Check refuses what makes a file that OPENS and is broken: a field with no name, a choice field with no rows, a checkbox whose on-state is the reserved name Off, two widgets of one radio group in the same state, a comb field with no MaxLen, an empty rectangle, a page before the first. CheckAll adds the one rule about the set: two fields of one name, which the reader keys by name, so the second silently shadows the first. Fuzzing the round trip found a real defect in the first run. decodeText reads a string with no mark at the front as PDFDocEncoding -- one rune per BYTE -- so writing Go's UTF-8 bytes turned "cafe" with an acute accent into mojibake, in a file that opens and looks well formed. Every accented or non-Latin field name in the fleet would have had the same fate. Text is now written as UTF-16 behind the byte-order mark when it is not ASCII, and text that is not valid UTF-8 is refused, since it cannot be stored under the name it was given. The oracle throughout is the reading half, which has a 69-form corpus behind it, rather than this half's own expectations. Measured: 100% of statements and 0 uncovered blocks, counted on the profile rather than the rounded percentage. Three mutations caught (a combo box written as a text field; MultiSelect written as bit 25 instead of bit 22; Required dropped). 4.5 million fuzz executions after the fix, clean. govulncheck reports 4 standard-library findings, all of them net/http fixed in go1.27.2 against a local toolchain of go1.27.1; CI builds with 1.27.2, so the published artefact does not carry them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The writing half of the package:
FieldType,Flags,Dict,CheckandCheckAllsay what a field has to be in the file forReadto read back the field that was asked for.This is what BentoPDF's
form-creatorneeds. The fleet had every other piece already —opsfills forms,formsdraws appearances,renderdraws widgets — and only the direction that creates a field was missing.One definition of each flag
It shares the reader's constants rather than restating them. The bits are the one thing a second copy gets subtly wrong — bit 22 (
MultiSelect) and bit 25 (Comb) are the two the tables make easy to miscount — and a writer that disagreed with the reader would produce a form this package itself reads as something else, which no test of either half alone could see.A flag that does not belong to the kind is dropped, not written.
Multilineis bit 13 of a text field andNoToggleToOffis bit 15 of a button, so writing the caller's flags blindly turns one reused struct into a checkbox that cannot be unticked.What
CheckrefusesEach of these makes a file that opens and is broken, which is the worst outcome available:
Off, the name the format reserves for not chosen;MaxLen— a comb field isMaxLencells;CheckAlladds the one rule about the set: two fields of one name. The reader keys fields by name, so the second shadows the first — the document carries both widgets, the form offers one field, and whoever fills it in cannot see that half of what they typed went nowhere.Fuzzing found a real defect in its first run
decodeTextreads a string with no mark at the front as PDFDocEncoding — one rune per byte. So writing Go's UTF-8 bytes turnedcaféintocafé, in a file that opens and looks perfectly well formed. Every accented or non-Latin field name in the fleet would have had the same fate.Non-ASCII text is now written as UTF-16 behind the byte-order mark, which is what the reader decodes. Text that is not valid UTF-8 is refused, because it cannot be stored and read back under the name it was given — refusing is honest, writing something that silently differs is not.
Controls
The oracle throughout is the reading half, which has a 69-form, 13 854-field corpus behind it — not this half's own expectations. A field written as something the format spells differently comes back as the wrong kind rather than as a passing test.
-funcroundsMultiSelectwritten as bit 25 instead of bit 22;Requireddroppedgovulncheckreports 4 standard-library findings, allnet/httpfixed in go1.27.2, against a local toolchain of go1.27.1. CI builds with 1.27.2, so the published artefact does not carry them — the finding is the toolchain the scan ran under, not this code.Next
opsgainsAddFieldsandpdfopsa verb, which is where the widget references and page links get allocated; thenform-creatorcan be claimed in the parity table.🤖 Generated with Claude Code