//! Each format's documents, built from scratch: what `sniff`, `info`, `renditions` and `outline` //! return for them. mod common; use common::*; 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 { 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, ""); assert_eq!(sniff(vc.as_bytes()), Some(Kind::VectorCraft)); assert_eq!(sniff(&gzip(vc.as_bytes())), Some(Kind::VectorCraft)); let legacy = vc.replacen("\"vectorcraft\"", "\"drawcraft\"", 1); assert_eq!(sniff(legacy.as_bytes()), Some(Kind::VectorCraft)); // A gzip stream cut short still shows its start. let gz = gzip(vc.as_bytes()); assert_eq!(sniff(gz.get(..gz.len() - 8).unwrap()), Some(Kind::VectorCraft)); let pc = pcraft(&png(4, 4, 9), &png(8, 8, 9)); assert_eq!(sniff(&pc), Some(Kind::PhotoCraft), "the manifest last, found through the central directory"); let first = zip(&[store("manifest.json", b"{}"), store("thumb.png", &png(1, 1, 0))]); assert_eq!(sniff(first.get(..60).unwrap()), Some(Kind::PhotoCraft), "the manifest first, in a short head"); let second = zip(&[store("tiles/a.zst", b"x"), store("manifest.json", b"{}")]); assert_eq!(sniff(second.get(..100).unwrap()), Some(Kind::PhotoCraft), "a later local header in the head"); assert_eq!(sniff(ecproj(2).as_bytes()), Some(Kind::EffectCraft)); let old = r#"{"schema":1,"settings":{},"items":{},"next_id":1}"#; assert_eq!(sniff(old.as_bytes()), Some(Kind::EffectCraft), "saved before savedBy"); } #[test] fn sniff_refuses_other_files() { let plain_zip = zip(&[store("word/document.xml", b""), deflated("readme.txt", b"hello hello hello")]); let behind = zip(&[store("tiles/a.zst", &[7; 4096]), store("manifest.json", b"{}")]); for (what, bytes) in [ ("empty", &b""[..]), ("a PDF", PDF), ("a PNG", &png(2, 2, 1)), ("a ZIP without a manifest", &plain_zip), ("a manifest past the head", behind.get(..100).unwrap()), ("other JSON", br#"{"name":"x","items":[],"schema":"y"}"#), ("JSON naming the format in a value", br#"{"title":"vectorcraft"}"#), ("another format", br#"{"format":"vectorcraftx","version":4}"#), ("a JSON array", br#"["vectorcraft"]"#), ("savedBy without schema", br#"{"savedBy":"1.0","items":{}}"#), ("savedBy with schema only in a nested object", br#"{"savedBy":"1.0","metadata":{"schema":1}}"#), ("savedBy with schema only in a value", br#"{"savedBy":"1.0","note":"\"schema\""}"#), ("schema with items only in a nested object", br#"{"schema":1,"meta":{"items":[]}}"#), ("gzip of other JSON", &gzip(br#"{"format":"svg"}"#)), ("PK alone", b"PK\x03\x04"), ] { assert_eq!(sniff(bytes), None, "{what}"); } } #[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(); assert_eq!((i.kind, i.version, i.generator.as_deref(), i.supported), (Kind::VectorCraft, 3, Some("VectorCraft 0.1.0"), true)); let i = info(&gzip(vectorcraft_json(4, "").as_bytes()), &limits()).unwrap(); assert_eq!((i.version, i.supported), (4, true)); let i = info(&pcraft(&png(1, 1, 0), &png(1, 1, 0)), &limits()).unwrap(); assert_eq!((i.kind, i.version, i.generator.as_deref(), i.supported), (Kind::PhotoCraft, 1, Some("photocraft-format 0.1.0"), true)); let i = info(ecproj(2).as_bytes(), &limits()).unwrap(); assert_eq!((i.kind, i.version, i.generator.as_deref(), i.supported), (Kind::EffectCraft, 2, Some("0.12.0"), true)); let i = info(br#"{"schema":1,"settings":{},"items":{},"next_id":1}"#, &limits()).unwrap(); assert_eq!((i.version, i.generator), (1, None)); assert_eq!(info(PDF, &limits()), Err(Error::Unrecognized)); } #[test] fn too_new_files_are_reported_and_refused() { // Newer versions may change everything after the version: a member of a new shape. let vc = vectorcraft_json(5, r#""pdf":"elsewhere""#); let manifest = r#"{"format_version":2,"generator":"photocraft-format 9","document":"elsewhere","thumbnail":7}"#; let pc = zip(&[store("manifest.json", manifest.as_bytes())]); let ec = r#"{"savedBy":"9.0.0","schema":3,"items":[]}"#.to_string(); for (bytes, kind, version) in [(vc.into_bytes(), Kind::VectorCraft, 5), (pc, Kind::PhotoCraft, 2), (ec.into_bytes(), Kind::EffectCraft, 3)] { let i = info(&bytes, &limits()).unwrap(); assert_eq!((i.kind, i.version, i.supported), (kind, version, false)); 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:?}"); } } #[test] fn a_pcraft_format_version_of_zero_is_malformed() { let pc = zip(&[store("manifest.json", pcraft_manifest(0, "[]", "").as_bytes())]); assert!(matches!(info(&pc, &limits()), Err(Error::Malformed(_)))); assert!(matches!(outline(&pc, &limits()), Err(Error::Malformed(_)))); } #[test] fn vectorcraft_members_inline_as_base64() { let preview = png(16, 8, 200); let members = format!(r#""preview":{},"pdf":{}"#, inline("image/png", &preview), inline("application/pdf", PDF)); let json = vectorcraft_json(3, &members); for bytes in [json.clone().into_bytes(), gzip(json.as_bytes())] { let r = renditions(&bytes, &limits()).unwrap(); assert_eq!(r.len(), 2); assert_eq!((r[0].kind, r[0].mime.as_str(), r[0].bytes.as_slice()), (RenditionKind::Thumbnail, "image/png", preview.as_slice())); assert_eq!((r[1].kind, r[1].mime.as_str(), r[1].bytes.as_slice()), (RenditionKind::Pdf, "application/pdf", PDF)); } } #[test] fn vectorcraft_members_after_the_json() { // Version 4: the PDF after the marker, behind an image the document uses; the preview inline. let image = vec![0xAB; 9000]; let preview = png(3, 3, 1); let members = format!(r#""preview":{},"pdf":{}"#, inline("image/png", &preview), blob("application/pdf", image.len(), PDF.len())); let mut file = vectorcraft_json(4, &members).into_bytes(); file.extend(BLOB_MAGIC); file.extend(&image); file.extend(PDF); for bytes in [file.clone(), gzip(&file)] { let r = renditions(&bytes, &limits()).unwrap(); assert_eq!(r.iter().map(|r| r.kind).collect::>(), [RenditionKind::Thumbnail, RenditionKind::Pdf]); assert_eq!(r[0].bytes, preview); assert_eq!(r[1].bytes, PDF); // The JSON alone answers info and outline. assert_eq!(info(&bytes, &limits()).unwrap().version, 4); assert_eq!(outline(&bytes, &limits()).unwrap().pages.len(), 2); } } #[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. let base = vectorcraft_json(4, &format!(r#""pdf":{},"pad":"""#, blob("application/pdf", 0, PDF.len()))); for json_len in (64 << 10) - 12..(64 << 10) + 2 { let json = base.replacen(r#""pad":"""#, &format!(r#""pad":"{}""#, "x".repeat(json_len - base.len())), 1); assert_eq!(json.len(), json_len); let mut file = json.into_bytes(); file.extend(BLOB_MAGIC); file.extend(PDF); let gz = gzip(&file); assert_eq!(info(&gz, &limits()).unwrap().version, 4, "{json_len}"); assert_eq!(outline(&gz, &limits()).unwrap().pages.len(), 2, "{json_len}"); assert_eq!(renditions(&gz, &limits()).unwrap()[0].bytes, PDF, "{json_len}"); } } #[test] fn a_padded_base64_member_fits_a_limit_of_its_own_size() { for len in 1..=3 { let data = vec![7u8; len]; let bytes = vectorcraft_json(3, &format!(r#""preview":{}"#, inline("image/png", &data))).into_bytes(); let mut l = limits(); l.max_entry = len as u64; assert_eq!(renditions(&bytes, &l).unwrap()[0].bytes, data); } } #[test] fn vectorcraft_without_members_has_no_renditions() { let json = vectorcraft_json(4, ""); assert_eq!(renditions(json.as_bytes(), &limits()).unwrap(), vec![]); assert_eq!(renditions(&gzip(json.as_bytes()), &limits()).unwrap(), vec![]); } #[test] fn vectorcraft_outline_has_artboards_and_top_level_layers() { for bytes in [vectorcraft_json(4, "").into_bytes(), gzip(vectorcraft_json(4, "").as_bytes())] { let o = outline(&bytes, &limits()).unwrap(); assert_eq!(o.canvas, None); let pages: Vec<_> = o.pages.iter().map(|p| (p.name.as_str(), p.x, p.y, p.width, p.height)).collect(); assert_eq!(pages, [("Front", 0.0, 0.0, 612.0, 792.0), ("Back", 650.0, 700.0, 50.0, 100.0)]); assert!(o.pages.iter().all(|p| p.timing.is_none() && p.layers.is_empty())); let layers: Vec<_> = o.layers.iter().map(|l| (l.name.as_str(), l.kind.as_str(), l.visible, l.opacity, l.blend.as_deref())).collect(); assert_eq!(layers, [("Art", "layer", true, Some(1.0), Some("Normal")), ("Notes", "layer", false, Some(0.5), Some("Multiply"))]); assert!(o.layers.iter().all(|l| l.children.is_empty())); } } /// Thumbnail and legacy composite previews never claim a full-size source. #[test] fn pcraft_previews_stored_and_deflated_with_the_manifest_last() { let (thumb, composite) = (png(4, 3, 10), png(40, 30, 20)); let r = renditions(&pcraft(&thumb, &composite), &limits()).unwrap(); assert_eq!(r.len(), 2); assert_eq!((r[0].kind, r[0].mime.as_str(), &r[0].bytes), (RenditionKind::Thumbnail, "image/png", &thumb)); assert_eq!((r[1].kind, r[1].mime.as_str(), &r[1].bytes), (RenditionKind::Composite, "image/png", &composite)); assert!(r.iter().all(|rendition| !rendition.is_full_composite)); } /// Full-member identity is retained even when its dimensions differ from the document canvas. #[test] fn pcraft_prefers_the_full_size_composite_without_reading_the_preview() { let full = png(80, 60, 30); let small_preview = png(4, 3, 20); for preview in [false, true] { let manifest = pcraft_manifest(1, "[]", r#", "full_composite":"composite/full.png", "composite":"composite/preview.png""#); let mut entries = vec![store("manifest.json", manifest.as_bytes()), deflated("composite/full.png", &full)]; if preview { entries.push(store("composite/preview.png", &small_preview)); } let r = renditions(&zip(&entries), &limits()).unwrap(); assert_eq!(r.len(), 1); assert_eq!((r[0].kind, r[0].mime.as_str(), &r[0].bytes), (RenditionKind::Composite, "image/png", &full)); assert!(r[0].is_full_composite); let canvas = outline(&zip(&entries), &limits()).unwrap().canvas.unwrap(); assert_eq!((canvas.width, canvas.height), (640, 480), "the flag does not validate the 80 by 60 PNG against this canvas"); } let manifest = pcraft_manifest(1, "[]", r#", "full_composite":"composite/full.png", "composite":"composite/preview.png""#); let mut tight = limits(); tight.max_entry = manifest.len().max(full.len()) as u64; let oversized = vec![0; tight.max_entry as usize + 1]; for preview in [store("composite/preview.png", &oversized), deflated("composite/preview.png", &oversized)] { let pc = zip(&[store("manifest.json", manifest.as_bytes()), deflated("composite/full.png", &full), preview]); assert_eq!(renditions(&pc, &tight).unwrap()[0].bytes, full); } let manifest = pcraft_manifest(1, "[]", r#", "full_composite":"composite/full.png""#); let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/full.png", &full)]); let selected = renditions(&pc, &limits()).unwrap().pop().unwrap(); assert_eq!(selected.bytes, full); assert!(selected.is_full_composite); let mut small = limits(); small.max_entry = manifest.len() as u64; // Keep the manifest under the limit while bounding the image independently. let large = vec![0; manifest.len() + full.len()]; let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/full.png", &large)]); assert!(matches!(renditions(&pc, &small), Err(Error::LimitExceeded(_)))); } /// The source flag distinguishes identical payloads selected from different manifest members. #[test] fn pcraft_identical_images_keep_their_composite_source() { let image = png(4, 3, 30); let mut variants = Vec::new(); for declared in [false, true] { let fields = if declared { r#", "full_composite":"composite/full.png", "composite":"composite/preview.png""# } else { r#", "composite":"composite/preview.png""# }; let manifest = pcraft_manifest(1, "[]", fields); let bundle = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/full.png", &image), store("composite/preview.png", &image)]); let rendition = renditions(&bundle, &limits()).unwrap().pop().unwrap(); assert_eq!(rendition.is_full_composite, declared); variants.push(rendition); } assert_eq!((variants[0].kind, &variants[0].mime, &variants[0].bytes), (variants[1].kind, &variants[1].mime, &variants[1].bytes)); assert_ne!(variants[0], variants[1], "equality includes the selected source"); } /// Selected extraction keeps full-composite precedence and provenance without reading other images. #[test] fn selected_full_composite_skips_missing_thumbnail_and_damaged_preview() { let full = png(4, 3, 30); let manifest = pcraft_manifest(1, "[]", r#", "thumbnail":"thumb.png", "composite":"composite/preview.png", "full_composite":"composite/full.png""#); let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/full.png", &full), store("composite/preview.png", b"unused preview")]); let selected = rendition(&pc, RenditionKind::Composite, &limits()).unwrap().unwrap(); assert_eq!(selected.bytes, full); assert!(selected.is_full_composite); assert!(rendition(&pc, RenditionKind::Thumbnail, &limits()).is_err()); let without_full = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &full)]); assert!(rendition(&without_full, RenditionKind::Composite, &limits()).is_err(), "a declared full image cannot silently fall back"); } /// Missing and null full declarations ignore an undeclared full entry; thumbnails stay false. #[test] fn pcraft_legacy_and_thumbnail_sources_are_not_full_composites() { let image = png(4, 3, 30); for declaration in ["", r#", "full_composite":null"#, r#", "full_composite":"composite/full.png""#] { let manifest = pcraft_manifest(1, "[]", &format!(r#", "thumbnail":"thumb.png", "composite":"composite/preview.png"{declaration}"#)); let bundle = zip(&[ store("manifest.json", manifest.as_bytes()), store("thumb.png", &image), store("composite/preview.png", &image), store("composite/full.png", &image), ]); let renditions = renditions(&bundle, &limits()).unwrap(); assert_eq!(renditions.len(), 2); assert_eq!(renditions[0].kind, RenditionKind::Thumbnail); assert!(!renditions[0].is_full_composite); assert_eq!(renditions[1].kind, RenditionKind::Composite); assert_eq!(renditions[1].is_full_composite, declaration.contains("composite/full.png")); } } /// VectorCraft thumbnails and PDFs retain false provenance for plain and gzip documents. #[test] fn vectorcraft_renditions_are_not_full_composites() { let document = vectorcraft_json(3, &format!(r#""preview":{}, "pdf":{}"#, inline("image/png", &png(4, 3, 30)), inline("application/pdf", PDF))); for bytes in [document.as_bytes().to_vec(), gzip(document.as_bytes())] { let renditions = renditions(&bytes, &limits()).unwrap(); assert_eq!(renditions.iter().map(|rendition| rendition.kind).collect::>(), [RenditionKind::Thumbnail, RenditionKind::Pdf]); assert!(renditions.iter().all(|rendition| !rendition.is_full_composite)); } } /// A checksum failure in the declared full entry remains an error despite a valid preview. #[test] fn a_damaged_declared_full_composite_does_not_fall_back() { let image = png(4, 3, 30); let manifest = pcraft_manifest(1, "[]", r#", "full_composite":"composite/full.png", "composite":"composite/preview.png""#); let mut bundle = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &image), store("composite/full.png", &image)]); let central = bundle .windows(4) .enumerate() .find(|(at, magic)| *magic == b"PK\x01\x02" && bundle.get(at + 46..at + 46 + b"composite/full.png".len()) == Some(b"composite/full.png")) .unwrap() .0; bundle[central + 16] ^= 1; assert!(matches!(renditions(&bundle, &limits()), Err(Error::Malformed(_)))); } /// Invalid declarations and missing full entries never silently fall back to the preview. #[test] fn pcraft_refuses_invalid_or_missing_full_size_composites() { for field in [r#""../full.png""#, r#""composite/preview.png""#, "12", r#""composite/full.png", "full_composite":null"#] { let manifest = pcraft_manifest(1, "[]", &format!(r#", "full_composite":{field}"#)); let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &png(4, 3, 20))]); assert!(matches!(renditions(&pc, &limits()), Err(Error::Malformed(_))), "{field}"); } let manifest = pcraft_manifest(1, "[]", r#", "full_composite":"composite/full.png", "composite":"composite/preview.png""#); let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &png(4, 3, 20))]); assert!(matches!(renditions(&pc, &limits()), Err(Error::Malformed(_)))); let manifest = pcraft_manifest(1, "[]", r#", "full_composite":null, "composite":"composite/preview.png""#); let preview = png(4, 3, 20); let pc = zip(&[store("manifest.json", manifest.as_bytes()), store("composite/preview.png", &preview)]); let selected = renditions(&pc, &limits()).unwrap().pop().unwrap(); assert_eq!(selected.bytes, preview); assert!(!selected.is_full_composite); } #[test] fn pcraft_without_previews_has_none() { let pc = zip(&[store("manifest.json", pcraft_manifest(1, "[]", "").as_bytes())]); assert_eq!(renditions(&pc, &limits()).unwrap(), vec![]); let missing = zip(&[store("manifest.json", pcraft_manifest(1, "[]", r#","thumbnail":"thumb.png""#).as_bytes())]); assert!(matches!(renditions(&missing, &limits()), Err(Error::Malformed(_)))); } #[test] fn pcraft_outline_has_the_canvas_and_layer_tree() { let o = outline(&pcraft(&png(1, 1, 0), &png(1, 1, 0)), &limits()).unwrap(); let c = o.canvas.unwrap(); assert_eq!((c.width, c.height, c.resolution_dpi, c.mode.as_str(), c.depth.as_str()), (640, 480, 300.0, "Rgb", "U16")); assert!(o.pages.is_empty()); let layers: Vec<_> = o.layers.iter().map(|l| (l.name.as_str(), l.kind.as_str(), l.visible, l.opacity, l.blend.as_deref())).collect(); assert_eq!(layers, [("Background", "raster", true, Some(1.0), Some("Normal")), ("Group", "group", false, Some(0.25), Some("Screen"))]); let inner = &o.layers[1].children; assert_eq!(inner.len(), 1); assert_eq!((inner[0].name.as_str(), inner[0].blend.as_deref(), inner[0].opacity), ("Inner", Some("Multiply"), Some(0.5))); } #[test] 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()), (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] fn ecproj_has_no_renditions_and_lists_its_comps() { let p = ecproj(2); assert_eq!(renditions(p.as_bytes(), &limits()).unwrap(), vec![]); let o = outline(p.as_bytes(), &limits()).unwrap(); assert!(o.canvas.is_none() && o.layers.is_empty()); // By item id (3 before 10), the folder and the footage left out. let pages: Vec<_> = o.pages.iter().map(|p| (p.name.as_str(), p.width, p.height)).collect(); assert_eq!(pages, [("Intro", 640.0, 360.0), ("Main", 1920.0, 1080.0)]); let t = o.pages[1].timing.unwrap(); assert_eq!((t.frame_rate_num, t.frame_rate_den, t.duration_seconds), (24000, 1001, 5.0)); assert_eq!(o.pages[0].timing.unwrap().duration_seconds, 1.0); // Bottom first: the file lists the top layer first. let layers: Vec<_> = o.pages[1].layers.iter().map(|l| (l.name.as_str(), l.kind.as_str(), l.visible, l.opacity, l.blend.as_deref())).collect(); assert_eq!(layers, [("Plate", "Footage", false, None, Some("Normal")), ("Title", "Text", true, None, Some("Add"))]); } #[test] fn an_ecproj_comp_without_a_duration_has_no_timing() { let p = ecproj(2).replacen(r#""duration": 254016000000, "#, "", 1); let o = outline(p.as_bytes(), &limits()).unwrap(); assert_eq!((o.pages[0].name.as_str(), o.pages[0].timing), ("Intro", None)); assert!(o.pages[1].timing.is_some()); } #[test] fn kinds_name_their_extension_and_version() { assert_eq!( [Kind::PhotoCraft, Kind::VectorCraft, Kind::EffectCraft].map(|k| (k.extension(), k.supported_version())), [("pcraft", 1), ("vectorcraft", 4), ("ecproj", 2)] ); }