Skip to content

Preserve undecodable archives instead of aborting the read - #72

Merged
psobot merged 3 commits into
masterfrom
psobot/skip-unknown-messages
Aug 7, 2026
Merged

Preserve undecodable archives instead of aborting the read#72
psobot merged 3 commits into
masterfrom
psobot/skip-unknown-messages

Conversation

@psobot

@psobot psobot commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Problem

A single archive we can't decode killed the entire document. IWAArchiveSegment.from_buffer raised NotImplementedError, and IWAFile.from_buffer wraps that in a per-file ValueError, so ls, cat and unpack all aborted — usually at Index/CalculationEngine.iwa, which is read long before any slide content.

That's the second half of what #70 asks for, and it's the whole of #60 ("parsing stopped for the first non-extractable item"). It matters independently of #71: Apple adds message types between Keynote releases, so there will always be a next unknown type.

Fix

Fall back to an UnknownArchive holding the raw payload, and warn once per (file, message type) pair.

  • message_info.length delimits each archive independently, so an undecodable archive never desynchronises the ones after it. The payload slice is now taken before the type lookup, so the cursor advances either way.
  • UnknownArchive.SerializeToString() returns the original bytes, so pack/unpack stays lossless for data we can't interpret.
  • It survives the YAML form via a base64 field, so unpack → edit → pack round-trips too.
  • The same fallback covers a mapped type whose payload fails to parse — the other way one bad archive could take down a document.

Callers who'd rather fail loudly keep that option:

warnings.simplefilter("error", codec.UnknownArchiveWarning)

Verification

Simulated #70 by removing a type that table.key actually uses, then unpacking:

Warnings emitted: 1
  - Don't know how to parse Protobuf message type 6001; preserving it verbatim...
Unpack COMPLETED: 47 files extracted

Before this change that aborted with zero usable output.

tests/test_unknown_archives.py covers: warn-not-raise, byte-for-byte payload preservation, YAML round-trip, the undecodable-payload path, promoting the warning to an error, and once-per-type deduplication. Full suite: 26 passed.

Fixes #60.

Apple adds Protobuf message types between Keynote releases, so a document
written by a newer Keynote than we have protos for will contain archives we
can't decode. Until now the first such archive raised, and because
IWAFile.from_buffer wraps that in a per-file ValueError, `ls`, `cat` and
`unpack` died on the whole document - typically at
Index/CalculationEngine.iwa, long before reaching any slide content.

Fall back to an UnknownArchive holding the raw payload, and warn once per
(file, message type). message_info.length delimits each archive
independently, so an undecodable one never desynchronises those that follow.

UnknownArchive writes its bytes back verbatim, so pack/unpack stays lossless
for data we can't interpret; it survives the YAML representation via a
base64 field. Callers who would rather fail loudly can promote the warning:

    warnings.simplefilter("error", codec.UnknownArchiveWarning)

The same fallback covers a mapped type whose payload won't parse, which is
the other way a single bad archive could take down an entire document.

Fixes #60.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179M4xvAKPGrgKpy4AeCsM7
For a should_merge patch, `klass` is a functools.partial whose repr embeds the
whole message_info, so the warning ran to several lines of Protobuf dump:

    Failed to deserialize functools.partial(<bound method ...>, type: 0
    version: 65535
    ...

Name the message type instead, defensively enough to survive a class without a
DESCRIPTOR:

    Failed to deserialize patch to KN.BuildArchive of length 17
    (KeyError: 10000); preserving it verbatim. (in Index/Chart.iwa)

This is the exact failure reported in #54, where a chart's diff_field_path
points at a field number the base message's descriptor doesn't carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179M4xvAKPGrgKpy4AeCsM7
@psobot
psobot merged commit 0d83f6b into master Aug 7, 2026
4 checks passed
@psobot
psobot deleted the psobot/skip-unknown-messages branch August 7, 2026 15:28
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.

Parsing stopped for the first non-extractable item

1 participant