mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 04:52:13 +00:00
Merge pull request #546 from dotCooCoo/feat/doc-selected-rendition
craft-doc: read a selected rendition without extracting other payloads
This commit is contained in:
5 files changed
+261
-27
No files matched your search
@@ -43,6 +43,15 @@ let outline = craft_doc::outline(&bytes, &limits)?;
|
||||
reads.
|
||||
- `renditions(bytes, &Limits) -> Result<Vec<Rendition>, Error>`: each `Rendition` has a `kind`
|
||||
(`Pdf`, `Thumbnail`, `Composite`), a `mime` and the `bytes`.
|
||||
- `rendition(bytes, RenditionKind, &Limits) -> Result<Option<Rendition>, Error>`: the requested
|
||||
kind only, or `None` when absent. Container metadata and JSON limits are still checked. Other
|
||||
rendition payloads are not decoded or copied: a damaged preview does not prevent reading the
|
||||
PDF, and a missing thumbnail ZIP entry does not prevent reading the composite. A compressed
|
||||
member's offset still counts against `max_total`, including bytes traversed to reach it.
|
||||
Normal gzip reads remain buffered. If read-ahead reaches a damaged appended member, the
|
||||
metadata read retries with exact compressed input and output through the JSON marker;
|
||||
already-counted prefix bytes are replayed without retaining or charging another copy.
|
||||
JSON and selected-payload limits still apply, and requesting the damaged member still fails.
|
||||
- `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`. A
|
||||
|
||||
+15
-2
@@ -5,6 +5,7 @@
|
||||
//! - [`sniff`] tells which app wrote a file, from its first bytes.
|
||||
//! - [`info`] reads the format version and the writer the file names.
|
||||
//! - [`renditions`] returns the previews and the PDF a file carries.
|
||||
//! - [`rendition`] reads just one kind of preview or PDF.
|
||||
//! - [`outline`] returns the canvas, the pages (artboards or compositions) and the layers.
|
||||
//! - `pcraft_layer_rgba`, with the feature `pcraft-pixels`, decodes a PhotoCraft layer's pixels
|
||||
//! and its layer mask.
|
||||
@@ -291,14 +292,26 @@ pub fn info(bytes: &[u8], limits: &Limits) -> Result<Info, Error> {
|
||||
/// Create PDF-Compatible File; a `.pcraft` bundle has the `thumbnail` and `composite` its
|
||||
/// manifest names; an `.ecproj` project has none.
|
||||
pub fn renditions(bytes: &[u8], limits: &Limits) -> Result<Vec<Rendition>, Error> {
|
||||
read_renditions(bytes, None, limits)
|
||||
}
|
||||
|
||||
fn read_renditions(bytes: &[u8], wanted: Option<RenditionKind>, limits: &Limits) -> Result<Vec<Rendition>, Error> {
|
||||
let mut budget = Budget::new(limits);
|
||||
match sniff(bytes).ok_or(Error::Unrecognized)? {
|
||||
Kind::PhotoCraft => pcraft::renditions(bytes, &mut budget),
|
||||
Kind::VectorCraft => vectorcraft::renditions(bytes, &mut budget),
|
||||
Kind::PhotoCraft => pcraft::renditions(bytes, wanted, &mut budget),
|
||||
Kind::VectorCraft => vectorcraft::renditions(bytes, wanted, &mut budget),
|
||||
Kind::EffectCraft => ecproj::renditions(bytes, &mut budget),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the requested rendition, or `None` when this format or document carries none of that
|
||||
/// kind. Container metadata is still validated. Other rendition payloads are not decoded,
|
||||
/// copied or charged against the entry budget; JSON and bytes traversed to reach the selected
|
||||
/// compressed member still count against the usual limits.
|
||||
pub fn rendition(bytes: &[u8], kind: RenditionKind, limits: &Limits) -> Result<Option<Rendition>, Error> {
|
||||
Ok(read_renditions(bytes, Some(kind), limits)?.pop())
|
||||
}
|
||||
|
||||
/// The document's canvas, pages and layers (see [`Outline`]).
|
||||
pub fn outline(bytes: &[u8], limits: &Limits) -> Result<Outline, Error> {
|
||||
let mut budget = Budget::new(limits);
|
||||
|
||||
@@ -131,11 +131,14 @@ pub(crate) fn info(bytes: &[u8], budget: &mut Budget) -> Result<(u32, Option<Str
|
||||
stamp(&json, budget)
|
||||
}
|
||||
|
||||
pub(crate) fn renditions(bytes: &[u8], budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
pub(crate) fn renditions(bytes: &[u8], wanted: Option<RenditionKind>, budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
let (zip, json) = manifest(bytes, budget)?;
|
||||
let m: Envelope<IgnoredAny> = read(&json, budget)?;
|
||||
let mut out = vec![];
|
||||
for (kind, path) in [(RenditionKind::Thumbnail, m.thumbnail), (RenditionKind::Composite, m.composite)] {
|
||||
if wanted.is_some_and(|wanted| wanted != kind) {
|
||||
continue;
|
||||
}
|
||||
let Some(path) = path else { continue };
|
||||
let entry = zip.find(&path).ok_or_else(|| malformed(format!("the manifest names `{path}`, which the bundle does not have")))?;
|
||||
let max = budget.limits.max_entry;
|
||||
|
||||
@@ -33,7 +33,34 @@ use crate::{
|
||||
const CHUNK: u64 = 64 << 10;
|
||||
|
||||
pub(crate) fn sniff(head: &[u8]) -> bool {
|
||||
craft_vectorcraft_format::sniff(head)
|
||||
if craft_vectorcraft_format::sniff(head) {
|
||||
return true;
|
||||
}
|
||||
if !is_compressed(head) {
|
||||
return false;
|
||||
}
|
||||
// A decoder can inspect broken compressed data past the requested output. Retry the
|
||||
// bounded sniff with one compressed byte at a time, retaining any intact prefix.
|
||||
let mut unpacked = Vec::new();
|
||||
let head = head.get(..64 * 1024).unwrap_or(head);
|
||||
let _ = gzip_decoder(head, 1).take(256).read_to_end(&mut unpacked);
|
||||
!is_compressed(&unpacked) && craft_vectorcraft_format::sniff(&unpacked)
|
||||
}
|
||||
|
||||
struct GzipInput<'a> {
|
||||
bytes: &'a [u8],
|
||||
step: usize,
|
||||
}
|
||||
|
||||
impl Read for GzipInput<'_> {
|
||||
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
|
||||
let len = out.len().min(self.step);
|
||||
self.bytes.read(&mut out[..len])
|
||||
}
|
||||
}
|
||||
|
||||
fn gzip_decoder(bytes: &[u8], step: usize) -> GzDecoder<GzipInput<'_>> {
|
||||
GzDecoder::new(GzipInput { bytes, step })
|
||||
}
|
||||
|
||||
/// Where the JSON ends and the data after the marker starts, when `text` holds a NUL. JSON holds
|
||||
@@ -60,7 +87,7 @@ enum Tail<'a> {
|
||||
Slice(&'a [u8]),
|
||||
/// 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> },
|
||||
Stream { dec: Box<GzDecoder<GzipInput<'a>>>, read: Vec<u8> },
|
||||
}
|
||||
|
||||
impl Tail<'_> {
|
||||
@@ -137,14 +164,31 @@ fn unpack<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<Unpacked<'a>, Erro
|
||||
budget.check_json(json.len() as u64, JSON)?;
|
||||
return Ok(Unpacked { json: Cow::Borrowed(json), tail: Tail::Slice(tail) });
|
||||
}
|
||||
let mut dec = Box::new(GzDecoder::new(bytes));
|
||||
let mut dec = Box::new(gzip_decoder(bytes, usize::MAX));
|
||||
let mut step = CHUNK;
|
||||
let mut buf = Vec::new();
|
||||
let mut split: Option<(usize, usize)> = None;
|
||||
let mut nul: Option<usize> = None;
|
||||
loop {
|
||||
let start = buf.len();
|
||||
let want = CHUNK.min(budget.left().saturating_add(1));
|
||||
let n = (&mut *dec).take(want).read_to_end(&mut buf).map_err(damaged)?;
|
||||
let want = step.min(budget.left().saturating_add(1));
|
||||
let n = match (&mut *dec).take(want).read_to_end(&mut buf) {
|
||||
Ok(n) => n,
|
||||
Err(_) if step > 1 => {
|
||||
// Keep the fast path for intact files. On damage, replay the already-counted
|
||||
// prefix and read only through the marker, with no compressed read-ahead.
|
||||
let mut retry = Box::new(gzip_decoder(bytes, 1));
|
||||
let skipped = std::io::copy(&mut (&mut *retry).take(start as u64), &mut std::io::sink()).map_err(damaged)?;
|
||||
if skipped != start as u64 {
|
||||
return Err(malformed("the compressed document ends before its previously read prefix"));
|
||||
}
|
||||
dec = retry;
|
||||
buf.truncate(start);
|
||||
step = 1;
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(damaged(e)),
|
||||
};
|
||||
// The JSON ends before the first NUL: until there is one, all of it is JSON. The marker
|
||||
// may end in a later chunk than its first NUL.
|
||||
if nul.is_none() {
|
||||
@@ -345,8 +389,9 @@ impl<'de, T: Container> de::DeserializeSeed<'de> for Contained<T> {
|
||||
/// 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> {
|
||||
/// repeated `document` is refused too; its value is skipped. `PREVIEW` and `PDF` choose which
|
||||
/// image values to read; unrequested values are skipped without copying their strings.
|
||||
struct Stamp<const IMAGES: bool, const PREVIEW: bool, const PDF: bool> {
|
||||
format: String,
|
||||
version: u32,
|
||||
generator: Option<String>,
|
||||
@@ -363,17 +408,17 @@ fn once<'de, A: MapAccess<'de>, T: Deserialize<'de>>(map: &mut A, slot: &mut Opt
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
impl<'de, const IMAGES: bool, const PREVIEW: bool, const PDF: bool> Deserialize<'de> for Stamp<IMAGES, PREVIEW, PDF> {
|
||||
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>;
|
||||
struct StampVisitor<const IMAGES: bool, const PREVIEW: bool, const PDF: bool>;
|
||||
impl<'de, const IMAGES: bool, const PREVIEW: bool, const PDF: bool> Visitor<'de> for StampVisitor<IMAGES, PREVIEW, PDF> {
|
||||
type Value = Stamp<IMAGES, PREVIEW, PDF>;
|
||||
|
||||
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> {
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Stamp<IMAGES, PREVIEW, PDF>, 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>()? {
|
||||
@@ -382,8 +427,8 @@ impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
"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,
|
||||
"preview" if IMAGES && PREVIEW => preview = map.next_value::<Lenient<Image>>()?.0,
|
||||
"pdf" if IMAGES && PDF => pdf = map.next_value::<Lenient<Image>>()?.0,
|
||||
_ => {
|
||||
map.next_value::<IgnoredAny>()?;
|
||||
}
|
||||
@@ -401,14 +446,18 @@ impl<'de, const IMAGES: bool> Deserialize<'de> for Stamp<IMAGES> {
|
||||
})
|
||||
}
|
||||
}
|
||||
d.deserialize_map(StampVisitor::<IMAGES>)
|
||||
d.deserialize_map(StampVisitor::<IMAGES, PREVIEW, PDF>)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<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)? };
|
||||
fn stamp<const IMAGES: bool, const PREVIEW: bool, const PDF: bool>(
|
||||
json: &[u8],
|
||||
budget: &Budget,
|
||||
newer: bool,
|
||||
) -> Result<Stamp<IMAGES, PREVIEW, PDF>, Error> {
|
||||
let s: Stamp<IMAGES, PREVIEW, PDF> = 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}`"))),
|
||||
@@ -420,16 +469,28 @@ fn stamp<const IMAGES: bool>(json: &[u8], budget: &Budget, newer: bool) -> Resul
|
||||
|
||||
pub(crate) fn info(bytes: &[u8], budget: &mut Budget) -> Result<(u32, Option<String>), Error> {
|
||||
let json = unpack(bytes, budget)?.json;
|
||||
let s = stamp::<false>(&json, budget, true)?;
|
||||
let s = stamp::<false, false, false>(&json, budget, true)?;
|
||||
Ok((s.version, s.generator))
|
||||
}
|
||||
|
||||
pub(crate) fn renditions(bytes: &[u8], budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
/// Skip unrequested image fields during deserialization, including their inline base64 strings.
|
||||
fn images<const PREVIEW: bool, const PDF: bool>(json: &[u8], budget: &Budget) -> Result<(Option<Image>, Option<Image>), Error> {
|
||||
let head = stamp::<true, PREVIEW, PDF>(json, budget, false)?;
|
||||
Ok((head.preview, head.pdf))
|
||||
}
|
||||
|
||||
pub(crate) fn renditions(bytes: &[u8], wanted: Option<RenditionKind>, budget: &mut Budget) -> Result<Vec<Rendition>, Error> {
|
||||
let Unpacked { json, tail } = unpack(bytes, budget)?;
|
||||
let head = stamp::<true>(&json, budget, false)?;
|
||||
let (preview, pdf) = match wanted {
|
||||
None => images::<true, true>(&json, budget),
|
||||
Some(RenditionKind::Thumbnail) => images::<true, false>(&json, budget),
|
||||
Some(RenditionKind::Pdf) => images::<false, true>(&json, budget),
|
||||
Some(RenditionKind::Composite) => images::<false, false>(&json, budget),
|
||||
}?;
|
||||
// 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")]
|
||||
let members: Vec<_> = [(RenditionKind::Thumbnail, preview, "preview"), (RenditionKind::Pdf, pdf, "PDF")]
|
||||
.into_iter()
|
||||
.filter(|(kind, _, _)| wanted.is_none_or(|wanted| wanted == *kind))
|
||||
.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. A member after the
|
||||
@@ -516,7 +577,7 @@ struct KindM {
|
||||
|
||||
pub(crate) fn outline(bytes: &[u8], budget: &mut Budget) -> Result<Outline, Error> {
|
||||
let json = unpack(bytes, budget)?.json;
|
||||
stamp::<false>(&json, budget, false)?;
|
||||
stamp::<false, false, false>(&json, budget, false)?;
|
||||
let f: WithDocument = parse(&json, budget.limits)?;
|
||||
let pages = f
|
||||
.document
|
||||
@@ -554,6 +615,21 @@ mod tests {
|
||||
use crate::tests::{unread_array, unread_object};
|
||||
use serde::de::value::{Error as ValueError, MapDeserializer, SeqDeserializer, StrDeserializer};
|
||||
|
||||
/// An ignored field must use IgnoredAny, which can skip this sentinel without asking it
|
||||
/// to deserialize an image or materialize any strings.
|
||||
#[test]
|
||||
fn unrequested_images_are_skipped_before_their_strings_are_read() {
|
||||
fn skipped<const PREVIEW: bool, const PDF: bool>(field: &str) {
|
||||
let map = MapDeserializer::<_, ValueError>::new([(field, crate::tests::Unread), ("document", crate::tests::Unread)].into_iter());
|
||||
let err = Stamp::<true, PREVIEW, PDF>::deserialize(map).err().unwrap();
|
||||
assert!(err.to_string().contains("missing field `format`"), "{err}");
|
||||
}
|
||||
skipped::<false, true>("preview");
|
||||
skipped::<true, false>("pdf");
|
||||
skipped::<false, false>("preview");
|
||||
skipped::<false, false>("pdf");
|
||||
}
|
||||
|
||||
#[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());
|
||||
@@ -612,7 +688,7 @@ mod tests {
|
||||
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();
|
||||
let stamp: Stamp<true, true, 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}");
|
||||
}
|
||||
}
|
||||
|
||||
+134
-1
@@ -4,12 +4,145 @@
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
use craft_doc::{Error, Kind, Limits, RenditionKind, info, outline, renditions, sniff};
|
||||
use craft_doc::{Error, Kind, Limits, RenditionKind, info, outline, rendition, renditions, sniff};
|
||||
|
||||
fn limits() -> Limits {
|
||||
Limits::default()
|
||||
}
|
||||
|
||||
/// A PDF-only importer need not decode a broken preview, for inline or blob payloads.
|
||||
#[test]
|
||||
fn selected_rendition_ignores_other_payloads() {
|
||||
let inline = vectorcraft_json(
|
||||
3,
|
||||
&format!(r#""preview":{{"mime":"image/png","data":"!bad!"}}, "pdf":{{"mime":"application/pdf","data":"{}"}}"#, base64(PDF)),
|
||||
);
|
||||
let mut blob = vectorcraft_json(
|
||||
4,
|
||||
&format!(r#""preview":{{"mime":"image/png","blob":[{},100]}}, "pdf":{{"mime":"application/pdf","blob":[0,{}]}}"#, PDF.len() + 100, PDF.len()),
|
||||
)
|
||||
.into_bytes();
|
||||
blob.extend(BLOB_MAGIC);
|
||||
blob.extend(PDF);
|
||||
for bytes in [inline.into_bytes(), blob] {
|
||||
for bytes in [bytes.clone(), gzip(&bytes)] {
|
||||
assert!(renditions(&bytes, &limits()).is_err());
|
||||
let selected = rendition(&bytes, RenditionKind::Pdf, &limits()).unwrap().unwrap();
|
||||
assert_eq!(selected.bytes, PDF);
|
||||
assert!(rendition(&bytes, RenditionKind::Thumbnail, &limits()).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stored DEFLATE blocks contain the intact prefix; the next block has reserved BTYPE=3.
|
||||
fn gzip_with_broken_tail(prefix: &[u8]) -> Vec<u8> {
|
||||
let mut bytes = vec![0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255];
|
||||
for chunk in prefix.chunks(u16::MAX as usize) {
|
||||
let length = chunk.len() as u16;
|
||||
bytes.push(0); // Non-final stored block.
|
||||
bytes.extend(length.to_le_bytes());
|
||||
bytes.extend((!length).to_le_bytes());
|
||||
bytes.extend(chunk);
|
||||
}
|
||||
bytes.push(7); // Final block with an invalid block type.
|
||||
bytes.extend([0; 8]);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_inline_pdf_ignores_gzip_damage_after_its_json_marker() {
|
||||
for padding in [0, 128 * 1024] {
|
||||
let json = vectorcraft_json(
|
||||
4,
|
||||
&format!(r#""padding":"{}","preview":{},"pdf":{}"#, "x".repeat(padding), blob("image/png", 0, 100), inline("application/pdf", PDF)),
|
||||
);
|
||||
let mut prefix = json.as_bytes().to_vec();
|
||||
prefix.extend(BLOB_MAGIC);
|
||||
let bytes = gzip_with_broken_tail(&prefix);
|
||||
let mut exact = limits();
|
||||
exact.max_json = json.len() as u64;
|
||||
exact.max_total = (prefix.len() + PDF.len()) as u64;
|
||||
exact.max_entry = PDF.len() as u64;
|
||||
assert_eq!(rendition(&bytes, RenditionKind::Pdf, &exact).unwrap().unwrap().bytes, PDF);
|
||||
assert!(matches!(rendition(&bytes, RenditionKind::Thumbnail, &limits()), Err(Error::Malformed(_))));
|
||||
assert!(matches!(renditions(&bytes, &limits()), Err(Error::Malformed(_))));
|
||||
exact.max_total -= 1;
|
||||
assert!(matches!(rendition(&bytes, RenditionKind::Pdf, &exact), Err(Error::LimitExceeded(_))));
|
||||
exact.max_total += 1;
|
||||
exact.max_json -= 1;
|
||||
assert!(matches!(rendition(&bytes, RenditionKind::Pdf, &exact), Err(Error::LimitExceeded(_))));
|
||||
// A broken stream without a complete marker still fails, even after a complete JSON.
|
||||
assert!(matches!(rendition(&gzip_with_broken_tail(json.as_bytes()), RenditionKind::Pdf, &limits()), Err(Error::Malformed(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vectorcraft_sniff_does_not_unwrap_nested_gzip() {
|
||||
let json = vectorcraft_json(4, "");
|
||||
assert_eq!(sniff(&gzip(&gzip(json.as_bytes()))), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_rendition_skips_missing_zip_payloads_and_enforces_its_own_limits() {
|
||||
let manifest = pcraft_manifest(1, "[]", r#", "thumbnail":"thumb.png", "composite":"composite/preview.png""#);
|
||||
let image = png(2, 2, 1);
|
||||
let bytes = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &image)]);
|
||||
assert!(renditions(&bytes, &limits()).is_err());
|
||||
assert_eq!(rendition(&bytes, RenditionKind::Composite, &limits()).unwrap().unwrap().bytes, image);
|
||||
assert!(rendition(&bytes, RenditionKind::Thumbnail, &limits()).is_err());
|
||||
assert_eq!(rendition(&bytes, RenditionKind::Pdf, &limits()).unwrap(), None);
|
||||
let mut small = limits();
|
||||
small.max_entry = PDF.len() as u64 - 1;
|
||||
let vc = vectorcraft_json(3, &format!(r#""pdf":{{"mime":"application/pdf","data":"{}"}}"#, base64(PDF)));
|
||||
assert!(matches!(rendition(vc.as_bytes(), RenditionKind::Pdf, &small), Err(Error::LimitExceeded(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_rendition_preserves_format_and_version_checks() {
|
||||
assert_eq!(rendition(b"not a document", RenditionKind::Pdf, &limits()), Err(Error::Unrecognized));
|
||||
assert!(matches!(rendition(vectorcraft_json(99, "").as_bytes(), RenditionKind::Pdf, &limits()), Err(Error::TooNew { .. })));
|
||||
for bytes in [vectorcraft_json(4, "").into_bytes(), ecproj(2).into_bytes()] {
|
||||
assert_eq!(rendition(&bytes, RenditionKind::Pdf, &limits()).unwrap(), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ignored output is not charged, but gzip still has to traverse bytes before the PDF.
|
||||
#[test]
|
||||
fn selected_rendition_counts_its_payload_and_the_compressed_prefix() {
|
||||
let preview = vec![0; 128 * 1024];
|
||||
for before in [false, true] {
|
||||
let (preview_at, pdf_at) = if before { (0, preview.len()) } else { (PDF.len(), 0) };
|
||||
let json = vectorcraft_json(
|
||||
4,
|
||||
&format!(
|
||||
r#""preview":{{"mime":"image/png","blob":[{preview_at},{}]}},"pdf":{{"mime":"application/pdf","blob":[{pdf_at},{}]}}"#,
|
||||
preview.len(),
|
||||
PDF.len()
|
||||
),
|
||||
);
|
||||
let mut bytes = json.as_bytes().to_vec();
|
||||
bytes.extend(BLOB_MAGIC);
|
||||
if before {
|
||||
bytes.extend(&preview);
|
||||
bytes.extend(PDF);
|
||||
} else {
|
||||
bytes.extend(PDF);
|
||||
bytes.extend(&preview);
|
||||
}
|
||||
let mut small = limits();
|
||||
small.max_entry = PDF.len() as u64;
|
||||
small.max_total = (json.len() + BLOB_MAGIC.len() + PDF.len()) as u64;
|
||||
assert!(matches!(renditions(&bytes, &small), Err(Error::LimitExceeded(_))));
|
||||
assert_eq!(rendition(&bytes, RenditionKind::Pdf, &small).unwrap().unwrap().bytes, PDF);
|
||||
let compressed = rendition(&gzip(&bytes), RenditionKind::Pdf, &small);
|
||||
if before {
|
||||
assert!(matches!(compressed, Err(Error::LimitExceeded(_))));
|
||||
} else {
|
||||
assert_eq!(compressed.unwrap().unwrap().bytes, PDF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniff_tells_the_three_formats() {
|
||||
let vc = vectorcraft_json(4, "");
|
||||
|
||||
Reference in new issue
Block a user