This plan was prepared on 2026-05-13 after checking the local repository,
/home/eldryoth/Work/codex-projects/fluxheim, and current upstream package
metadata.
Current baseline:
- Rust stable: 1.95.0.
- Crate license:
MIT OR Apache-2.0. - Core dependencies:
quick-xml 0.40.1,zip 8.6.0,indexmap 2.14.0,thiserror 2.0.18,serde 1.0.228,base64-ng 1.0.0. - Test dependencies:
assert_cmd 2.2.2,tempfile 3.27.0. - Security tools:
cargo-deny 0.19.6,cargo-audit,cargo-license,cargo-sbom.
Adobe currently documents IDML as an InDesign export format and exposes
generateIDMLSchema for generating an IDML schema from the installed
application. The old PDF specification is useful historical context, but the
implementation must validate against generated schemas and real exported IDML
fixtures instead of trusting only the old PDF.
Threat assumptions:
- IDML files are untrusted input.
- ZIP entries may attempt path traversal, duplicate names, decompression bombs, extreme compression ratios, oversized entry names, unsupported compression methods, or malformed metadata.
- XML may contain oversized attributes, malformed namespaces, invalid UTF-8, entity tricks, malicious ID references, and resource exhaustion payloads.
- Embedded base64 assets may be non-canonical, oversized, padded incorrectly, or intentionally ambiguous for cache/key confusion.
Hard rules:
#![forbid(unsafe_code)]for the crate.- No network access while parsing or writing IDML.
- No filesystem writes during read-only parsing.
- No extraction APIs that write archive paths directly to disk.
- All archive paths are normalized as logical ZIP paths, never host paths.
- Archive paths reject traversal, host path syntax, directory entries, and control characters.
- Archive entry paths are capped by configured UTF-8 byte length before logical path validation or writer emission.
- DesignMap package
srcattributes are capped by configured UTF-8 byte length before they are accepted as logical archive paths. - DesignMap package
idPkg:*element names are capped by raw byte length before they are preserved or emitted. - Archive inventory rejects encrypted entries and non-regular file metadata such as symlinks.
- Compression-ratio diagnostics use division/remainder ceiling arithmetic instead of addition-based formulas that can overflow at ZIP metadata limits.
- Count-limit diagnostics use checked increments instead of saturating arithmetic, so overflow attempts report explicit limit failures.
- Writer and bounded-read byte counts use explicit checked
usizetou64conversions instead of sentinel fallbacks. - Direct XML parser entry points enforce configured XML byte limits before event parsing.
- Archive-backed XML readers enforce configured parser byte limits against ZIP metadata before allocating or decompressing entry bodies.
- DesignMap writer output enforces default layer and package reference count ceilings before XML emission.
- Spread writer output enforces default page, text frame, and recursive page item count ceilings before XML emission.
- Story writer output enforces default text and attribute byte ceilings before XML emission.
- Resource inventory construction uses checked reference counts and enforces the default package-reference ceiling before preallocation or collection growth.
- Lazy DesignMap package-reference initialization uses checked sizing and enforces the default package-reference ceiling before allocation.
- Document generator APIs check package-reference capacity before mutating DesignMap maps, ordered package references, or model storage.
- DesignMap resource pointer iteration is lazy and does not allocate a temporary vector proportional to resource package-reference count.
- Document preserved-entry iteration follows ordered package references when present and stays lazy for both parsed and manually constructed manifests.
- Duplicate XML attributes are rejected instead of being silently overwritten or interpreted by first-match behavior.
- Decoded XML attribute values captured into the model are validated against the crate XML character policy before they can become IDs, references, or resource metadata.
- DesignMap XML attributes are capped by configured raw byte length before normalization or decoding can allocate their values.
- Story XML attributes are capped by configured raw byte length before normalization or decoding can allocate their values.
- Spread XML attributes are capped by configured raw byte length before normalization or decoding can allocate their values.
- Resource item XML attributes are capped by configured raw byte length before normalization or decoding can allocate their values.
- Resource item XML attribute names are capped by configured raw byte length before allocating captured names.
- Resource item XML attribute count limits are enforced before decoding attribute values.
- Semantically equivalent XML start and empty events are handled consistently where IDML allows self-closing elements, avoiding first/empty event parser confusion.
- Top-level XML parsers reject missing, wrong, or repeated root elements before interpreting recognized child tags as a valid model.
- Resource XML extractors reject missing roots, repeated roots, and non-whitespace data outside the document root.
- Story
Contentpayloads accept only text, CDATA, and explicit XML entity references; child markup is rejected instead of being folded into text. - Embedded asset
Contentspayloads accept only text and CDATA chunks; child markup and entity references are rejected before Base64 decoding. - Embedded asset count limits are enforced before decoding each accepted
Contentspayload. - Non-whitespace character data, CDATA, and entity references outside the root element are rejected instead of being ignored.
- XML entity references are accepted only in explicit story text
Contentor decoded attribute values; structural element bodies reject them. - EOF before closing the root or a nested Group is rejected as malformed XML state instead of producing a partial model.
- XML root and depth trackers use checked close-state transitions instead of saturating counters, so unexpected closing events cannot be normalized away.
- Story
Contentclose-state tracking uses checked transitions instead of saturating counters. - Resource XML extractors finalize depth tracking and reject EOF inside open
elements, including partial embedded
Contentspayloads. - XML readers are centrally configured to keep end-tag name validation enabled and reject malformed comments instead of silently skipping invalid XML.
DOCTYPEdeclarations are rejected by IDML XML parsers to keep DTD/entity processing out of the trusted parse surface.- All parsing APIs accept explicit size limits.
- All public error types are typed, non-panicking, and non-exhaustive.
- No
unwrap,expect, or panics in parser, resolver, writer, or CLI paths.
Primary modules:
archive: ZIP reader/writer, path policy, compression policy, mimetype placement, entry inventory.model: typed structures forDesignMap,Spread,Story, and resources.core::resolver: lazy ID-to-entry resolution and relational integrity.core::units: f64 point/mm/in conversions with round-trip tests.encoding: base64 engines for strict modern and legacy-compatible decoding.traits:XmlLoadable,XmlSaveable, and validation traits.validate: cross-file integrity checks and writer preflight.
Parsing flow:
- Open archive through
IdmlPackage. - Inventory entries with strict path normalization, configured entry/path byte limits, and per-entry compression-ratio limits.
- Parse
designmap.xmlfirst. - Build ordered indexes with
IndexMap. - Return lazy pointers for spreads, stories, and resources.
- Resolve on demand with lifetime-aware borrowed buffers where possible.
- Validate before serialization.
- Write a new archive with
mimetypefirst and stored, then XML/resources.
Use published base64-ng 1.0.0 through explicit engines only. Do not use
deprecated global helpers or implicit configs. Do not inspect or depend on a
local sibling checkout for base64-ng; published indesign-idml releases must
use the crates.io version only.
Modern default:
- RFC 4648 standard alphabet.
- Canonical padding required unless the IDML field explicitly requires no pad.
- Reject non-zero trailing bits.
- Reject whitespace and non-base64 bytes.
- Decode into caller-provided buffers when possible.
- Enforce decoded size limits before allocation.
Legacy compatibility:
- Exposed only through a named
LegacyBase64mode. - Allows documented legacy padding and whitespace behavior only when requested.
- Returns diagnostics that identify the compatibility relaxation used.
- Never silently canonicalizes for equality or cache keys.
- Preserve namespaces and original qualified names unless the writer owns the whole element.
- Treat self-closing package references as XML empty events.
- Preserve order for spreads, layers, page items, and resources.
- Model
Self,ParentStory, and packagesrcreferences as typed IDs. - Validate that every
ParentStoryresolves to a story entry. - Validate that every designmap entry points to an existing archive entry.
- Keep unknown XML attributes/elements in lossless extension fields for writer round trips.
- Use
f64for geometry and deterministic formatting on write.
The local gate mirrors Fluxheim's style, adapted for a Rust library:
cargo fmt --all --checkcargo clippy --all-targets --all-features -- -D warnings- default, no-default,
std, andserdefeature tests cargo doc --no-deps --all-features- release metadata validation
cargo deny checkcargo auditcargo license- SBOM generation
- reproducible release build check
- optional
idml-textCLI release artifact and checksum build - libFuzzer harness placeholders for parser targets
Test categories to add as implementation grows:
- Unit tests for XML event handling, attributes, IDs, and unit conversions.
- Golden fixtures exported from current InDesign and older IDML versions.
- Malicious ZIP fixture tests for traversal, absolute paths, oversized names, duplicates, bombs, unsupported compression, and invalid mimetype placement.
- Malicious XML tests for huge tokens, malformed namespaces, broken encodings, invalid references, and text extraction corner cases.
- Property tests for unit conversion and base64 round trips.
- Fuzz targets for designmap, story, spread, archive inventory, and base64.
- Round-trip writer tests compared with schema generation and InDesign-opened fixtures when available.
- Keep the crate compiling under Rust 1.95.0.
- Implement
IdmlError, size limit config, archive inventory, and path policy. - Implement strict dependency policy and local gate.
- Add first malicious archive fixtures.
Exit criteria: all local checks pass and unsafe code remains forbidden.
- Implement event-based
DesignMapparsing withquick-xml. - Capture ordered
Spread,Story,MasterSpread, and resource references. - Preserve unknown package references.
- Validate missing
src, malformed IDs, duplicates, and missing archive entries.
Exit criteria: designmap golden tests, malformed XML tests, and fuzz target pass.
- Implement story parsing for visible text extraction.
- Stream large stories without collecting all story XML in one string.
- Add CLI binary behind the
clifeature. - Publish the library crate to crates.io and attach prebuilt companion CLI binaries to GitHub Releases.
- Decode XML entities correctly and keep text order stable.
Exit criteria: extract text from representative IDML fixtures under memory limits.
- Implement spread parsing, text frame geometry,
ParentStorylinks, and unit conversion. - Add resolver joins from spread frames to story content.
- Add bounding-box queries in points, mm, and inches.
Exit criteria: location-based text queries work against golden fixtures.
- Implement loss-aware XML writing.
- Preserve namespaces, unknown fields, and stable ordering.
- Write
mimetypefirst and uncompressed. - Validate relational integrity before writing.
Exit criteria: generated IDML opens in InDesign and passes schema validation.
- Expand fuzz corpus and run long fuzz campaigns before release.
- Generate SBOM and license reports.
- Run
cargo audit,cargo deny,cargo license, and reproducibility checks. - Tag only after clean local and CI gates.
Exit criteria: release candidate has zero known advisories, approved licenses, documented fixtures, and reproducible artifacts.
Every implementation step should end with:
sh scripts/checks.shgit status --shortgit add ...git commit
Push remains manual.