Skip to content

Latest commit

 

History

History
291 lines (240 loc) · 12.9 KB

File metadata and controls

291 lines (240 loc) · 12.9 KB

indesign-idml Secure Implementation Plan

Verified Baseline

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.

Security Model

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 src attributes 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 usize to u64 conversions 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 Content payloads accept only text, CDATA, and explicit XML entity references; child markup is rejected instead of being folded into text.
  • Embedded asset Contents payloads 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 Contents payload.
  • 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 Content or 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 Content close-state tracking uses checked transitions instead of saturating counters.
  • Resource XML extractors finalize depth tracking and reject EOF inside open elements, including partial embedded Contents payloads.
  • XML readers are centrally configured to keep end-tag name validation enabled and reject malformed comments instead of silently skipping invalid XML.
  • DOCTYPE declarations 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.

Architecture

Primary modules:

  • archive: ZIP reader/writer, path policy, compression policy, mimetype placement, entry inventory.
  • model: typed structures for DesignMap, 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:

  1. Open archive through IdmlPackage.
  2. Inventory entries with strict path normalization, configured entry/path byte limits, and per-entry compression-ratio limits.
  3. Parse designmap.xml first.
  4. Build ordered indexes with IndexMap.
  5. Return lazy pointers for spreads, stories, and resources.
  6. Resolve on demand with lifetime-aware borrowed buffers where possible.
  7. Validate before serialization.
  8. Write a new archive with mimetype first and stored, then XML/resources.

Base64 Policy

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 LegacyBase64 mode.
  • 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.

IDML-Specific Correctness

  • 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 package src references as typed IDs.
  • Validate that every ParentStory resolves 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 f64 for geometry and deterministic formatting on write.

Testing System

The local gate mirrors Fluxheim's style, adapted for a Rust library:

  • cargo fmt --all --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • default, no-default, std, and serde feature tests
  • cargo doc --no-deps --all-features
  • release metadata validation
  • cargo deny check
  • cargo audit
  • cargo license
  • SBOM generation
  • reproducible release build check
  • optional idml-text CLI 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.

Milestones

1. Secure Foundation

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

2. DesignMap Reader

  • Implement event-based DesignMap parsing with quick-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.

3. Text Miner

  • Implement story parsing for visible text extraction.
  • Stream large stories without collecting all story XML in one string.
  • Add CLI binary behind the cli feature.
  • 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.

4. Layout Navigator

  • Implement spread parsing, text frame geometry, ParentStory links, 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.

5. Writer

  • Implement loss-aware XML writing.
  • Preserve namespaces, unknown fields, and stable ordering.
  • Write mimetype first and uncompressed.
  • Validate relational integrity before writing.

Exit criteria: generated IDML opens in InDesign and passes schema validation.

6. Hardened Release

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

Commit Policy

Every implementation step should end with:

  1. sh scripts/checks.sh
  2. git status --short
  3. git add ...
  4. git commit

Push remains manual.