mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
Merge pull request #524 from dotCooCoo/fix/craft-doc-review-points
craft-doc: read envelopes as the format crates and apps do
This commit is contained in:
7 files changed
+916
-126
No files matched your search
+23
-10
@@ -7,8 +7,8 @@ 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`](../pcraft-format/README.md) (the `Manifest` members and checks, and
|
||||
`check_manifest`),
|
||||
[`craft-vectorcraft-format`](../vectorcraft-format/README.md) (`Head`, `Image`, `sniff`, the
|
||||
version and the marker) and [`craft-ecproj-format`](../ecproj-format/README.md) (`Head`, the
|
||||
[`craft-vectorcraft-format`](../vectorcraft-format/README.md) (`Image`, `sniff`, the version
|
||||
and the marker) and [`craft-ecproj-format`](../ecproj-format/README.md) (`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).
|
||||
|
||||
@@ -34,7 +34,10 @@ 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 `.pcraft` whose `manifest.json` entry starts after the head is
|
||||
recognized only when `head` holds the whole file.
|
||||
recognized only when `head` holds the whole file. When `head` holds a whole ZIP archive, the
|
||||
names in its central directory decide. A VectorCraft file is recognized by its top-level
|
||||
`format` member, as `craft_vectorcraft_format::sniff` recognizes it, so a file written as a
|
||||
JSON array, which `File::read` also reads, is not.
|
||||
- `info(bytes, &Limits) -> Result<Info, Error>`: the kind, the version the file declares, the
|
||||
writer it names, and `supported`, which is `false` when the version is newer than the crate
|
||||
reads.
|
||||
@@ -42,7 +45,8 @@ let outline = craft_doc::outline(&bytes, &limits)?;
|
||||
(`Pdf`, `Thumbnail`, `Composite`), a `mime` and the `bytes`.
|
||||
- `outline(bytes, &Limits) -> Result<Outline, Error>`: `canvas`, `pages` and `layers`.
|
||||
- `Error` is `Unrecognized`, `Malformed`, `TooNew`, `Unsupported` or `LimitExceeded`. A file
|
||||
whose version is newer than the crate reads gives `TooNew` from `renditions` and `outline`.
|
||||
whose version is newer than the crate reads gives `TooNew` from `renditions` and `outline`. A
|
||||
ZIP archive whose `manifest.json` has no `format_version` gives `Unrecognized`.
|
||||
|
||||
## What each format gives
|
||||
|
||||
@@ -72,9 +76,12 @@ A `.pcraft` bundle saved as a folder, ZIP64 archives and encrypted entries are n
|
||||
- `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.
|
||||
returns as a rendition, together. A rendition decompressed from gzip is counted once, and a
|
||||
second rendition of the same bytes is counted as another copy.
|
||||
- `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.
|
||||
`renditions` and `outline` refuse a PhotoCraft manifest nested deeper than 367 levels
|
||||
(`craft_pcraft_format::manifest_depth(MAX_GROUP_DEPTH)`), as PhotoCraft does.
|
||||
|
||||
A PhotoCraft manifest with more than 4,194,304 JSON items
|
||||
(`craft_pcraft_format::MAX_MANIFEST_ITEMS`) is refused with `Error::LimitExceeded`, as PhotoCraft
|
||||
@@ -82,7 +89,8 @@ 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
|
||||
before it decompresses any of that data, and the offset where the last member ends against
|
||||
`max_total`. It then decompresses only as far as the members end, and keeps only their bytes. The
|
||||
struct is `#[non_exhaustive]`: start from the defaults and set the fields you
|
||||
change.
|
||||
|
||||
@@ -103,11 +111,16 @@ limits.max_entry = 32 << 20;
|
||||
- `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 than `max_entry`, JSON nested past `max_depth`, ZIP64, encryption, other
|
||||
compression methods and CRC mismatches.
|
||||
compression methods and CRC mismatches. An array of a million elements in place of a
|
||||
`generator` or a preview is skipped.
|
||||
- `tests/envelopes.rs`: files whose envelope is written with the format crates' types read
|
||||
through each call, and envelopes those crates refuse (a manifest without `generator`, a preview
|
||||
that names another entry, a `format` member inside another member) are refused. A preview or
|
||||
PDF member without `mime` gives no rendition.
|
||||
through each call, and envelopes those crates refuse (a manifest without `generator`, a
|
||||
manifest that is a JSON array, a preview that names another entry, a `format` member inside
|
||||
another member) are refused. A preview or PDF member without `mime` gives no rendition, and a
|
||||
VectorCraft `generator`, `preview` or `pdf` reads as `File::read` reads it: as `None` when it
|
||||
has the wrong shape, and as its last value when it is repeated. A value of the wrong shape is
|
||||
skipped as it is parsed, and none of it is kept. A repeated `version`, and in `renditions` a
|
||||
missing or repeated `document`, are refused, as `File::read` refuses them.
|
||||
|
||||
`.github/workflows/doc.yml` runs fmt, clippy and the tests on Linux and Windows, and a
|
||||
`wasm32-unknown-unknown` check.
|
||||
+222
-15
@@ -91,7 +91,9 @@ pub struct Info {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// [`sniff`] does not recognize the bytes as a Craft document.
|
||||
/// [`sniff`] does not recognize the bytes as a Craft document, or a ZIP archive's
|
||||
/// `manifest.json` is a JSON object without `format_version`, so the archive is not a
|
||||
/// PhotoCraft bundle.
|
||||
#[error("not a PhotoCraft, VectorCraft or EffectCraft document")]
|
||||
Unrecognized,
|
||||
/// The file is damaged or does not follow its format.
|
||||
@@ -126,10 +128,13 @@ pub struct Limits {
|
||||
/// Most bytes of one rendition or one ZIP entry. Default 256 MiB.
|
||||
pub max_entry: u64,
|
||||
/// Most bytes one call decompresses from gzip, reads out of ZIP entries and returns as
|
||||
/// renditions, together. Default 1 GiB.
|
||||
/// renditions, together; a rendition decompressed from gzip is counted once, and a second
|
||||
/// rendition of the same bytes as another copy. Default 1 GiB.
|
||||
pub max_total: u64,
|
||||
/// Deepest nesting of JSON arrays and objects, at most [`MAX_DEPTH`] (a larger value counts
|
||||
/// as [`MAX_DEPTH`]). Default [`MAX_DEPTH`].
|
||||
/// as [`MAX_DEPTH`]). Default [`MAX_DEPTH`]. [`renditions`] and [`outline`] also refuse a
|
||||
/// PhotoCraft manifest nested deeper than PhotoCraft reads, 367 levels
|
||||
/// (`craft_pcraft_format::manifest_depth(MAX_GROUP_DEPTH)`).
|
||||
pub max_depth: u32,
|
||||
/// Most list elements one call reads, together: EffectCraft project items and composition
|
||||
/// layers, VectorCraft artboards and layers, and PhotoCraft layers at every level. Each is
|
||||
@@ -326,11 +331,17 @@ impl<'a> Budget<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check one rendition or ZIP entry of `n` bytes against `max_entry`, then count it.
|
||||
pub(crate) fn take_entry(&mut self, n: u64, what: &str) -> Result<(), Error> {
|
||||
/// Check one rendition or ZIP entry of `n` bytes against `max_entry`.
|
||||
pub(crate) fn check_entry(&self, n: u64, what: &str) -> Result<(), Error> {
|
||||
if n > self.limits.max_entry {
|
||||
return Err(Error::LimitExceeded(format!("{what} is {n} bytes, more than max_entry ({})", self.limits.max_entry)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check one rendition or ZIP entry of `n` bytes against `max_entry`, then count it.
|
||||
pub(crate) fn take_entry(&mut self, n: u64, what: &str) -> Result<(), Error> {
|
||||
self.check_entry(n, what)?;
|
||||
self.take(n, what)
|
||||
}
|
||||
|
||||
@@ -487,27 +498,175 @@ fn parse_checked<T: DeserializeOwned>(json: &[u8]) -> Result<T, serde_json::Erro
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// A document's version number: VectorCraft's `version`, PhotoCraft's `format_version` or
|
||||
/// EffectCraft's `schema`.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Version {
|
||||
#[serde(alias = "format_version", alias = "schema")]
|
||||
version: u32,
|
||||
/// The top-level member that holds a document's version number: VectorCraft's `version`,
|
||||
/// PhotoCraft's `format_version` or EffectCraft's `schema`.
|
||||
fn version_member(kind: Kind) -> &'static str {
|
||||
match kind {
|
||||
Kind::PhotoCraft => "format_version",
|
||||
Kind::VectorCraft => "version",
|
||||
Kind::EffectCraft => "schema",
|
||||
}
|
||||
}
|
||||
|
||||
/// The version number in the member a [`Visitor`] is given, read from the top-level JSON object;
|
||||
/// the other members are skipped, and a repeated version member is refused.
|
||||
struct VersionIn(&'static str);
|
||||
|
||||
impl<'de> Visitor<'de> for VersionIn {
|
||||
type Value = Option<u32>;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a JSON object")
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Option<u32>, A::Error> {
|
||||
let mut version = None;
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
if key == self.0 {
|
||||
if version.is_some() {
|
||||
return Err(de::Error::duplicate_field(self.0));
|
||||
}
|
||||
version = Some(map.next_value()?);
|
||||
} else {
|
||||
map.next_value::<de::IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `T` from `json`; when that fails, report [`Error::TooNew`] instead if the file's version
|
||||
/// (its top-level `version`, `format_version` or `schema`) is newer than `kind` supports, since a
|
||||
/// (its top-level member that [`version_member`] names) is newer than `kind` supports, since a
|
||||
/// newer format may change what `T` expects.
|
||||
pub(crate) fn parse_versioned<T: DeserializeOwned>(json: &[u8], limits: &Limits, kind: Kind) -> Result<T, Error> {
|
||||
check_depth(json, limits.max_depth)?;
|
||||
counting(limits.max_items, || {
|
||||
parse_checked(json).map_err(|e| match parse_checked::<Version>(json) {
|
||||
Ok(v) if v.version > kind.supported_version() => too_new(kind, v.version),
|
||||
_ => malformed(e),
|
||||
parse_checked(json).map_err(|e| {
|
||||
let mut de = serde_json::Deserializer::from_slice(json);
|
||||
de.disable_recursion_limit();
|
||||
let version = (&mut de).deserialize_map(VersionIn(version_member(kind))).and_then(|v| de.end().map(|()| v));
|
||||
match version {
|
||||
Ok(Some(version)) if version > kind.supported_version() => too_new(kind, version),
|
||||
_ => malformed(e),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// A member read as `None` when its value has the wrong shape, as the format crates read a
|
||||
/// file's optional members. The value is read as it is parsed: the elements of an array or
|
||||
/// object of the wrong shape are skipped one at a time with [`de::IgnoredAny`], and none of the
|
||||
/// value is kept.
|
||||
pub(crate) struct Lenient<T>(pub(crate) Option<T>);
|
||||
|
||||
/// A type [`Lenient`] reads.
|
||||
pub(crate) trait Shaped: Sized {
|
||||
/// The value, or `None` when it has the wrong shape.
|
||||
fn read<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Self>, D::Error>;
|
||||
}
|
||||
|
||||
impl<'de, T: Shaped> Deserialize<'de> for Lenient<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
T::read(d).map(Lenient)
|
||||
}
|
||||
}
|
||||
|
||||
impl Shaped for String {
|
||||
fn read<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
Ok(Scalar::deserialize(d)?.string())
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Lenient`] as a `deserialize_with` function.
|
||||
pub(crate) fn lenient<'de, D: Deserializer<'de>, T: Shaped>(d: D) -> Result<Option<T>, D::Error> {
|
||||
Lenient::deserialize(d).map(|l| l.0)
|
||||
}
|
||||
|
||||
/// One JSON value: a string, a non-negative integer or a boolean is kept, and an array or object
|
||||
/// is skipped one element at a time with [`de::IgnoredAny`].
|
||||
pub(crate) enum Scalar {
|
||||
Str(String),
|
||||
Unsigned(u64),
|
||||
Bool(bool),
|
||||
Null,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Scalar {
|
||||
pub(crate) fn string(self) -> Option<String> {
|
||||
match self {
|
||||
Scalar::Str(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unsigned(self) -> Option<u64> {
|
||||
match self {
|
||||
Scalar::Unsigned(n) => Some(n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bool(self) -> Option<bool> {
|
||||
match self {
|
||||
Scalar::Bool(b) => Some(b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Scalar {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
struct ScalarVisitor;
|
||||
impl<'de> Visitor<'de> for ScalarVisitor {
|
||||
type Value = Scalar;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a JSON value")
|
||||
}
|
||||
|
||||
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Bool(v))
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Scalar, E> {
|
||||
Ok(u64::try_from(v).map_or(Scalar::Other, Scalar::Unsigned))
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Unsigned(v))
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, _: f64) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Other)
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, v: &str) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Str(v.to_owned()))
|
||||
}
|
||||
|
||||
fn visit_string<E: de::Error>(self, v: String) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Str(v))
|
||||
}
|
||||
|
||||
fn visit_unit<E: de::Error>(self) -> Result<Scalar, E> {
|
||||
Ok(Scalar::Null)
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Scalar, A::Error> {
|
||||
while seq.next_element::<de::IgnoredAny>()?.is_some() {}
|
||||
Ok(Scalar::Other)
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Scalar, A::Error> {
|
||||
while map.next_entry::<de::IgnoredAny, de::IgnoredAny>()?.is_some() {}
|
||||
Ok(Scalar::Other)
|
||||
}
|
||||
}
|
||||
d.deserialize_any(ScalarVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn too_new(kind: Kind, version: u32) -> Error {
|
||||
Error::TooNew { kind, version, supported: kind.supported_version() }
|
||||
}
|
||||
@@ -533,4 +692,52 @@ mod tests {
|
||||
assert_eq!(b.left(), 3);
|
||||
assert!(b.check_json(11, "x").is_err() && b.check_json(10, "x").is_ok());
|
||||
}
|
||||
|
||||
/// A value that can be skipped but not read: reading it whole, as a buffer of the value
|
||||
/// would, fails.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Unread;
|
||||
|
||||
impl<'de> Deserializer<'de> for Unread {
|
||||
type Error = de::value::Error;
|
||||
|
||||
fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value, Self::Error> {
|
||||
Err(de::Error::custom("the value was read, not skipped"))
|
||||
}
|
||||
|
||||
fn deserialize_ignored_any<V: Visitor<'de>>(self, v: V) -> Result<V::Value, Self::Error> {
|
||||
v.visit_unit()
|
||||
}
|
||||
|
||||
serde::forward_to_deserialize_any! {
|
||||
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf option unit
|
||||
unit_struct newtype_struct seq tuple tuple_struct map struct enum identifier
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> de::IntoDeserializer<'de> for Unread {
|
||||
type Deserializer = Unread;
|
||||
|
||||
fn into_deserializer(self) -> Unread {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// An array of `n` [`Unread`] elements.
|
||||
pub(crate) fn unread_array(n: usize) -> de::value::SeqDeserializer<std::iter::RepeatN<Unread>, de::value::Error> {
|
||||
de::value::SeqDeserializer::new(std::iter::repeat_n(Unread, n))
|
||||
}
|
||||
|
||||
/// An object whose `n` members hold [`Unread`] values.
|
||||
pub(crate) fn unread_object(n: usize) -> de::value::MapDeserializer<'static, std::vec::IntoIter<(String, Unread)>, de::value::Error> {
|
||||
de::value::MapDeserializer::new((0..n).map(|i| (i.to_string(), Unread)).collect::<Vec<_>>().into_iter())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lenient_string_skips_an_array_or_object_without_reading_it_whole() {
|
||||
assert!(Lenient::<String>::deserialize(unread_array(1000)).unwrap().0.is_none());
|
||||
assert!(Lenient::<String>::deserialize(unread_object(1000)).unwrap().0.is_none());
|
||||
let s: Lenient<String> = Lenient::deserialize(de::value::StrDeserializer::<de::value::Error>::new("VectorCraft 0.1.0")).unwrap();
|
||||
assert_eq!(s.0.as_deref(), Some("VectorCraft 0.1.0"));
|
||||
}
|
||||
}
|
||||
+63
-19
@@ -3,15 +3,26 @@
|
||||
//! (`thumbnail`, `composite`). Only the ZIP form is read; a bundle saved as a folder is not. The
|
||||
//! manifest's envelope is [`craft_pcraft_format::Manifest`]'s.
|
||||
|
||||
use craft_pcraft_format::{self as format, COMPOSITE, FORMAT_VERSION, MANIFEST, THUMB};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use craft_pcraft_format::{self as format, COMPOSITE, FORMAT_VERSION, MANIFEST, MAX_GROUP_DEPTH, THUMB, manifest_depth};
|
||||
use serde::Deserialize;
|
||||
use serde::de::{DeserializeOwned, IgnoredAny};
|
||||
use serde::de::value::MapAccessDeserializer;
|
||||
use serde::de::{DeserializeOwned, Deserializer, IgnoredAny, MapAccess, Visitor};
|
||||
|
||||
use crate::zip::{Zip, local_names_include};
|
||||
use crate::{Budget, Canvas, Error, Kind, Layer, MAX_DEPTH, Outline, Rendition, RenditionKind, counted, malformed, parse, parse_versioned, too_new};
|
||||
use crate::{Budget, Canvas, Error, Kind, Layer, Outline, Rendition, RenditionKind, counted, lenient, malformed, parse, too_new};
|
||||
|
||||
/// A ZIP archive with a `manifest.json` entry. When `head` holds the whole archive, the names in
|
||||
/// its central directory decide, as [`Zip::find`] reads them; otherwise the names in the local
|
||||
/// headers that `head` holds.
|
||||
pub(crate) fn sniff(head: &[u8]) -> bool {
|
||||
head.starts_with(b"PK\x03\x04") && (local_names_include(head, MANIFEST) || Zip::open(head).is_ok_and(|z| z.find(MANIFEST).is_some()))
|
||||
head.starts_with(b"PK\x03\x04")
|
||||
&& match Zip::open(head) {
|
||||
Ok(zip) => zip.find(MANIFEST).is_some(),
|
||||
Err(_) => local_names_include(head, MANIFEST),
|
||||
}
|
||||
}
|
||||
|
||||
/// The archive and its `manifest.json`.
|
||||
@@ -23,27 +34,60 @@ fn manifest<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<(Zip<'a>, Vec<u8
|
||||
Ok((zip, json))
|
||||
}
|
||||
|
||||
/// A `T` read only from a JSON object, as [`craft_pcraft_format::read_manifest`] reads the
|
||||
/// manifest. serde's derived `Deserialize` also reads a struct from a JSON array.
|
||||
struct Object<T>(T);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Object<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
struct ObjectVisitor<T>(PhantomData<T>);
|
||||
impl<'de, T: Deserialize<'de>> Visitor<'de> for ObjectVisitor<T> {
|
||||
type Value = Object<T>;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("the manifest's JSON object")
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Object<T>, A::Error> {
|
||||
T::deserialize(MapAccessDeserializer::new(map)).map(Object)
|
||||
}
|
||||
}
|
||||
d.deserialize_map(ObjectVisitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
/// The version and the writer, read without the rest of the manifest, so that a manifest of a
|
||||
/// newer format still reports them.
|
||||
/// newer format still reports them. A `generator` that is not a string reads as `None`, as
|
||||
/// [`craft_pcraft_format::migrate`] reads it.
|
||||
#[derive(Deserialize)]
|
||||
struct Stamp {
|
||||
format_version: u32,
|
||||
#[serde(default)]
|
||||
/// `None` only when the member is missing; a `null` is not a version.
|
||||
#[serde(default, deserialize_with = "present")]
|
||||
format_version: Option<u32>,
|
||||
#[serde(default, deserialize_with = "lenient")]
|
||||
generator: Option<String>,
|
||||
}
|
||||
|
||||
fn checked(version: u32) -> Result<u32, Error> {
|
||||
/// A member that is present, read as `T`.
|
||||
fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
|
||||
T::deserialize(d).map(Some)
|
||||
}
|
||||
|
||||
/// The manifest's version and writer. A manifest without `format_version` is not PhotoCraft's:
|
||||
/// [`Error::Unrecognized`]. A version of 0 is refused.
|
||||
fn stamp(json: &[u8], budget: &Budget) -> Result<(u32, Option<String>), Error> {
|
||||
let Object(stamp) = parse::<Object<Stamp>>(json, budget.limits)?;
|
||||
let version = stamp.format_version.ok_or(Error::Unrecognized)?;
|
||||
if version == 0 {
|
||||
return Err(malformed("format_version 0 is invalid"));
|
||||
}
|
||||
Ok(version)
|
||||
Ok((version, stamp.generator))
|
||||
}
|
||||
|
||||
/// The members of [`craft_pcraft_format::Manifest`], with the document as `D`, read without
|
||||
/// building the manifest as a `serde_json::Value` first.
|
||||
/// The members of [`craft_pcraft_format::Manifest`] after `format_version`, with the document as
|
||||
/// `D`, read without building the manifest as a `serde_json::Value` first.
|
||||
#[derive(Deserialize)]
|
||||
struct Envelope<D> {
|
||||
format_version: u32,
|
||||
/// Required, and a string, as in the manifest; [`info`] reports it.
|
||||
#[serde(rename = "generator")]
|
||||
_generator: String,
|
||||
@@ -55,22 +99,23 @@ struct Envelope<D> {
|
||||
}
|
||||
|
||||
/// The manifest's envelope, checked as [`craft_pcraft_format::read_manifest`] checks it: the
|
||||
/// size, nesting and item count with [`craft_pcraft_format::check_manifest`], a version of 0 or
|
||||
/// newer than [`FORMAT_VERSION`] refused, and a preview that names an entry other than [`THUMB`]
|
||||
/// size, nesting and item count with [`craft_pcraft_format::check_manifest`] and PhotoCraft's
|
||||
/// own nesting limit, a manifest that is not a JSON object refused, the version read first (0 or
|
||||
/// newer than [`FORMAT_VERSION`] refused), and a preview that names an entry other than [`THUMB`]
|
||||
/// or [`COMPOSITE`] refused. A member that appears twice is refused.
|
||||
fn read<D: DeserializeOwned>(json: &[u8], budget: &Budget) -> Result<Envelope<D>, Error> {
|
||||
let mut limits = format::Limits::default();
|
||||
limits.max_manifest_bytes = json.len();
|
||||
limits.max_depth = budget.limits.max_depth.min(MAX_DEPTH) as usize;
|
||||
limits.max_depth = (budget.limits.max_depth as usize).min(manifest_depth(MAX_GROUP_DEPTH));
|
||||
format::check_manifest(json, &limits).map_err(|e| match e {
|
||||
format::Error::LimitExceeded(m) => Error::LimitExceeded(m),
|
||||
e => malformed(e),
|
||||
})?;
|
||||
let m: Envelope<D> = parse_versioned(json, budget.limits, Kind::PhotoCraft)?;
|
||||
let version = checked(m.format_version)?;
|
||||
let (version, _) = stamp(json, budget)?;
|
||||
if version > FORMAT_VERSION {
|
||||
return Err(too_new(Kind::PhotoCraft, version));
|
||||
}
|
||||
let Object(m) = parse::<Object<Envelope<D>>>(json, budget.limits)?;
|
||||
for (member, entry, expected) in [("thumbnail", &m.thumbnail, THUMB), ("composite", &m.composite, COMPOSITE)] {
|
||||
if let Some(entry) = entry
|
||||
&& entry != expected
|
||||
@@ -83,8 +128,7 @@ fn read<D: DeserializeOwned>(json: &[u8], budget: &Budget) -> Result<Envelope<D>
|
||||
|
||||
pub(crate) fn info(bytes: &[u8], budget: &mut Budget) -> Result<(u32, Option<String>), Error> {
|
||||
let (_, json) = manifest(bytes, budget)?;
|
||||
let stamp: Stamp = parse(&json, budget.limits)?;
|
||||
Ok((checked(stamp.format_version)?, stamp.generator))
|
||||
stamp(&json, budget)
|
||||
}
|
||||
|
||||
pub(crate) fn renditions(bytes: &[u8], budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
|
||||
+390
-73
@@ -11,17 +11,23 @@
|
||||
//! Since version 4, large members may follow the JSON after the marker `\n\0VCBLOBS\0`; such a
|
||||
//! member gives `"blob": [offset, length]` (from the end of the marker) instead of `data`. In a
|
||||
//! compressed file, the marker and the data after it are inside the gzip stream too. Files
|
||||
//! written before the rename say `"format": "drawcraft"`. The envelope's members are
|
||||
//! [`craft_vectorcraft_format::Head`] and [`craft_vectorcraft_format::Image`].
|
||||
//! written before the rename say `"format": "drawcraft"`. The envelope's members are those of
|
||||
//! [`craft_vectorcraft_format::File`], read as [`craft_vectorcraft_format::File::read`] reads
|
||||
//! them, and [`craft_vectorcraft_format::Image`].
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use craft_vectorcraft_format::{BLOB_MAGIC, Head, Image, is_compressed};
|
||||
use craft_vectorcraft_format::{BLOB_MAGIC, Image, entry, is_compressed, marker_at};
|
||||
use flate2::read::GzDecoder;
|
||||
use serde::Deserialize;
|
||||
use serde::de::{self, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
|
||||
|
||||
use crate::{Budget, Error, Kind, Layer, Outline, Page, Rendition, RenditionKind, counted, malformed, parse, parse_versioned, too_new};
|
||||
use crate::{
|
||||
Budget, Error, Kind, Layer, Lenient, Outline, Page, Rendition, RenditionKind, Scalar, Shaped, counted, malformed, parse, parse_versioned, too_new,
|
||||
};
|
||||
|
||||
/// Bytes decompressed at a time.
|
||||
const CHUNK: u64 = 64 << 10;
|
||||
@@ -31,13 +37,11 @@ pub(crate) fn sniff(head: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
/// Where the JSON ends and the data after the marker starts, when `text` holds a NUL. JSON holds
|
||||
/// no raw NUL and the marker's second byte is one, so the JSON ends right before the first NUL;
|
||||
/// a NUL that is not the marker's makes the file malformed.
|
||||
fn marker(text: &[u8], nul: usize) -> Result<(usize, usize), Error> {
|
||||
match nul.checked_sub(1) {
|
||||
Some(at) if text.get(at..).is_some_and(|r| r.starts_with(BLOB_MAGIC)) => Ok((at, at + BLOB_MAGIC.len())),
|
||||
_ => Err(malformed("the file holds a NUL byte outside the marker after its JSON")),
|
||||
}
|
||||
/// no raw NUL and the marker's second byte is one, so the JSON ends right before the first NUL.
|
||||
/// The marker must follow the JSON's closing `}`, `]` or `"`, as
|
||||
/// [`craft_vectorcraft_format::marker_at`] requires; any other NUL makes the file malformed.
|
||||
fn marker(text: &[u8]) -> Result<(usize, usize), Error> {
|
||||
marker_at(text).map(|at| (at, at + BLOB_MAGIC.len())).ok_or_else(|| malformed("the file holds a NUL byte outside the marker after its JSON"))
|
||||
}
|
||||
|
||||
fn damaged(e: std::io::Error) -> Error {
|
||||
@@ -54,32 +58,66 @@ struct Unpacked<'a> {
|
||||
enum Tail<'a> {
|
||||
/// In an uncompressed file.
|
||||
Slice(&'a [u8]),
|
||||
/// Still in the gzip stream: `read` holds what was decompressed past the marker already,
|
||||
/// of which `counted` bytes are counted against the budget.
|
||||
Stream { dec: Box<GzDecoder<&'a [u8]>>, read: Vec<u8>, counted: usize },
|
||||
/// Still in the gzip stream: `read` holds what was decompressed past the marker already, not
|
||||
/// yet counted against the budget.
|
||||
Stream { dec: Box<GzDecoder<&'a [u8]>>, read: Vec<u8> },
|
||||
}
|
||||
|
||||
impl Tail<'_> {
|
||||
/// The data up to `end` (less where the file ends first), decompressing only that far.
|
||||
fn up_to(&mut self, end: u64, budget: &mut Budget) -> Result<&[u8], Error> {
|
||||
match self {
|
||||
Tail::Slice(s) => Ok(s),
|
||||
Tail::Stream { dec, read, counted } => {
|
||||
let end = usize::try_from(end).map_err(|_| Error::LimitExceeded("the data after the JSON does not fit in memory".into()))?;
|
||||
let had = read.len().min(end);
|
||||
if had > *counted {
|
||||
budget.take((had - *counted) as u64, "the decompressed document")?;
|
||||
*counted = had;
|
||||
}
|
||||
if read.len() < end {
|
||||
let want = ((end - read.len()) as u64).min(budget.left().saturating_add(1));
|
||||
let n = (&mut **dec).take(want).read_to_end(read).map_err(damaged)?;
|
||||
budget.take(n as u64, "the decompressed document")?;
|
||||
*counted += n;
|
||||
}
|
||||
Ok(read.as_slice())
|
||||
/// The bytes each `[offset, length]` in `ranges` names, or `None` for a range the file ends
|
||||
/// before. A compressed file is decompressed only as far as the last range ends, and that
|
||||
/// offset is checked against the budget before anything past the marker is decompressed.
|
||||
/// Every byte decompressed is counted; only the bytes inside the ranges are kept.
|
||||
fn ranges(self, ranges: &[[u64; 2]], budget: &mut Budget) -> Result<Vec<Option<Vec<u8>>>, Error> {
|
||||
const WHAT: &str = "the decompressed document";
|
||||
let (mut dec, read) = match self {
|
||||
Tail::Slice(s) => return Ok(ranges.iter().map(|&r| entry(s, r).map(<[u8]>::to_vec)).collect()),
|
||||
Tail::Stream { dec, read } => (dec, read),
|
||||
};
|
||||
let end = ranges.iter().map(|&[offset, len]| offset.saturating_add(len)).max().unwrap_or(0);
|
||||
if end > budget.left() {
|
||||
return Err(Error::LimitExceeded(format!(
|
||||
"the file states data up to byte {end} after its JSON, more than max_total ({} bytes) in all",
|
||||
budget.limits.max_total
|
||||
)));
|
||||
}
|
||||
// The ranges in file order, overlapping ones merged.
|
||||
let mut spans: Vec<[u64; 2]> = ranges.iter().map(|&[offset, len]| [offset, offset.saturating_add(len)]).collect();
|
||||
spans.sort_unstable();
|
||||
let mut merged: Vec<[u64; 2]> = vec![];
|
||||
for [start, end] in spans {
|
||||
match merged.last_mut() {
|
||||
Some(last) if start <= last[1] => last[1] = last[1].max(end),
|
||||
_ => merged.push([start, end]),
|
||||
}
|
||||
}
|
||||
// Each range is returned as its own copy: bytes that more than one range names are
|
||||
// decompressed once and counted again here for each further copy.
|
||||
let stated = ranges.iter().fold(0u64, |n, &[_, len]| n.saturating_add(len));
|
||||
let spanned = merged.iter().fold(0u64, |n, &[start, end]| n.saturating_add(end - start));
|
||||
budget.take(stated.saturating_sub(spanned), WHAT)?;
|
||||
let mut src = read.as_slice().chain(&mut *dec);
|
||||
let (mut at, mut kept) = (0u64, Vec::<(u64, Vec<u8>)>::new());
|
||||
for [start, end] in merged {
|
||||
let skipped = std::io::copy(&mut (&mut src).take(start.saturating_sub(at)), &mut std::io::sink()).map_err(damaged)?;
|
||||
budget.take(skipped, WHAT)?;
|
||||
at += skipped;
|
||||
if at < start {
|
||||
break;
|
||||
}
|
||||
let mut bytes = vec![];
|
||||
let n = (&mut src).take(end - start).read_to_end(&mut bytes).map_err(damaged)?;
|
||||
budget.take(n as u64, WHAT)?;
|
||||
at += n as u64;
|
||||
kept.push((start, bytes));
|
||||
if at < end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(ranges
|
||||
.iter()
|
||||
.map(|&[offset, len]| kept.iter().find_map(|(start, bytes)| entry(bytes, [offset.checked_sub(*start)?, len])).map(<[u8]>::to_vec))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +128,11 @@ fn unpack<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<Unpacked<'a>, Erro
|
||||
const JSON: &str = "the document's JSON";
|
||||
let too_long = |budget: &Budget| Error::LimitExceeded(format!("{JSON} is more than max_json ({} bytes)", budget.limits.max_json));
|
||||
if !is_compressed(bytes) {
|
||||
let (json, tail) = match bytes.iter().position(|&b| b == 0) {
|
||||
Some(nul) => {
|
||||
let (end, tail) = marker(bytes, nul)?;
|
||||
(bytes.get(..end).unwrap_or_default(), bytes.get(tail..).unwrap_or_default())
|
||||
}
|
||||
None => (bytes, &[][..]),
|
||||
let (json, tail) = if bytes.contains(&0) {
|
||||
let (end, tail) = marker(bytes)?;
|
||||
(bytes.get(..end).unwrap_or_default(), bytes.get(tail..).unwrap_or_default())
|
||||
} else {
|
||||
(bytes, &[][..])
|
||||
};
|
||||
budget.check_json(json.len() as u64, JSON)?;
|
||||
return Ok(Unpacked { json: Cow::Borrowed(json), tail: Tail::Slice(tail) });
|
||||
@@ -120,7 +157,7 @@ fn unpack<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<Unpacked<'a>, Erro
|
||||
if let Some(nul) = nul
|
||||
&& buf.len() >= nul + BLOB_MAGIC.len() - 1
|
||||
{
|
||||
let s = marker(&buf, nul)?;
|
||||
let s = marker(&buf)?;
|
||||
split = Some(s);
|
||||
// The data after the marker in this chunk is counted when it is read.
|
||||
used = s.1.saturating_sub(start).min(n);
|
||||
@@ -133,29 +170,245 @@ fn unpack<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<Unpacked<'a>, Erro
|
||||
let (json_len, tail_at) = match (split, nul) {
|
||||
(Some(s), _) => s,
|
||||
// The stream ended inside the marker.
|
||||
(None, Some(nul)) => marker(&buf, nul)?,
|
||||
(None, Some(_)) => marker(&buf)?,
|
||||
(None, None) => (buf.len(), buf.len()),
|
||||
};
|
||||
budget.check_json(json_len as u64, JSON)?;
|
||||
let read = if tail_at <= buf.len() { buf.split_off(tail_at) } else { vec![] };
|
||||
buf.truncate(json_len);
|
||||
Ok(Unpacked { json: Cow::Owned(buf), tail: Tail::Stream { dec, read, counted: 0 } })
|
||||
Ok(Unpacked { json: Cow::Owned(buf), tail: Tail::Stream { dec, read } })
|
||||
}
|
||||
|
||||
/// The format name, the version and the writer, read without the rest of the file, so that a file
|
||||
/// of a newer version still reports them.
|
||||
#[derive(Deserialize)]
|
||||
struct Stamp {
|
||||
/// A value of type `Self` that is a JSON array or object, read by [`ContainerVisitor`] as it is
|
||||
/// parsed; any other value has the wrong shape, apart from `null` where [`Container::null`] takes
|
||||
/// it.
|
||||
trait Container: Sized {
|
||||
fn seq<'de, A: SeqAccess<'de>>(seq: A) -> Result<Option<Self>, A::Error>;
|
||||
fn map<'de, A: MapAccess<'de>>(map: A) -> Result<Option<Self>, A::Error>;
|
||||
fn null() -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a [`Container`], or `None` when the value has the wrong shape.
|
||||
struct ContainerVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T: Container> Visitor<'de> for ContainerVisitor<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a JSON value")
|
||||
}
|
||||
|
||||
fn visit_bool<E: de::Error>(self, _: bool) -> Result<Option<T>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, _: i64) -> Result<Option<T>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, _: u64) -> Result<Option<T>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, _: f64) -> Result<Option<T>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, _: &str) -> Result<Option<T>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E: de::Error>(self) -> Result<Option<T>, E> {
|
||||
Ok(T::null())
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Option<T>, A::Error> {
|
||||
T::seq(seq)
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Option<T>, A::Error> {
|
||||
T::map(map)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets `slot` to `value`; `false` when `value` is `None` (the wrong shape) or `slot` is already
|
||||
/// set (a repeated member).
|
||||
fn fill<T>(slot: &mut Option<T>, value: Option<T>) -> bool {
|
||||
match value {
|
||||
Some(v) if slot.is_none() => {
|
||||
*slot = Some(v);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Skips the rest of `seq`; `true` when nothing was left.
|
||||
fn ended<'de, A: SeqAccess<'de>>(mut seq: A) -> Result<bool, A::Error> {
|
||||
let mut ended = true;
|
||||
while seq.next_element::<IgnoredAny>()?.is_some() {
|
||||
ended = false;
|
||||
}
|
||||
Ok(ended)
|
||||
}
|
||||
|
||||
/// [`Image::blob`]: `null`, or an array of two non-negative integers.
|
||||
struct Blob(Option<[u64; 2]>);
|
||||
|
||||
impl Container for Blob {
|
||||
fn seq<'de, A: SeqAccess<'de>>(mut seq: A) -> Result<Option<Blob>, A::Error> {
|
||||
let (mut range, mut n, mut ok) = ([0u64; 2], 0usize, true);
|
||||
while let Some(v) = seq.next_element::<Scalar>()? {
|
||||
match (range.get_mut(n), v.unsigned()) {
|
||||
(Some(slot), Some(v)) => *slot = v,
|
||||
_ => ok = false,
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
Ok((ok && n == 2).then_some(Blob(Some(range))))
|
||||
}
|
||||
|
||||
fn map<'de, A: MapAccess<'de>>(mut map: A) -> Result<Option<Blob>, A::Error> {
|
||||
while map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn null() -> Option<Blob> {
|
||||
Some(Blob(None))
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Image`] read as its derived `Deserialize` reads it, from a JSON object or from an array of
|
||||
/// its fields in order, without keeping a value of the wrong shape: an unknown member is skipped,
|
||||
/// and a missing `mime`, a repeated member or a member of the wrong type gives `None`.
|
||||
impl Container for Image {
|
||||
fn seq<'de, A: SeqAccess<'de>>(mut seq: A) -> Result<Option<Image>, A::Error> {
|
||||
let mut image = Image::default();
|
||||
let Some(mime) = seq.next_element::<Scalar>()? else { return Ok(None) };
|
||||
let mut ok = mime.string().map(|m| image.mime = m).is_some();
|
||||
let Some(data) = seq.next_element::<Scalar>()? else { return Ok(ok.then_some(image)) };
|
||||
ok &= data.string().map(|d| image.data = d).is_some();
|
||||
let Some(blob) = seq.next_element_seed(Contained::<Blob>(PhantomData))? else { return Ok(ok.then_some(image)) };
|
||||
ok &= blob.map(|b| image.blob = b.0).is_some();
|
||||
let Some(proxy) = seq.next_element::<Scalar>()? else { return Ok(ok.then_some(image)) };
|
||||
ok &= proxy.bool().map(|p| image.proxy = p).is_some();
|
||||
ok &= ended(seq)?;
|
||||
Ok(ok.then_some(image))
|
||||
}
|
||||
|
||||
fn map<'de, A: MapAccess<'de>>(mut map: A) -> Result<Option<Image>, A::Error> {
|
||||
let (mut mime, mut data, mut blob, mut proxy) = (None, None, None, None);
|
||||
let mut ok = true;
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
ok &= match key.as_str() {
|
||||
"mime" => fill(&mut mime, map.next_value::<Scalar>()?.string()),
|
||||
"data" => fill(&mut data, map.next_value::<Scalar>()?.string()),
|
||||
"blob" => fill(&mut blob, map.next_value_seed(Contained::<Blob>(PhantomData))?),
|
||||
"proxy" => fill(&mut proxy, map.next_value::<Scalar>()?.bool()),
|
||||
_ => {
|
||||
map.next_value::<IgnoredAny>()?;
|
||||
true
|
||||
}
|
||||
};
|
||||
}
|
||||
let (Some(mime), true) = (mime, ok) else { return Ok(None) };
|
||||
let mut image = Image::default();
|
||||
image.mime = mime;
|
||||
image.data = data.unwrap_or_default();
|
||||
image.blob = blob.and_then(|b| b.0);
|
||||
image.proxy = proxy.unwrap_or_default();
|
||||
Ok(Some(image))
|
||||
}
|
||||
}
|
||||
|
||||
impl Shaped for Image {
|
||||
fn read<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Image>, D::Error> {
|
||||
d.deserialize_any(ContainerVisitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a [`Container`] as an element or member value.
|
||||
struct Contained<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T: Container> de::DeserializeSeed<'de> for Contained<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Option<T>, D::Error> {
|
||||
d.deserialize_any(ContainerVisitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
/// The format name, the version, the writer and, when `IMAGES` is true, the preview and the PDF,
|
||||
/// read without the rest of the file, so that a file of a newer version still reports them. The
|
||||
/// members are read as [`craft_vectorcraft_format::File::read`] reads them: a missing or repeated
|
||||
/// `format` or `version` is refused, and a `generator`, `preview` or `pdf` of the wrong shape
|
||||
/// reads as `None` and a repeated one as its last value. When `IMAGES` is true, a missing or
|
||||
/// repeated `document` is refused too; its value is skipped.
|
||||
struct Stamp<const IMAGES: bool> {
|
||||
format: String,
|
||||
version: u32,
|
||||
#[serde(default)]
|
||||
generator: Option<String>,
|
||||
preview: Option<Image>,
|
||||
pdf: Option<Image>,
|
||||
}
|
||||
|
||||
/// Reads the value of member `name` into `slot`, refusing a second one.
|
||||
fn once<'de, A: MapAccess<'de>, T: Deserialize<'de>>(map: &mut A, slot: &mut Option<T>, name: &'static str) -> Result<(), A::Error> {
|
||||
if slot.is_some() {
|
||||
return Err(de::Error::duplicate_field(name));
|
||||
}
|
||||
*slot = Some(map.next_value()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
struct StampVisitor<const IMAGES: bool>;
|
||||
impl<'de, const IMAGES: bool> Visitor<'de> for StampVisitor<IMAGES> {
|
||||
type Value = Stamp<IMAGES>;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a VectorCraft file's JSON object")
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Stamp<IMAGES>, A::Error> {
|
||||
let (mut format, mut version, mut generator, mut preview, mut pdf) = (None, None, None, None, None);
|
||||
let mut document: Option<IgnoredAny> = None;
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
match key.as_str() {
|
||||
"format" => once(&mut map, &mut format, "format")?,
|
||||
"version" => once(&mut map, &mut version, "version")?,
|
||||
"document" if IMAGES => once(&mut map, &mut document, "document")?,
|
||||
"generator" => generator = map.next_value::<Lenient<String>>()?.0,
|
||||
"preview" if IMAGES => preview = map.next_value::<Lenient<Image>>()?.0,
|
||||
"pdf" if IMAGES => pdf = map.next_value::<Lenient<Image>>()?.0,
|
||||
_ => {
|
||||
map.next_value::<IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if IMAGES && document.is_none() {
|
||||
return Err(de::Error::missing_field("document"));
|
||||
}
|
||||
Ok(Stamp {
|
||||
format: format.ok_or_else(|| de::Error::missing_field("format"))?,
|
||||
version: version.ok_or_else(|| de::Error::missing_field("version"))?,
|
||||
generator,
|
||||
preview,
|
||||
pdf,
|
||||
})
|
||||
}
|
||||
}
|
||||
d.deserialize_map(StampVisitor::<IMAGES>)
|
||||
}
|
||||
}
|
||||
|
||||
/// The file's [`Stamp`], refused when the format is not VectorCraft's, and when the version is
|
||||
/// newer than this crate reads unless `newer` is true.
|
||||
fn stamp(json: &[u8], budget: &Budget, newer: bool) -> Result<Stamp, Error> {
|
||||
let s: Stamp = if newer { parse(json, budget.limits)? } else { parse_versioned(json, budget.limits, Kind::VectorCraft)? };
|
||||
fn stamp<const IMAGES: bool>(json: &[u8], budget: &Budget, newer: bool) -> Result<Stamp<IMAGES>, Error> {
|
||||
let s: Stamp<IMAGES> = if newer { parse(json, budget.limits)? } else { parse_versioned(json, budget.limits, Kind::VectorCraft)? };
|
||||
match craft_vectorcraft_format::check(&s.format, s.version) {
|
||||
Ok(()) => Ok(s),
|
||||
Err(craft_vectorcraft_format::Error::Format(format)) => Err(malformed(format!("format is `{format}`"))),
|
||||
@@ -167,44 +420,38 @@ fn stamp(json: &[u8], budget: &Budget, newer: bool) -> Result<Stamp, Error> {
|
||||
|
||||
pub(crate) fn info(bytes: &[u8], budget: &mut Budget) -> Result<(u32, Option<String>), Error> {
|
||||
let json = unpack(bytes, budget)?.json;
|
||||
let s = stamp(&json, budget, true)?;
|
||||
let s = stamp::<false>(&json, budget, true)?;
|
||||
Ok((s.version, s.generator))
|
||||
}
|
||||
|
||||
/// Check the entry's size against `max_entry` and count it; for an entry after the JSON, the
|
||||
/// offset where it ends.
|
||||
fn take(image: &Image, budget: &mut Budget, what: &str) -> Result<Option<u64>, Error> {
|
||||
budget.take_entry(image.len(), what)?;
|
||||
Ok(image.blob.map(|[offset, len]| offset.saturating_add(len)))
|
||||
}
|
||||
|
||||
/// The entry's bytes, after [`take`] has checked its size.
|
||||
fn entry_bytes(image: &Image, tail: &[u8], what: &str) -> Result<Vec<u8>, Error> {
|
||||
image.bytes_at_most(tail, u64::MAX).ok_or_else(|| match image.blob {
|
||||
Some(_) => malformed(format!("the file ends before its {what}")),
|
||||
None => malformed(format!("the {what} is not valid base64")),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn renditions(bytes: &[u8], budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
let Unpacked { json, mut tail } = unpack(bytes, budget)?;
|
||||
stamp(&json, budget, false)?;
|
||||
let head: Head = parse(&json, budget.limits)?;
|
||||
let Unpacked { json, tail } = unpack(bytes, budget)?;
|
||||
let head = stamp::<true>(&json, budget, false)?;
|
||||
// A member without a MIME type gives no rendition: a preview may be PNG or JPEG.
|
||||
let members: Vec<_> = [(RenditionKind::Thumbnail, head.preview, "preview"), (RenditionKind::Pdf, head.pdf, "PDF")]
|
||||
.into_iter()
|
||||
.filter_map(|(kind, m, what)| Some((kind, m.filter(|m| !m.mime.is_empty())?, what)))
|
||||
.collect();
|
||||
// Every size is checked before any data after the JSON is decompressed.
|
||||
let mut end = 0u64;
|
||||
// Every size is checked before any data after the JSON is decompressed. A member after the
|
||||
// JSON of a compressed file is counted against max_total as it is decompressed; any other
|
||||
// member is counted here.
|
||||
let compressed = matches!(tail, Tail::Stream { .. });
|
||||
for (_, m, what) in &members {
|
||||
end = end.max(take(m, budget, what)?.unwrap_or(0));
|
||||
if compressed && m.blob.is_some() {
|
||||
budget.check_entry(m.len(), what)?;
|
||||
} else {
|
||||
budget.take_entry(m.len(), what)?;
|
||||
}
|
||||
}
|
||||
let tail = if end > 0 { tail.up_to(end, budget)? } else { &[] };
|
||||
let ranges: Vec<[u64; 2]> = members.iter().filter_map(|(_, m, _)| m.blob).collect();
|
||||
let mut blobs = if ranges.is_empty() { vec![] } else { tail.ranges(&ranges, budget)? }.into_iter();
|
||||
members
|
||||
.into_iter()
|
||||
.map(|(kind, m, what)| {
|
||||
let bytes = entry_bytes(&m, tail, what)?;
|
||||
let bytes = match m.blob {
|
||||
Some(_) => blobs.next().flatten().ok_or_else(|| malformed(format!("the file ends before its {what}")))?,
|
||||
None => m.bytes_at_most(&[], u64::MAX).ok_or_else(|| malformed(format!("the {what} is not valid base64")))?,
|
||||
};
|
||||
Ok(Rendition { kind, mime: m.mime, bytes })
|
||||
})
|
||||
.collect()
|
||||
@@ -269,7 +516,7 @@ struct KindM {
|
||||
|
||||
pub(crate) fn outline(bytes: &[u8], budget: &mut Budget) -> Result<Outline, Error> {
|
||||
let json = unpack(bytes, budget)?.json;
|
||||
stamp(&json, budget, false)?;
|
||||
stamp::<false>(&json, budget, false)?;
|
||||
let f: WithDocument = parse(&json, budget.limits)?;
|
||||
let pages = f
|
||||
.document
|
||||
@@ -300,3 +547,73 @@ pub(crate) fn outline(bytes: &[u8], budget: &mut Budget) -> Result<Outline, Erro
|
||||
.collect();
|
||||
Ok(Outline { canvas: None, pages, layers })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{unread_array, unread_object};
|
||||
use serde::de::value::{Error as ValueError, MapDeserializer, SeqDeserializer, StrDeserializer};
|
||||
|
||||
#[test]
|
||||
fn an_image_of_the_wrong_shape_is_skipped_without_reading_it_whole() {
|
||||
assert!(Lenient::<Image>::deserialize(unread_object(1000)).unwrap().0.is_none());
|
||||
// A `mime` that is an array, in the array form and in an object, and a `blob` that is an
|
||||
// object.
|
||||
let array_form = SeqDeserializer::<_, ValueError>::new([unread_array(1000), unread_array(1000)].into_iter());
|
||||
assert!(Lenient::<Image>::deserialize(array_form).unwrap().0.is_none());
|
||||
let mime_array = MapDeserializer::<_, ValueError>::new([("mime", unread_array(1000))].into_iter());
|
||||
assert!(Lenient::<Image>::deserialize(mime_array).unwrap().0.is_none());
|
||||
let blob_object = MapDeserializer::<_, ValueError>::new([("blob", unread_object(1000))].into_iter());
|
||||
assert!(Lenient::<Image>::deserialize(blob_object).unwrap().0.is_none());
|
||||
let fine: Lenient<Image> =
|
||||
Lenient::deserialize(MapDeserializer::<_, ValueError>::new([("mime", StrDeserializer::<ValueError>::new("image/png"))].into_iter()))
|
||||
.unwrap();
|
||||
assert_eq!(fine.0.map(|i| i.mime).as_deref(), Some("image/png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preview_and_pdf_read_as_file_read_reads_them() {
|
||||
let shapes = [
|
||||
r#"{"mime":"image/png","data":"AAAA"}"#,
|
||||
r#"{"mime":"image/png","data":"AAAA","extra":[1,{"a":2}]}"#,
|
||||
r#"{"mime":"image/png","blob":[1,2],"proxy":true}"#,
|
||||
r#"{"mime":"image/png","blob":null}"#,
|
||||
r#"{"mime":"","data":""}"#,
|
||||
r#"{"data":"AAAA"}"#,
|
||||
r#"{"mime":7}"#,
|
||||
r#"{"mime":"image/png","data":null}"#,
|
||||
r#"{"mime":"image/png","proxy":1}"#,
|
||||
r#"{"mime":"image/png","proxy":null}"#,
|
||||
r#"{"mime":"image/png","blob":[1,2,3]}"#,
|
||||
r#"{"mime":"image/png","blob":[1]}"#,
|
||||
r#"{"mime":"image/png","blob":[-1,2]}"#,
|
||||
r#"{"mime":"image/png","blob":[1.0,2]}"#,
|
||||
r#"{"mime":"image/png","blob":[1,18446744073709551616]}"#,
|
||||
r#"{"mime":"image/png","blob":{"0":1}}"#,
|
||||
r#"{"mime":"image/png","blob":"1,2"}"#,
|
||||
r#"{"mime":"image/png","mime":"image/jpeg"}"#,
|
||||
r#"{"mime":"image/png","blob":[1,2],"blob":[1,2]}"#,
|
||||
r#"["image/png"]"#,
|
||||
r#"["image/png","AAAA"]"#,
|
||||
r#"["image/png","",[3,4]]"#,
|
||||
r#"["image/png","",null,true]"#,
|
||||
r#"["image/png","",null,true,0]"#,
|
||||
r#"["image/png",[0,0]]"#,
|
||||
r#"[7]"#,
|
||||
r#"[]"#,
|
||||
r#"[[0,0,0]]"#,
|
||||
r#""image/png""#,
|
||||
r#"7"#,
|
||||
r#"-7"#,
|
||||
r#"1.5"#,
|
||||
r#"true"#,
|
||||
r#"null"#,
|
||||
];
|
||||
for shape in shapes {
|
||||
let json = format!(r#"{{"format":"vectorcraft","version":4,"generator":{shape},"preview":{shape},"pdf":{shape},"document":0}}"#);
|
||||
let file = craft_vectorcraft_format::File::<IgnoredAny>::read(json.as_bytes()).unwrap();
|
||||
let stamp: Stamp<true> = crate::parse(json.as_bytes(), &crate::Limits::default()).unwrap();
|
||||
assert_eq!((&stamp.generator, &stamp.preview, &stamp.pdf), (&file.generator, &file.preview, &file.pdf), "{shape}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,82 @@ fn a_pcraft_manifest_with_a_repeated_member_or_a_late_newer_version_is_refused()
|
||||
assert_eq!(outline(&bundle, &limits()), Err(refused));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pcraft_manifest_that_is_a_json_array_is_malformed() {
|
||||
// The members of a manifest in field order; read_manifest reads only a JSON object.
|
||||
let array = r#"[1,"photocraft-format 0.1.0",{"size":{"width":1,"height":1},"resolution_dpi":72.0,"mode":"Rgb","depth":"U8","layers":[]},"thumb.png",null]"#;
|
||||
let format_limits = craft_pcraft_format::Limits::default();
|
||||
assert!(craft_pcraft_format::read_manifest::<serde_json::Value, craft_pcraft_format::Error>(array.as_bytes(), &format_limits, &[], 1).is_err());
|
||||
let bundle = zip(&[store("manifest.json", array.as_bytes()), store("thumb.png", &png(1, 1, 0))]);
|
||||
assert!(matches!(info(&bundle, &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(renditions(&bundle, &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(outline(&bundle, &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_newer_file_with_another_format_s_version_member_is_too_new() {
|
||||
// `version` and `schema` are not PhotoCraft's version member, nor `schema` VectorCraft's.
|
||||
let manifest = r#"{"format_version":2,"version":1,"schema":1,"generator":"photocraft-format 9","document":"elsewhere"}"#;
|
||||
let pc = zip(&[store("manifest.json", manifest.as_bytes())]);
|
||||
// A repeated `format` makes the stamp unreadable; the version still decides.
|
||||
let vc = vectorcraft_json(5, r#""schema":1,"format":"vectorcraft""#);
|
||||
for (bytes, kind, version) in [(pc, Kind::PhotoCraft, 2), (vc.into_bytes(), Kind::VectorCraft, 5)] {
|
||||
let refused = Error::TooNew { kind, version, supported: kind.supported_version() };
|
||||
assert_eq!(renditions(&bytes, &limits()), Err(refused.clone()), "{kind:?}");
|
||||
assert_eq!(outline(&bytes, &limits()), Err(refused), "{kind:?}");
|
||||
}
|
||||
// Bytes after the JSON make a newer file malformed, not too new.
|
||||
let trailing = format!("{}x", vectorcraft_json(5, ""));
|
||||
assert!(matches!(renditions(trailing.as_bytes(), &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_generator_that_is_not_a_string_reads_as_none() {
|
||||
let vc = vectorcraft_json(4, "").replacen(r#""generator":"VectorCraft 0.1.0""#, r#""generator":{"name":"VectorCraft"}"#, 1);
|
||||
assert_eq!(info(vc.as_bytes(), &limits()).unwrap().generator, None);
|
||||
assert_eq!(outline(vc.as_bytes(), &limits()).unwrap().pages.len(), 2);
|
||||
let file = craft_vectorcraft_format::File::<serde_json::Value>::read(vc.as_bytes()).unwrap();
|
||||
assert_eq!(file.generator, None);
|
||||
// read_manifest needs a string, so the other calls refuse it; migrate reads it as none.
|
||||
let manifest = pcraft_manifest(1, "[]", "").replacen(r#""generator":"photocraft-format 0.1.0""#, r#""generator":7"#, 1);
|
||||
let bundle = zip(&[store("manifest.json", manifest.as_bytes())]);
|
||||
assert_eq!(info(&bundle, &limits()).unwrap().generator, None);
|
||||
assert!(matches!(renditions(&bundle, &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repeated_vectorcraft_member_reads_as_its_last_value() {
|
||||
let (first, last) = (png(1, 1, 0), png(2, 2, 9));
|
||||
let members = format!(r#""generator":"VectorCraft 0.2.0","preview":{},"preview":{}"#, inline("image/png", &first), inline("image/png", &last));
|
||||
let vc = vectorcraft_json(4, &members);
|
||||
let file = craft_vectorcraft_format::File::<serde_json::Value>::read(vc.as_bytes()).unwrap();
|
||||
assert_eq!(file.preview.unwrap().bytes(&[]).unwrap(), last);
|
||||
let r = renditions(vc.as_bytes(), &limits()).unwrap();
|
||||
assert_eq!((r.len(), &r[0].bytes), (1, &last));
|
||||
assert_eq!(info(vc.as_bytes(), &limits()).unwrap().generator.as_deref(), Some("VectorCraft 0.2.0"));
|
||||
// A repeated version is refused, as File::read refuses it.
|
||||
let twice = vectorcraft_json(4, r#""version":4"#);
|
||||
assert!(craft_vectorcraft_format::File::<serde_json::Value>::read(twice.as_bytes()).is_err());
|
||||
assert!(matches!(info(twice.as_bytes(), &limits()), Err(Error::Malformed(_))));
|
||||
// A repeated version is malformed, not too new, when the second one is newer.
|
||||
let newer = vectorcraft_json(4, r#""version":9"#);
|
||||
assert!(craft_vectorcraft_format::File::<serde_json::Value>::read(newer.as_bytes()).is_err());
|
||||
assert!(matches!(renditions(newer.as_bytes(), &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(outline(newer.as_bytes(), &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vectorcraft_file_without_one_document_gives_no_renditions() {
|
||||
let preview = vectorcraft_json(4, &format!(r#""preview":{}"#, inline("image/png", &png(1, 1, 0))));
|
||||
let missing = preview.replacen(r#""document":"#, r#""elsewhere":"#, 1);
|
||||
let repeated = preview.replacen(r#""images":{}"#, r#""document":{}"#, 1);
|
||||
for file in [missing, repeated] {
|
||||
assert!(craft_vectorcraft_format::File::<serde_json::Value>::read(file.as_bytes()).is_err());
|
||||
assert!(matches!(renditions(file.as_bytes(), &limits()), Err(Error::Malformed(_))), "{file}");
|
||||
}
|
||||
assert_eq!(renditions(preview.as_bytes(), &limits()).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vectorcraft_member_without_a_mime_type_is_skipped() {
|
||||
let file = vectorcraft_json(4, &format!(r#""preview":{{"data":"{}"}}"#, base64(&png(1, 1, 0))));
|
||||
|
||||
@@ -58,6 +58,34 @@ fn sniff_refuses_other_files() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniff_reads_the_names_in_the_central_directory_of_a_whole_archive() {
|
||||
let mut pc = zip(&[store("manifest.json", pcraft_manifest(1, "[]", "").as_bytes())]);
|
||||
// The local header still names manifest.json; the central directory names Xanifest.json.
|
||||
let c = pc.windows(4).rposition(|w| w == [0x50, 0x4b, 0x01, 0x02]).unwrap();
|
||||
pc[c + 46] = b'X';
|
||||
assert_eq!(sniff(&pc), None);
|
||||
assert_eq!(info(&pc, &limits()), Err(Error::Unrecognized));
|
||||
// A head too short to reach the central directory is read through its local headers.
|
||||
assert_eq!(sniff(pc.get(..60).unwrap()), Some(Kind::PhotoCraft));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zip_whose_manifest_has_no_format_version_is_not_photocraft() {
|
||||
// A browser extension: a ZIP archive with a manifest.json of its own.
|
||||
let ext = zip(&[store("manifest.json", br#"{"manifest_version":3,"name":"Example","version":"1.0"}"#)]);
|
||||
assert_eq!(sniff(&ext), Some(Kind::PhotoCraft));
|
||||
assert_eq!(info(&ext, &limits()), Err(Error::Unrecognized));
|
||||
assert_eq!(renditions(&ext, &limits()), Err(Error::Unrecognized));
|
||||
assert_eq!(outline(&ext, &limits()), Err(Error::Unrecognized));
|
||||
// A format_version that is present and not a number is a damaged manifest.
|
||||
for version in ["null", r#""1""#] {
|
||||
let damaged = zip(&[store("manifest.json", pcraft_manifest(1, "[]", "").replacen("1", version, 1).as_bytes())]);
|
||||
assert!(matches!(info(&damaged, &limits()), Err(Error::Malformed(_))), "{version}");
|
||||
assert!(matches!(outline(&damaged, &limits()), Err(Error::Malformed(_))), "{version}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_reads_version_and_writer() {
|
||||
let i = info(vectorcraft_json(3, "").as_bytes(), &limits()).unwrap();
|
||||
@@ -133,6 +161,43 @@ fn vectorcraft_members_after_the_json() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compressed_members_after_the_json_read_their_own_ranges() {
|
||||
// 100,000 bytes the document uses, then the preview and the PDF; then the same bytes named by
|
||||
// a PDF member that overlaps the preview.
|
||||
let image = vec![0xCD; 100_000];
|
||||
let preview = png(5, 5, 2);
|
||||
let mut tail = image.clone();
|
||||
tail.extend(&preview);
|
||||
tail.extend(PDF);
|
||||
let at = image.len();
|
||||
for (pdf_at, pdf_len, pdf) in [(at + preview.len(), PDF.len(), PDF.to_vec()), (at, preview.len() + PDF.len(), [&preview[..], PDF].concat())] {
|
||||
let members = format!(r#""preview":{},"pdf":{}"#, blob("image/png", at, preview.len()), blob("application/pdf", pdf_at, pdf_len));
|
||||
let mut file = vectorcraft_json(4, &members).into_bytes();
|
||||
file.extend(BLOB_MAGIC);
|
||||
file.extend(&tail);
|
||||
for bytes in [file.clone(), gzip(&file)] {
|
||||
let r = renditions(&bytes, &limits()).unwrap();
|
||||
assert_eq!((&r[0].bytes, &r[1].bytes), (&preview, &pdf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_marker_follows_the_json_directly() {
|
||||
// A space between the JSON and the marker: the format crate does not split the file there.
|
||||
let mut file = vectorcraft_json(4, &format!(r#""pdf":{}"#, blob("application/pdf", 0, PDF.len()))).into_bytes();
|
||||
file.push(b' ');
|
||||
file.extend(BLOB_MAGIC);
|
||||
file.extend(PDF);
|
||||
assert!(craft_vectorcraft_format::split_tail(&file).1.is_empty());
|
||||
for bytes in [file.clone(), gzip(&file)] {
|
||||
assert!(matches!(info(&bytes, &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(renditions(&bytes, &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(outline(&bytes, &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compressed_marker_across_the_reader_s_64_kib_chunks() {
|
||||
// The marker starts at every offset around the first 64 KiB boundary of the unpacked stream.
|
||||
@@ -213,21 +278,29 @@ fn pcraft_outline_has_the_canvas_and_layer_tree() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcraft_groups_nest_to_the_depth_cap() {
|
||||
// 3 levels to the layer list, 3 per group and 4 for the leaf: 397 of the 400 levels.
|
||||
let mut layer = pcraft_layer("Leaf", true, "Normal", 1.0, raster());
|
||||
for i in 0..130 {
|
||||
layer = pcraft_layer(&format!("G{i}"), true, "Normal", 1.0, &format!(r#"{{"kind":"group","expanded":false,"children":[{layer}]}}"#));
|
||||
}
|
||||
let pc = zip(&[deflated("manifest.json", pcraft_manifest(1, &format!("[{layer}]"), "").as_bytes())]);
|
||||
let o = outline(&pc, &limits()).unwrap();
|
||||
fn pcraft_groups_nest_to_photocraft_s_depth_limit() {
|
||||
// 3 levels to the layer list, 3 per group and 4 for the leaf: 367 levels for 120 groups,
|
||||
// PhotoCraft's limit (`manifest_depth(MAX_GROUP_DEPTH)`).
|
||||
let nested = |groups: usize| {
|
||||
let mut layer = pcraft_layer("Leaf", true, "Normal", 1.0, raster());
|
||||
for i in 0..groups {
|
||||
layer = pcraft_layer(&format!("G{i}"), true, "Normal", 1.0, &format!(r#"{{"kind":"group","expanded":false,"children":[{layer}]}}"#));
|
||||
}
|
||||
zip(&[deflated("manifest.json", pcraft_manifest(1, &format!("[{layer}]"), "").as_bytes())])
|
||||
};
|
||||
let o = outline(&nested(120), &limits()).unwrap();
|
||||
let mut depth = 0;
|
||||
let mut l = &o.layers[0];
|
||||
while let Some(c) = l.children.first() {
|
||||
depth += 1;
|
||||
l = c;
|
||||
}
|
||||
assert_eq!((depth, l.name.as_str()), (130, "Leaf"));
|
||||
assert_eq!((depth, l.name.as_str()), (120, "Leaf"));
|
||||
// One more group is past the limit, though within MAX_DEPTH.
|
||||
let deeper = nested(121);
|
||||
assert!(matches!(outline(&deeper, &limits()), Err(Error::LimitExceeded(_))));
|
||||
assert!(matches!(renditions(&deeper, &limits()), Err(Error::LimitExceeded(_))));
|
||||
assert_eq!(info(&deeper, &limits()).unwrap().version, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -115,6 +115,25 @@ fn a_compressed_member_past_max_entry_is_refused_before_the_tail_is_inflated() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compressed_member_whose_offset_passes_max_total_is_refused_before_the_tail_is_inflated() {
|
||||
// A 4-byte preview stated 200 MiB past the marker, in a file whose data after the JSON is
|
||||
// 1 MiB.
|
||||
let mut file = vectorcraft_json(4, &format!(r#""preview":{}"#, blob("image/png", 200 << 20, 4))).into_bytes();
|
||||
file.extend(BLOB_MAGIC);
|
||||
file.extend(vec![0u8; 1 << 20]);
|
||||
let gz = gzip(&file);
|
||||
let mut limits = small();
|
||||
limits.max_total = 64 << 20;
|
||||
match renditions(&gz, &limits) {
|
||||
Err(Error::LimitExceeded(m)) => assert!(m.contains("max_total"), "{m}"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// Within max_total, the stream is read as far as it goes, and it ends first.
|
||||
limits.max_total = 256 << 20;
|
||||
assert!(matches!(renditions(&gz, &limits), Err(Error::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nul_outside_the_marker_is_malformed_without_reading_on() {
|
||||
// A NUL where the marker is not, then 8 MiB that compress to almost nothing.
|
||||
@@ -225,6 +244,47 @@ fn only_the_json_of_a_compressed_file_is_counted_when_no_member_follows_it() {
|
||||
assert_eq!(renditions(&gz, &limits).unwrap(), vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compressed_member_is_counted_against_max_total_once() {
|
||||
// A 40 KiB PDF after the JSON, with max_total 64 KiB: it reads compressed as it does
|
||||
// uncompressed.
|
||||
let pdf: Vec<u8> = (0..40u32 << 10).map(|i| (i % 251) as u8).collect();
|
||||
let mut file = vectorcraft_json(4, &format!(r#""pdf":{}"#, blob("application/pdf", 0, pdf.len()))).into_bytes();
|
||||
file.extend(BLOB_MAGIC);
|
||||
file.extend(&pdf);
|
||||
let mut limits = small();
|
||||
limits.max_total = 64 << 10;
|
||||
for bytes in [file.clone(), gzip(&file)] {
|
||||
assert_eq!(renditions(&bytes, &limits).unwrap()[0].bytes, pdf);
|
||||
}
|
||||
// Without room for the JSON and the PDF, the compressed file is refused.
|
||||
limits.max_total = pdf.len() as u64;
|
||||
assert!(limit_exceeded(renditions(&gzip(&file), &limits)));
|
||||
// A preview naming the same bytes is a second copy, counted too.
|
||||
let mut twice = vectorcraft_json(4, &format!(r#""preview":{0},"pdf":{0}"#, blob("image/png", 0, pdf.len()))).into_bytes();
|
||||
twice.extend(BLOB_MAGIC);
|
||||
twice.extend(&pdf);
|
||||
for bytes in [twice.clone(), gzip(&twice)] {
|
||||
limits.max_total = 64 << 10;
|
||||
assert!(limit_exceeded(renditions(&bytes, &limits)));
|
||||
limits.max_total = 128 << 10;
|
||||
assert_eq!(renditions(&bytes, &limits).unwrap().len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_array_in_place_of_the_generator_or_a_preview_is_skipped() {
|
||||
// A million elements, where a buffered read would keep each one.
|
||||
let zeros = format!("[{}0]", "0,".repeat(1 << 20));
|
||||
let members = format!(r#""generator":{zeros},"preview":{{"mime":{zeros}}},"pdf":[{zeros}]"#);
|
||||
let vc = vectorcraft_json(4, &members).replacen(r#""generator":"VectorCraft 0.1.0","#, "", 1);
|
||||
assert_eq!(info(vc.as_bytes(), &Limits::default()).unwrap().generator, None);
|
||||
assert_eq!(renditions(vc.as_bytes(), &Limits::default()).unwrap(), vec![]);
|
||||
let manifest = pcraft_manifest(1, "[]", "").replacen(r#""generator":"photocraft-format 0.1.0""#, &format!(r#""generator":{zeros}"#), 1);
|
||||
let pc = zip(&[store("manifest.json", manifest.as_bytes())]);
|
||||
assert_eq!(info(&pc, &Limits::default()).unwrap().generator, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zip_entry_past_max_entry_is_refused_before_it_is_inflated() {
|
||||
let big = vec![0u8; 1 << 20];
|
||||
|
||||
Reference in new issue
Block a user