feat: UFO round-trip preservation contract#119
Merged
Conversation
UFO lib values of type date and uid were silently replaced with empty strings, and unsigned integers above i64::MAX were zeroed. LibValue now carries Date (RFC 3339 string, plist XML format), Uid (u64 payload), and UnsignedInteger variants, with tagged self-describing serde, and the UFO reader/writer, .shift package schema, and SQLite store round-trip them.
Shift models only a subset of fontinfo.plist; everything else (postscript hinting, OS/2, WOFF metadata, name records, ...) was dropped on save. The reader now captures unmapped keys as a plist-shaped remainder on Font, and the writer rebuilds fontinfo from it before overwriting every modeled key from the IR, so the IR always owns modeled fields. Font-level guideline names and colors are now carried too. The remainder persists through the .shift package (modules/shift.fontInfo.json) and the SQLite store. An empty styleMapFamilyName is deliberately not carried: the reference Python toolchain treats it as unset, and compilers that consume it verbatim export an empty family name.
This was referenced Jul 2, 2026
This was referenced Jul 2, 2026
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.
Implements the preservation contract from #118: model what you model, preserve what you don't. A UFO opened and saved by Shift now comes back with everything Shift doesn't deliberately edit intact. Closes #118.
What's preserved now
Date(RFC 3339 viaplist::Date::to_xml_format),Uid, and unsigned integers abovei64::MAXget realLibValuevariants instead of becoming empty strings / zero.LibValueserde moved fromuntaggedto tagged — untagged couldn't distinguish Date from String and already conflated Data with Array.data/andimages/directories — carried as opaque binary payloads (shift_font::BinaryData), raw zip entries in.shift(with path-traversal validation on load), a BLOB table in SQLite, and written back through the atomic staged-fsync-swap path.layerinfo.plist— layer color and layer lib now live onSourceand persist through.shiftand the store.Behavior notes
styleMapFamilyNameis treated as unset (matching Python tooling); carrying it verbatim made fontc emit an empty family name. Non-empty values are preserved, and carryingstyleMapStyleNamenow yields the same exported subfamily fontmake produces.identifier/lib(no IR fields yet).Acceptance tests
preserves_libs_binaries_layerinfo_and_fontinfo_through_round_tripbuilds a fixture with every plist type, nesteddata/paths, an image, layer color+lib, exotic fontinfo, and a named+colored guideline → open → save → reopen → exact survival.edited_ir_fields_win_over_preserved_fontinfo_on_savepins the one-owner rule..shiftround-trips covered inpackage_test.rsincludingUid.cargo test --workspace: 303 passed, 0 failed. fmt clean, clippy zero warnings.🤖 Generated with Claude Code