* craft-kra, craft-ora: read and write through craft-zip Both drop their own bounded entry read (and craft-ora its zip writer). New: the declared entry count is checked before the archive is indexed (MAX_ENTRIES, 100,000), and an archive with two entries of one name is refused. craft-ora writes a fixed timestamp. zip_mimetype moves into craft-zip as stored_mimetype; both crates keep their function. * craft-zip: Archive is Clone; compressed_size and read_head For the package crates moving onto it: a clone reads behind &self with its own count, compressed_size gives a format the ratio an entry claims, and read_head reads a signature without inflating the rest. * craft-sketch: read through craft-zip Its own budgets (ratio, per-member, JSON total, image total) stay. New: the declared entry count is checked before the archive is indexed (MAX_ENTRIES, 100,000), also when sniffing, and an archive with two entries of one name is refused. * craft-fig: read and write through craft-zip The image budget (per image, total, charged for failed reads, skipped count) stays this crate's. A failed image read is now charged its declared size, which bounds it because the read stops one byte past it; the separate checksum check at an exact budget goes, as the read now always reaches the end of the entry. New: the declared entry count is checked before the archive is indexed (MAX_ENTRIES, 1,000,000), also when sniffing, and two entries of one name are refused. The writer writes a fixed timestamp. * craft-penpot: read and write through craft-zip The 1 GiB byte budget is now the archive's total in craft-zip, counted from bytes actually read; the JSON value budget and the sticky exhaustion flag stay here. Duplicate names are found by craft-zip (the hand-read end record goes). A read that does not fit what is left is refused before it starts and fails every later check(); one that fails otherwise is charged its declared size. embedded_fonts reads four bytes of each candidate with read_head. New: the entry count is checked before the archive is indexed. * craft-opc: read and write through craft-zip The entry-count guard and its test sample move to craft-zip; check_entry_count and zip64_entry_count_sample stay here as wrappers. Package and LazyPackage keep their own counts and rules and read through craft-zip's bounded read. A part that declares more than is left is now refused before it is inflated, so it no longer spends the inflate budget (the test that pinned the old charge is updated). An exactly repeated entry name is logged, not refused. ZipOut wraps craft-zip's Writer; its output is unchanged in kind (level 6, fixed timestamp). * craft-odf: read and write through craft-zip The entry-count guard moves to craft-zip; zip_guard::check and zip64_entry_count_sample stay public here as wrappers. Package keeps its own count and reads through craft-zip's bounded read: a damaged file is still Ok(None), now charged its declared size. Two files of one name are refused. encrypt read every file whole and decrypt cut a file at the limit without saying so; both now refuse a file declaring more than the per-file limit. * craft-ora: a needless borrow in a test; Cargo.lock for the crates now on craft-zip
craft-sketch
A clean-room reader for the .sketch container, for files saved by Sketch 48 and newer.
No unsafe.
This crate is the container layer. It opens the archive, applies the version cut, and hands out
document.json, meta.json and the pages as the JSON the file holds (serde_json::Value, every
object with its kind in _class) and the images as bytes. What the layers mean, and how they map
onto an application's document, stays with the caller.
Provenance
Written by FigCraft's contributors (the .sketch import is Jindřich Šaršon's work), from the
public description of the format only, and files built for each case, checked against Sketch's
own renders and real files (Sketch 50, 63 and 2026). No code of Sketch or of any other product was copied, and no GPL, LGPL
or AGPL code was consulted. Moved here from figcraft/crates/format/src/sketch.rs (the archive
half).
The format
- A
.sketchfile is a ZIP archive:document.json(the pages in order as_refs topages/<id>, shared styles, swatches, library symbols copied into the document),meta.json(appVersion, the formatversion),user.json, onepages/<id>.jsonper page (the layer tree),images/<name>and, in newer files,fonts/. - Before version 43 a Sketch file was a SQLite database; until 47 text was a binary property
list inside the JSON. Both are refused as too old (
Error::TooOld, with a message saying to save the file again in a newer Sketch). meta.jsonversions above 139 (Sketch 83, the newest checked against real files) open with a warning.
Reading
if craft_sketch::is_sketch(&bytes) {
let archive = craft_sketch::Archive::open(&bytes)?;
for (id, page) in &archive.pages { // in the document's order
let layers = page.get("layers");
}
let styles = archive.document.get("layerStyles");
let image = archive.image("images/0a1b….png"); // Option<Vec<u8>>
let fonts = craft_sketch::embedded_font_families(&archive.document);
for warning in &archive.warnings { /* pages skipped, a newer format than tested */ }
}
A document.json that can't be read (damaged, nested too deep) loses the shared styles, not the
pages: they are then taken in archive order, with a warning. A page that can't be read is
skipped with a warning; the others open.
Limits
Input is hostile. Nothing panics; every limit has a test.
| Limit | Value |
|---|---|
meta.json, document.json (MAX_JSON_MEMBER) |
512 MiB |
A page (Limits::max_page) |
512 MiB |
JSON read in all (MAX_JSON_TOTAL) |
2 GiB (512 MiB on 32-bit targets) |
Unpacked size per compressed byte (MAX_RATIO), past a 16 MiB floor (MIN_ALLOWANCE) |
100 |
Pages (MAX_PAGES) |
1000 |
An image (Limits::max_image) |
256 MiB |
Image bytes in all (Limits::max_image_total) |
1 GiB (256 MiB on 32-bit targets) |
| A page id or image name | 128 characters of letters, digits, - and _ (an image may have one extension) |
| JSON nesting | serde_json's 128 levels |
A member is refused before unpacking when its declared size is past its allowance, and again if it unpacks past it. A read that fails is charged in full, and a page listed twice is read once, so a page list can't repeat an unpacking within the budget.
ZIP
The archive is read with the zip crate directly, with the bounds above applied to each member.
A shared bounded ZIP reader is planned (craft-libs #126); this crate moves to it when it lands.
Not here yet
- A typed model of the document and the layers (
_classobjects: pages, artboards, groups, shape paths, text with attributed strings, symbols and overrides, styles). FigCraft maps the JSON straight onto its own import shape today. - Writing.