Skip to content

feat: UFO round-trip preservation contract#119

Merged
kostyafarber merged 5 commits into
mainfrom
fix/ufo-preservation
Jul 2, 2026
Merged

feat: UFO round-trip preservation contract#119
kostyafarber merged 5 commits into
mainfrom
fix/ufo-preservation

Conversation

@kostyafarber

Copy link
Copy Markdown
Collaborator

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

  • Lib plist fidelityDate (RFC 3339 via plist::Date::to_xml_format), Uid, and unsigned integers above i64::MAX get real LibValue variants instead of becoming empty strings / zero. LibValue serde moved from untagged to tagged — untagged couldn't distinguish Date from String and already conflated Data with Array.
  • UFO data/ and images/ 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 on Source and persist through .shift and the store.
  • fontinfo passthrough (decision (b) on UFO preservation contract: close the remaining round-trip data-loss holes #118) — all unmapped fontinfo fields (postscript blues/stem snaps, OS/2 weight/width/fsType/panose/unicodeRanges, WOFF metadata, …) are carried as a neutral plist-dict remainder and written back verbatim. One owner per key is enforced structurally: modeled IR fields overwrite the remainder on save, pinned by a dedicated stale-shadow test. Export fidelity improves for free — the fontc path consumes the written UFO, so preserved hinting/OS2 data now reaches compiled binaries.
  • Guideline name/color were silently dropped despite being modeled — fixed.

Behavior notes

  • Empty styleMapFamilyName is treated as unset (matching Python tooling); carrying it verbatim made fontc emit an empty family name. Non-empty values are preserved, and carrying styleMapStyleName now yields the same exported subfamily fontmake produces.
  • Known remaining gap: guideline identifier/lib (no IR fields yet).

Acceptance tests

preserves_libs_binaries_layerinfo_and_fontinfo_through_round_trip builds a fixture with every plist type, nested data/ 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_save pins the one-owner rule. .shift round-trips covered in package_test.rs including Uid.

cargo test --workspace: 303 passed, 0 failed. fmt clean, clippy zero warnings.

🤖 Generated with Claude Code

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.
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.

UFO preservation contract: close the remaining round-trip data-loss holes

1 participant