* Add craft-doc: read PhotoCraft, VectorCraft and EffectCraft documents without the apps
A new crate, craft-doc, reads the native files of three apps from bytes:
PhotoCraft .pcraft (a ZIP bundle), VectorCraft .vectorcraft (JSON, plain
or gzip, with the version 4 data after the JSON) and EffectCraft .ecproj
(JSON).
- sniff tells which app wrote a file from its first bytes.
- info gives the format version, the writer the file names, and whether
the version is one the crate reads.
- renditions returns VectorCraft's preview and PDF and PhotoCraft's
thumbnail and composite, when the file has them.
- outline returns PhotoCraft's canvas and layer tree, VectorCraft's
artboards and top-level layers, and EffectCraft's compositions with
their layers.
A file whose version is newer than the crate reads is refused with
Error::TooNew. Limits bound the JSON, each entry, the total bytes
decompressed and the JSON nesting, and each is checked before the bytes
it covers are allocated. ZIP entries are read by a small STORE and
DEFLATE reader in the crate. The crate depends on serde, serde_json,
thiserror and flate2 and builds for wasm32-unknown-unknown.
The tests build every document they read: JSON written by hand, PNG,
ZIP and gzip bytes assembled in the test. doc.yml runs fmt, clippy, the
tests and a wasm32 check.
* ci: do not keep the checkout token in the doc workflow
The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.
* craft-doc: read each format's envelope through its format crate
craft-doc no longer keeps its own copies of the envelope types and
constants. It reads them with craft-pcraft-format (`MANIFEST`,
`FORMAT_VERSION`, `THUMB`, `COMPOSITE`, `check_manifest`),
craft-vectorcraft-format (`Head`, `Image`, `sniff`, `check`, `VERSION`,
`BLOB_MAGIC`, base64) and craft-ecproj-format (`Head::read`,
`SCHEMA_VERSION`). Its own base64 module is removed. A `.pcraft`
manifest is still read in one pass without building a
`serde_json::Value`, with the checks `read_manifest` applies. The
outlines are still read with craft-doc's read-only views of each
document, and the public API (`sniff`, `info`, `renditions`, `outline`,
`Limits`) does not change.
These inputs are now read as the format crates read them:
- A `.pcraft` manifest without `generator`, or with a `thumbnail` other
than `thumb.png` or a `composite` other than `composite/preview.png`,
is malformed for `renditions` and `outline`; `info` still reports its
version. A manifest with more than 4,194,304 JSON items is refused
with `LimitExceeded`.
- A `.vectorcraft` preview or PDF member without `mime` gives no
rendition.
- `sniff` takes `format` only from a top-level member, so
`{"meta":{"format":"vectorcraft"}}` is not a VectorCraft file.
tests/envelopes.rs writes a file of each format with the crates' types
and reads it through every call, and checks the inputs above.
doc.yml also runs when one of the three crates changes.
* craft-doc: sniff reads only top-level members of an EffectCraft project
`sniff` took any JSON object whose first member is `savedBy` for an
EffectCraft project when `"schema"` appeared anywhere in its first
bytes, so `{"savedBy":"1.0","metadata":{"schema":1}}` was claimed. It
now checks the top-level member names only, one at a time, without
keeping them.
* craft-doc: bound list elements with max_items; no rendition without a MIME type
Limits gains max_items (default 1,048,576): EffectCraft project items and
composition layers, VectorCraft artboards and layers, and PhotoCraft layers
at every level are counted before each one is read, and a file that lists
more gives Error::LimitExceeded. A list of empty objects within max_json no
longer expands into gigabytes of layers.
A VectorCraft preview or pdf member with an empty mime gives no rendition, as
one without a mime does; a preview may be PNG or JPEG. An EffectCraft
composition without a duration or a frame rate gets no timing instead of a
duration of 0.
6.1 KiB
craft-doc
Reads PhotoCraft .pcraft, VectorCraft .vectorcraft and EffectCraft .ecproj documents
without the apps: which app wrote a file, its format version, the previews and PDF it carries,
and its pages and layers. It takes bytes and returns plain data. It does not open files, render
or composite. No unsafe, no panics on malformed input, and it builds for
wasm32-unknown-unknown. Each format's envelope is read through its crate in this repository:
craft-pcraft-format (the Manifest members and checks, and
check_manifest),
craft-vectorcraft-format (Head, Image, sniff, the
version and the marker) and craft-ecproj-format (Head, the
schema check). The outlines are read with craft-doc's own read-only views of the documents. The
other dependencies are serde, serde_json, thiserror and flate2 (its pure-Rust backend).
craft-doc = { git = "https://github.com/storytold/craft-libs", rev = "<commit>" }
Use
use craft_doc::{Kind, Limits, RenditionKind};
let limits = Limits::default();
match craft_doc::sniff(&bytes) {
Some(Kind::VectorCraft) => {
let pdf = craft_doc::renditions(&bytes, &limits)?.into_iter().find(|r| r.kind == RenditionKind::Pdf);
}
Some(kind) => { /* ... */ }
None => { /* not a Craft document */ }
}
let outline = craft_doc::outline(&bytes, &limits)?;
sniff(head) -> Option<Kind>reads the first bytes. The first 64 KiB are enough for every file the apps write. A re-zipped.pcraftwhosemanifest.jsonentry starts after the head is recognized only whenheadholds the whole file.info(bytes, &Limits) -> Result<Info, Error>: the kind, the version the file declares, the writer it names, andsupported, which isfalsewhen the version is newer than the crate reads.renditions(bytes, &Limits) -> Result<Vec<Rendition>, Error>: eachRenditionhas akind(Pdf,Thumbnail,Composite), amimeand thebytes.outline(bytes, &Limits) -> Result<Outline, Error>:canvas,pagesandlayers.ErrorisUnrecognized,Malformed,TooNew,UnsupportedorLimitExceeded. A file whose version is newer than the crate reads givesTooNewfromrenditionsandoutline.
What each format gives
.vectorcraft |
.pcraft |
.ecproj |
|
|---|---|---|---|
| Container | JSON, plain or gzip; since version 4, large members after the JSON (\n\0VCBLOBS\0) |
ZIP of STORE or DEFLATE entries | JSON |
| Version read | version up to 4 ("format": "vectorcraft" or "drawcraft") |
format_version 1 |
schema up to 2 |
| Writer | generator |
generator |
savedBy |
| Renditions | preview (Thumbnail) and pdf (Pdf), when the file was saved with them |
the thumbnail and composite the manifest names |
none |
| Pages | artboards, in points | none | compositions by item id, in pixels, with frame rate and duration |
| Layers | top-level layers | the layer tree with groups | each composition's layers |
Layers are listed bottom first in every format. A layer's kind is the name the file uses:
layer in VectorCraft; raster, group, adjustment, fill, text, shape or smart in
PhotoCraft; the source type in EffectCraft (Footage, Comp, Solid, Text and others).
EffectCraft layers have no opacity, because opacity is an animated property there.
A .pcraft bundle saved as a folder, ZIP64 archives and encrypted entries are not read. The
.ecprojx XML copies are not read.
Limits
Limits (fields max_json, max_entry, max_total, max_depth) bounds each call:
max_json(256 MiB): the JSON read, which is the VectorCraft file's JSON, the PhotoCraft manifest or the EffectCraft project. The names and other strings returned are parts of it.max_entry(256 MiB): one rendition or one ZIP entry. The size a file states is checked before any of it is copied or inflated.max_total(1 GiB): every byte one call decompresses from gzip, reads out of ZIP entries or returns as a rendition, together.max_depth(400, also the largest value it takes,MAX_DEPTH): the nesting of JSON arrays and objects, checked before parsing. PhotoCraft nests up to 100 groups of layers, three levels each.
A PhotoCraft manifest with more than 4,194,304 JSON items
(craft_pcraft_format::MAX_MANIFEST_ITEMS) is refused with Error::LimitExceeded, as PhotoCraft
reads it.
On a compressed .vectorcraft file, info and outline decompress only as far as the end of
the JSON. renditions checks the size of each member after the JSON against max_entry
before it decompresses any of that data, then decompresses only as far as the members end. The
struct is #[non_exhaustive]: start from the defaults and set the fields you
change.
let mut limits = craft_doc::Limits::default();
limits.max_entry = 32 << 20;
Testing
cargo test -p craft-doc runs:
tests/formats.rs: documents of each format written by the tests (JSON by hand, PNG and ZIP bytes assembled in the test, gzip throughflate2), read back through each call: plain and gzip VectorCraft files, members inline as base64 and after the JSON,.pcraftbundles with STORE and DEFLATE entries and the manifest last,.ecprojcompositions, newer versions, and filessniffmust not claim (PDF, PNG, other ZIP and JSON files).tests/hostile.rs: every truncation of each sample, seeded mutations, proptest on arbitrary bytes, and inputs that pass each limit: a gzip bomb, a DEFLATE entry larger than its stated size, members longer thanmax_entry, JSON nested pastmax_depth, ZIP64, encryption, other compression methods and CRC mismatches.tests/envelopes.rs: files whose envelope is written with the format crates' types read through each call, and envelopes those crates refuse (a manifest withoutgenerator, a preview that names another entry, aformatmember inside another member) are refused. A preview or PDF member withoutmimegives no rendition.
.github/workflows/doc.yml runs fmt, clippy and the tests on Linux and Windows, and a
wasm32-unknown-unknown check.