Skip to content

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

Description

@kostyafarber

Follow-up to PR #117 and the format-interop review. Principle (the Fontra lesson, adapted): model what you model, preserve what you don't — a UFO placed in the middle of a pipeline must come back with everything Shift doesn't model intact.

Good news from the audit: the preservation architecture already exists end-to-end. LibData/LibValue in shift-font (including a Data bytes variant), reader conversion for font/glyph/layer libs and groups (ufo/reader.rs), writer conversion back to plists (ufo/writer.rs:130-153), and .shift persistence via modules/shift.libData.json (shift-source/src/package.rs:26,467). The remaining holes are bounded:

1. plist::Value::Date and Uid are destroyed (bug)

ufo/reader.rs:99 catch-all maps them to LibValue::String(String::new()) — empty string in, empty string round-tripped out. Dates are common in real-world libs (tool timestamps). Fix: add LibValue::Date (and either a Uid variant or a tagged encoding), extend the writer conversion and the .shift LibValueDoc accordingly. Also: Integer(i).as_signed().unwrap_or(0) at :85 silently zeroes u64 values above i64::MAX (edge case, same fix pass).

2. UFO data/ and images/ directories are silently dropped

Arbitrary binary payloads (source images, tool caches, foundry assets) never enter the IR. norad exposes them via DataRequest. Fix: carry them as opaque binary entries in the IR and .shift package (they're zip entries — natural fit), write back verbatim.

3. layerinfo.plist (per-layer color, lib) — verify and carry

Not obviously read today. Layer colors are widely used (background/marking workflows).

4. Unmapped fontinfo fields — the big one, needs a decision

Reader/writer copy a hand-picked fontinfo field list (names, versions, metrics). UFO fontinfo has ~100 fields — postscript blue values, OS/2 fields, font-level guidelines, WOFF metadata — all silently stripped on round-trip. Two strategies:

  • (a) Expand the mapped field set to full norad FontInfo coverage — mechanical but long, and every field becomes modeled (editable, interpolatable where relevant).
  • (b) Passthrough map — carry unmapped fontinfo as an opaque preserved blob alongside the modeled fields.
    (a) is more honest long-term (fontinfo is font data, not tool data); (b) ships the safety guarantee immediately. Could do (b) now and burn it down with (a) over time.

Acceptance

A pinned round-trip test: open a UFO fixture stuffed with every plist type, data/images payloads, layerinfo, and exotic fontinfo fields → save → byte-meaningful equality on everything not deliberately modeled-and-edited. This test is the preservation contract; fidelity regressions are silent by nature and only a pinned fixture makes them loud.

Items 1–3 are mechanical and agent-sized. Item 4(a) vs 4(b) is the one real design decision.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions