Files
Dylan M. Taylorandskylar taylor-barrick 58f6c4b9e8 craft-idml, craft-epub: read and write through craft-zip (#950)
* craft-zip: ZIP preflight, bounded entry read and Budget (from craft-archive, #129)

The ZIP half of the craft-archive proposal (#126, #129): an allocation-free check of the end
records and central directory that runs before a ZIP reader indexes the archive, a bounded
entry read that distrusts the declared size, and the named Budget both charge. The XML half
of #129 is not carried: craft-xml bounds XML now.

* craft-zip: Archive (entry-count guard, duplicate names, budgeted reads) and Writer

check_entry_count is craft-opc's guard: the declared entry count, ZIP64 included, checked on
the raw bytes before zip parses the central directory. Archive reads entries within a per-entry
limit and a total Budget through read_entry. Duplicate names are found by counting the central
directory headers against the entries zip kept. Writer is craft-opc's ZipOut: stored or deflated
entries, ZIP64 for large ones, one fixed timestamp.

* craft-idml, craft-epub: read and write through craft-zip

Both crates drop their own bounded ZIP reads and zip writers. They gain the entry-count check
before the archive is indexed (ZIP64 included) and archives whose bytes depend only on their
content (one fixed timestamp).

---------

Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-11-11 19:23:15 -05:00
..

craft-idml

IDML (InDesign Markup Language) packages, and InDesign Tagged Text. No unsafe.

An IDML package is a ZIP whose first entry is an uncompressed mimetype, followed by designmap.xml (the root Document element, which includes the other parts) and the Resources/, MasterSpreads/, Spreads/ and Stories/ parts.

Implemented clean-room from the public IDML File Format Specification and from inspecting IDML packages exported from synthetic documents.

This crate is the layer between the bytes and IDML's own element tree. What the elements mean (spreads, page items, stories, styles, swatches) is not modelled here yet: a caller walks the tree and maps it onto its own document.

Reading

# fn main() -> craft_idml::Result<()> {
# let bytes: Vec<u8> = Vec::new();
# if bytes.is_empty() { return Ok(()); }
use craft_idml::{Local, Props};

let mut package = craft_idml::Package::open(&bytes)?;
let doc = package.document()?;          // designmap.xml with its includes in place
for story in doc.top.iter().filter(|e| e.name == "Story") {
    for range in story.find_all(Local("CharacterStyleRange")) {
        let size = range.num("PointSize");          // attribute or <Properties> child
        let font = range.value("AppliedFont").and_then(|f| craft_idml::value::font_family(&f));
    }
}
for line in &doc.warnings { /* a part designmap.xml lists and the package lacks */ }
# Ok(()) }
  • Package::part finds a part by its exact name, then leniently: writers differ in percent-encoding, case, path separators and a leading ./ or /.
  • Package::document refuses a package whose mimetype says something else, that has no designmap.xml, or whose root is not Document. An included part that is missing is a warning; one that cannot be read is an error.
  • Elements are craft_xml::El, names as written (idPkg:Story), text exact. Processing instructions inside elements are kept (<?ACE 18?> is how InDesign writes special characters). Parts are read strictly: one that is cut off is an error, not half a part.
  • Props reads a property in either spelling: an attribute, or a child of <Properties>.

Limits

Limits field Default Past it
max_entries 100,000 Error::TooManyEntries
max_part_bytes 512 MiB Error::TooLarge
max_total_bytes (all parts read from one package) 2 GiB Error::TooLarge
xml.max_depth 256 Error::Part
xml.max_nodes, xml.max_attrs_total (one part) 8,000,000 and 32,000,000 Error::Part

The ZIP layer is craft-zip: the declared entry count is checked before the archive is indexed, and sizes are checked against what a part really expands to, not what the archive declares.

Writing

use craft_idml::{El, part_root, part_xml, designmap_xml, write_package, prop, with_props};

let range = with_props(El::new("CharacterStyleRange").with_attr("PointSize", "9"), vec![prop("Leading", "unit", "14")]);
let story = part_root("Story").with_child(El::new("Story").with_attr("Self", "u1").with_child(range));
let designmap = El::new("Document")
    .with_attr("xmlns:idPkg", craft_idml::PACKAGING_NS)
    .with_attr("DOMVersion", craft_idml::DOM_VERSION)
    .with_child(El::new("idPkg:Story").with_attr("src", "Stories/Story_u1.xml"));
let bytes = write_package(&[
    ("META-INF/container.xml", craft_idml::CONTAINER_XML.as_bytes().to_vec()),
    ("designmap.xml", designmap_xml(&designmap)),
    ("Stories/Story_u1.xml", part_xml(&story)),
])?;
assert!(craft_idml::is_idml(&bytes));
# Ok::<(), craft_idml::Error>(())

Parts are written with an element that has only element children one child per line, and any other element on one line, so story text is never changed by indentation.

Values

craft_idml::value: num and point_str (numbers as IDML writes them), nums, matrix, point, tint, escape_id / unescape_id / uid (Self ids), style_name_in / style_name_out / style_self (style groups and built-in names), font_family (AppliedFont), xmp_value (dates in META-INF/metadata.xml), base64_encode / base64_decode (embedded Contents), uri_to_path / path_to_uri (link URIs).

Tagged Text

craft_idml::tagged reads and writes InDesign Tagged Text: plain text with <Tag:value> markup.

use craft_idml::tagged;

let file = b"<ASCII-WIN>\r\n<Version:8>\r\n<ParaStyle:Body>A <CharStyle:Em>word<CharStyle:>";
assert!(tagged::is_tagged(file));
let text = tagged::parse(file);
assert_eq!(text.paragraphs[0].style, "Body");
assert_eq!(text.paragraphs[0].runs[1].format.style.as_deref(), Some("Em"));
assert_eq!(tagged::parse(tagged::write(&text.paragraphs).as_bytes()).paragraphs, text.paragraphs);

Paragraph styles, character styles, font family, font style and size are kept; other tags are counted (skipped_tags) and skipped. ASCII, UTF-8 and UTF-16 (by byte-order mark) are read; ASCII is written, other characters as <0xXXXX> UTF-16 code units. Style and font names are cut to MAX_NAME_CHARS (256). Every input reads as something.

Where it came from

DesignCraft's crates/idml (the package reader and writer, the <Properties> accessors, the value codecs) and crates/textimport/src/tagged.rs. DesignCraft's own XML tree is replaced by craft-xml.