feat(iwork): elementary support for apple iwork, starting with .pages - #732
Open
andiwand wants to merge 7 commits into
Open
feat(iwork): elementary support for apple iwork, starting with .pages#732andiwand wants to merge 7 commits into
andiwand wants to merge 7 commits into
Conversation
The framing an iWork package uses is Apple's own — a four-byte header per block, `0x00` and a little-endian 24-bit compressed length — so stock Snappy stream decoding does not apply and only the block decoder does. That is a varint length plus literal and copy tags, which is less code than a dependency would be and keeps it out of the wasm, android and apple builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
An iWork archive is protobuf, but Apple has never published the `.proto` schemas, so there is nothing for a code generator to generate and linking conan `protobuf` would drag it into every downstream build to replace this. Only the wire format is needed: varints, the three fixed and length-delimited forms, and unknown fields carried along rather than dropped. A group means the parse went wrong, so it throws. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
An `.iwa` is not a tree but a sequence of archived objects, each a `TSP.ArchiveInfo` naming an identifier and the type of the messages that follow. Objects reference each other by identifier across components, so the package reads its component list from `Index/Metadata.iwa` first — the file names carry identifier suffixes often enough that globbing for them finds nothing — and decompresses a component when something in it is asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
`.pages`, `.numbers` and `.key` are zips, so today they are reported as `[zip]` and open as an archive rather than a document. Naming them gives a caller the extensions and MIME types to route one and hand a file picker, which is what has to be decided before the file is held. Classification only for now: which app wrote a package is read off its root archive, and only `.pages` has a fixture to pin that against, so the rows declare no capabilities and nothing detects or decodes one yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
A `.pages` package now opens as a text document rather than as the zip it is made of, and its body comes out as paragraphs. Which app wrote the package is read off the type of the root archive in `Index/Document.iwa` — the extension is not consulted, since a caller may not have one. Paragraph boundaries come from the storage's paragraph run table rather than from splitting the text on `\n`, and `U+2028` inside a paragraph becomes a line break. The anchor a drawable leaves in the text is dropped: styles, page geometry, drawables, images and tables are all still to come, so this is the text and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
`PLAN.md` was written before any of it existed; this is the module's `AGENTS.md` alongside it — why a fixture is the citation here rather than a spec section, why snappy and protobuf are in-tree, the `Message` lifetime the whole engine rests on, and where the run tables sit. `PLAN.md` marks the two landed stages and the three places the plan and the code disagreed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADNSpk1CY88GMpqkafN6z
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.
🤖 Generated with Claude Code
Stages 1 and 2 of
src/odr/internal/iwork/PLAN.md: the container comes apart, and a.pagesopens as a text document and renders its body instead of coming back as the zip it is made of.What lands
iwork_snappy— Apple's own.iwaframing (0x00plus a little-endian 24-bit compressed length, repeated to EOF) plus Snappy block decompression. Stock Snappy stream framing is not present, so a conan dependency would be the wrong shape as well as an extra one in the wasm/android/apple builds.iwork_protobuf— the wire format only. Apple has never published the.protoschemas, so a code generator would have nothing to generate; archives are read against hand-written accessors by field number.iwork_archive— an.iwais an object graph, not a tree. The component list comes fromIndex/Metadata.iwa(file names carry identifier suffixes, so globbing for them does not work), and objects resolve by identifier across components.FileTypes —iwork_pages,iwork_numbers,iwork_keynote, with rows infile_type_table.cppand the three bindings that mirror the enum by ordinal. Only.pagesdetects and opens; the other two are classification-only, because which app wrote a package is read off its root archive type and neither has a fixture to pin that against.\n,U+2028as a line break, and the run tables' UTF-16 indices translated against the UTF-8 text.What does not
Styles, page geometry, drawables, images, tables, Numbers, Keynote, encryption — stages 3 to 7 of the plan.
src/odr/internal/iwork/AGENTS.mdrecords what the two landed stages decided and why;PLAN.mdmarks them and the three places plan and code disagreed.Testing
The container readers are covered by inline byte strings — hand-built Snappy blocks, varint edge cases, an unknown archive type that must be skipped rather than thrown on, truncated framing. The two committed
.pagesfixtures cover detection, the empty-body regression, the body text, and cross-component object resolution.Full suite green; the only new reference output is
output/pages/, pinned intest/data.cmake.