mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
craft-doc: skip wrong-shaped members without buffering them
- A VectorCraft or PhotoCraft `generator`, and a VectorCraft `preview` or `pdf`, are read as they are parsed. A value of the wrong shape is skipped element by element with `IgnoredAny` and none of it is kept; before, serde's untagged enum buffered the whole value first, with no limit on that buffer. The preview and PDF still read as `File::read` reads them, the array form of `Image` included. - A rendition after the JSON of a gzip file is counted against `max_total` once, as it is decompressed. A 40 KiB PDF with `max_total` at 64 KiB now reads compressed as it does uncompressed. A preview and PDF that name the same bytes are counted as two copies. - A repeated VectorCraft `version` is malformed, not too new, and `renditions` refuses a missing or repeated `document`, as `File::read` does. - The README says that a VectorCraft file written as a JSON array is not recognized by `sniff`.
This commit is contained in:
1 parent
86b48935cf
commit
5b1f01d036
5 files changed
+490
-26
No files matched your search
+10
-4
@@ -35,7 +35,9 @@ 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. When `head` holds a whole ZIP archive, the
|
||||
names in its central directory decide.
|
||||
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.
|
||||
@@ -74,7 +76,8 @@ 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
|
||||
@@ -108,13 +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
|
||||
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.
|
||||
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.
|
||||
+165
-17
@@ -128,7 +128,8 @@ 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`]. [`renditions`] and [`outline`] also refuse a
|
||||
@@ -330,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)
|
||||
}
|
||||
|
||||
@@ -502,7 +509,7 @@ fn version_member(kind: Kind) -> &'static str {
|
||||
}
|
||||
|
||||
/// 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 one reads as its last value.
|
||||
/// the other members are skipped, and a repeated version member is refused.
|
||||
struct VersionIn(&'static str);
|
||||
|
||||
impl<'de> Visitor<'de> for VersionIn {
|
||||
@@ -516,6 +523,9 @@ impl<'de> Visitor<'de> for VersionIn {
|
||||
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>()?;
|
||||
@@ -544,29 +554,119 @@ pub(crate) fn parse_versioned<T: DeserializeOwned>(json: &[u8], limits: &Limits,
|
||||
}
|
||||
|
||||
/// A member read as `None` when its value has the wrong shape, as the format crates read a
|
||||
/// file's optional members.
|
||||
/// 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>);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Lenient<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> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Shape<T> {
|
||||
Value(T),
|
||||
Other(de::IgnoredAny),
|
||||
}
|
||||
Ok(Lenient(match Shape::deserialize(d)? {
|
||||
Shape::Value(v) => Some(v),
|
||||
Shape::Other(_) => None,
|
||||
}))
|
||||
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: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
|
||||
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() }
|
||||
}
|
||||
@@ -592,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"));
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,16 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
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, Visitor};
|
||||
use serde::de::{self, Deserializer, IgnoredAny, MapAccess, SeqAccess, Visitor};
|
||||
|
||||
use crate::{Budget, Error, Kind, Layer, Lenient, 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;
|
||||
@@ -88,6 +91,11 @@ impl Tail<'_> {
|
||||
_ => 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 {
|
||||
@@ -171,11 +179,173 @@ fn unpack<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<Unpacked<'a>, Erro
|
||||
Ok(Unpacked { json: Cow::Owned(buf), tail: Tail::Stream { dec, read } })
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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,
|
||||
@@ -205,10 +375,12 @@ impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
|
||||
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,
|
||||
@@ -217,6 +389,9 @@ impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
}
|
||||
}
|
||||
}
|
||||
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"))?,
|
||||
@@ -257,9 +432,16 @@ pub(crate) fn renditions(bytes: &[u8], budget: &mut Budget) -> Result<Vec<Rendit
|
||||
.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.
|
||||
// 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 {
|
||||
budget.take_entry(m.len(), what)?;
|
||||
if compressed && m.blob.is_some() {
|
||||
budget.check_entry(m.len(), what)?;
|
||||
} else {
|
||||
budget.take_entry(m.len(), what)?;
|
||||
}
|
||||
}
|
||||
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();
|
||||
@@ -365,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,23 @@ fn a_repeated_vectorcraft_member_reads_as_its_last_value() {
|
||||
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]
|
||||
|
||||
@@ -244,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