use std::io::Write; use craft_vectorcraft_format::{ BLOB_MAGIC, Contents, EXTENSION, Entries, Error, File, Head, INLINE_MAX, Image, LEGACY_EXTENSION, MAX_ENTRY, ReadError, Tail, VERSION, base64, check, entry, is_compressed, json_part, marker_at, sniff, split_tail, }; use serde::de::IgnoredAny; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] struct Layer { name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] struct Doc { layers: Vec, } const PNG: &[u8] = &[0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n']; fn doc() -> Doc { Doc { layers: vec![Layer { name: "Layer 1".into() }] } } fn pdf() -> Vec { let mut pdf = b"%PDF-1.7\n".to_vec(); pdf.resize(INLINE_MAX, b' '); pdf } /// A PNG entry at `[offset, length]` in the data after the JSON. fn blob(at: [u64; 2]) -> Image { let mut image = Image::default(); image.mime = "image/png".into(); image.blob = Some(at); image } fn gzip(bytes: &[u8]) -> Vec { let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); gz.write_all(bytes).unwrap(); gz.finish().unwrap() } fn written(file: &File, tail: &Tail<'_>, pretty: bool) -> Vec { let mut out = if pretty { serde_json::to_vec_pretty(file) } else { serde_json::to_vec(file) }.unwrap(); tail.write_to(&mut out).unwrap(); out } fn full_file(pdf: &[u8]) -> (File, Tail<'_>) { let mut tail = Tail::new(VERSION); let mut file = File::new(doc()); file.generator = Some("VectorCraft 0.17.0".into()); file.preview = Some(Image::inline("image/png", PNG)); file.pdf = Some(tail.image("application/pdf", pdf, false)); file.images.insert("k".into(), tail.image("image/png", &[1, 2, 3, 250], false)); (file, tail) } #[test] fn a_version_4_file_with_members_after_the_json_is_written_as_the_app_writes_it() { let pdf = pdf(); let (file, tail) = full_file(&pdf); let mut expected = br#"{"format":"vectorcraft","version":4,"generator":"VectorCraft 0.17.0","preview":{"mime":"image/png","data":"iVBORw0KGgo="},"pdf":{"mime":"application/pdf","blob":[0,8192]},"document":{"layers":[{"name":"Layer 1"}]},"images":{"k":{"mime":"image/png","data":"AQID+g=="}}}"#.to_vec(); expected.extend(b"\n\0VCBLOBS\0"); expected.extend(&pdf); assert_eq!(written(&file, &tail, false), expected); assert_eq!(tail.len(), INLINE_MAX as u64); assert_eq!(tail.parts().len(), 1); } #[test] fn a_pretty_file_without_optional_members_is_written_as_the_app_writes_it() { let mut file = File::new(doc()); file.generator = Some("VectorCraft 0.17.0".into()); let expected = r#"{ "format": "vectorcraft", "version": 4, "generator": "VectorCraft 0.17.0", "document": { "layers": [ { "name": "Layer 1" } ] }, "images": {} }"#; assert_eq!(String::from_utf8(written(&file, &Tail::new(VERSION), true)).unwrap(), expected); } #[test] fn a_version_2_file_keeps_large_entries_inline_under_the_former_name() { let pdf = pdf(); let mut tail = Tail::new(2); let mut file = File::new(doc()); file.format = LEGACY_EXTENSION.into(); file.version = 2; file.profiles.insert("Gray".into(), tail.image(craft_vectorcraft_format::ICC_MIME, &pdf, false)); let bytes = written(&file, &tail, false); assert!(tail.is_empty()); assert!(!bytes.contains(&0)); let text = String::from_utf8(bytes).unwrap(); assert!(text.starts_with(r#"{"format":"drawcraft","version":2,"profiles":{"Gray":{"mime":"application/vnd.iccprofile","data":"JVBERi0xLjcK"#)); let back = File::::read(text.as_bytes()).unwrap(); assert!(back.is_legacy()); assert_eq!(back.profiles["Gray"].bytes(&[]).unwrap(), pdf); } #[test] fn a_written_file_reads_back_with_its_entries() { let pdf = pdf(); let (file, tail) = full_file(&pdf); for bytes in [written(&file, &tail, false), written(&file, &tail, true)] { let (json, after) = split_tail(&bytes); assert_eq!(json_part(&bytes), json); assert_eq!(after, pdf.as_slice()); let back = File::::read(json).unwrap(); assert_eq!(back, file); back.check().unwrap(); assert!(!back.is_legacy()); assert_eq!(back.pdf.as_ref().unwrap().bytes(after).unwrap(), pdf); assert_eq!(back.pdf.as_ref().unwrap().len(), pdf.len() as u64); assert_eq!(back.preview.as_ref().unwrap().bytes(after).unwrap(), PNG); assert_eq!(back.images["k"].bytes(after).unwrap(), [1, 2, 3, 250]); assert_eq!(back.images["k"].len(), 4); let head: Head = serde_json::from_slice(json).unwrap(); assert_eq!(head.preview, file.preview); assert_eq!(head.pdf, file.pdf); let skipped = File::::read(json).unwrap(); assert_eq!(skipped.generator.as_deref(), Some("VectorCraft 0.17.0")); } } #[test] fn a_linked_image_preview_is_marked_proxy() { let mut tail = Tail::new(VERSION); let image = tail.image("image/png", PNG, true); assert_eq!(serde_json::to_string(&image).unwrap(), r#"{"mime":"image/png","data":"iVBORw0KGgo=","proxy":true}"#); let back: Image = serde_json::from_str(r#"{"mime":"image/png","data":"iVBORw0KGgo=","proxy":true}"#).unwrap(); assert_eq!(back, image); } #[test] fn entries_outside_the_data_or_with_bad_base64_give_none() { let tail = b"abcd"; assert_eq!(entry(tail, [1, 3]), Some(&b"bcd"[..])); assert_eq!(entry(tail, [1, 4]), None); assert_eq!(entry(tail, [u64::MAX, 1]), None); assert_eq!(entry(tail, [1, u64::MAX]), None); assert_eq!(blob([2, 8]).bytes(tail), None); let mut bad = Image::default(); bad.data = "QU*D".into(); assert_eq!(bad.bytes(tail), None); assert!(Image::default().is_empty()); } #[test] fn image_bytes_refuses_an_entry_past_the_limit_before_copying_it() { assert_eq!(MAX_ENTRY, 1 << 30); let tail = b"abcd"; let inline = Image::inline("image/png", tail); for image in [blob([0, 4]), inline.clone()] { assert_eq!(image.bytes_at_most(tail, 4).as_deref(), Some(&tail[..])); assert_eq!(image.bytes_at_most(tail, 3), None); assert_eq!(image.bytes(tail).as_deref(), Some(&tail[..])); } // The stated length is checked, so a range past the limit is refused even where the data // holds it. assert_eq!(blob([0, MAX_ENTRY + 1]).bytes_at_most(&[0; 64], MAX_ENTRY), None); assert_eq!(blob([0, 2]).bytes_at_most(tail, 2).as_deref(), Some(&b"ab"[..])); } #[test] fn entries_copy_at_most_the_data_after_the_json_in_all() { let tail = vec![7u8; 1 << 20]; let len = tail.len() as u64; let whole = blob([0, len]); // Many entries naming the same range share one copy, which counts once. let mut entries = Entries::new(&tail); let first = entries.bytes(&whole).ok().flatten(); assert_eq!(first.as_deref().map(Vec::as_slice), Some(&tail[..])); assert!(entries.blob([0, len]).ok().flatten().zip(first.as_ref()).is_some_and(|(a, b)| std::sync::Arc::ptr_eq(&a, b))); for _ in 0..3 { let again = entries.bytes(&whole).ok().flatten(); assert!(again.as_ref().zip(first.as_ref()).is_some_and(|(a, b)| std::sync::Arc::ptr_eq(a, b))); } assert_eq!(entries.copied(), len); // Overlapping ranges that differ copy the shared bytes once each, and pass the data's size. let from = |at: u64| blob([at, len - at]); assert!(matches!(entries.bytes(&from(1)), Err(Error::TooBig))); // A refused entry is not counted, so the entries after it still read. assert_eq!(entries.copied(), len); let mut entries = Entries::new(&tail); let part = |at: u64, n: u64| blob([at, n]); assert!(entries.bytes(&part(0, len / 2)).is_ok_and(|b| b.is_some())); assert!(matches!(entries.bytes(&part(1, len - 1)), Err(Error::TooBig))); assert_eq!(entries.copied(), len / 2); assert!(entries.bytes(&part(len / 2, len / 2)).is_ok_and(|b| b.is_some())); assert_eq!(entries.copied(), len); // Entries that name each byte once read whole, with inline and missing entries beside them. let mut entries = Entries::new(&tail); let half = |at: u64| blob([at, len / 2]); assert_eq!(entries.bytes(&half(0)).unwrap().map(|b| b.len()), Some(tail.len() / 2)); assert_eq!(entries.bytes(&Image::inline("image/png", PNG)).unwrap().as_deref().map(Vec::as_slice), Some(PNG)); assert_eq!(entries.bytes(&half(len)), Ok(None)); assert_eq!(entries.bytes(&half(len / 2)).unwrap().map(|b| b.len()), Some(tail.len() / 2)); assert_eq!(entries.copied(), len); assert_eq!(Entries::new(&[]).bytes(&whole), Ok(None)); } #[test] fn optional_members_of_the_wrong_shape_read_as_none() { let json = br#"{"format":"vectorcraft","version":4,"generator":7,"preview":"x","pdf":{"blob":"y"},"document":{"layers":[]}}"#; let file = File::::read(json).unwrap(); assert_eq!((file.generator, file.preview, file.pdf), (None, None, None)); let skipped = File::::read(json).unwrap(); assert!(skipped.preview.is_none()); let head: Head = serde_json::from_slice(json).unwrap(); assert_eq!(head, Head::default()); let good = br#"{"preview":{"mime":"image/png","data":"iVBORw0KGgo="},"pdf":null}"#; let head: Head = serde_json::from_slice(good).unwrap(); assert_eq!(head.preview, Some(Image::inline("image/png", PNG))); assert!(head.pdf.is_none()); } #[test] fn contents_read_the_document_images_and_profiles_without_the_preview_and_pdf() { let big = vec![b'%'; 64 << 10]; let mut file = File::new(doc()); file.version = 2; file.generator = Some("VectorCraft 0.17.0".into()); file.preview = Some(Image::inline("image/png", &big)); file.pdf = Some(Image::inline("application/pdf", &big)); file.profiles.insert("Gray".into(), Image::inline(craft_vectorcraft_format::ICC_MIME, &[4, 5])); file.images.insert("k".into(), Image::inline("image/png", PNG)); for json in [serde_json::to_vec(&file).unwrap(), serde_json::to_vec_pretty(&file).unwrap()] { let contents = Contents::::read(&json).unwrap(); let Contents { format, version, profiles, document, images, .. } = contents.clone(); assert_eq!((format, version, profiles, document, images), (file.format.clone(), 2, file.profiles.clone(), doc(), file.images.clone())); assert!(contents.is_legacy() == file.is_legacy()); } // Members of the wrong shape are skipped as `File` reads them as `None`; a missing document // is refused as in `File`. let odd = br#"{"format":"vectorcraft","version":4,"generator":7,"preview":"x","pdf":{"blob":"y"},"document":{"layers":[]},"z":[1]}"#; let contents = Contents::::read(odd).unwrap(); assert!(contents.document.layers.is_empty() && contents.images.is_empty()); let no_document = br#"{"format":"vectorcraft","version":4}"#; assert!(File::::read(no_document).is_err()); assert!(Contents::::read(no_document).is_err()); } #[test] fn contents_read_pdf_preference_while_skipping_the_payload() { for (members, expected) in [ ("", false), (r#", "pdf":null"#, false), (r#", "pdf":{"data":"not base64"}"#, true), (r#", "pdf":[1,2,3]"#, true), (r#", "pdfCompatible":true"#, true), (r#", "pdf":{}, "pdfCompatible":false"#, false), (r#", "pdfCompatible":false, "pdf":{}"#, false), (r#", "pdfCompatible":true, "pdfCompatible":null, "pdf":{}"#, true), (r#", "pdf":{}, "pdf":null"#, false), (r#", "pdfCompatible":false, "pdfCompatible":true"#, true), (r#", "unknown":{"pdf":{}}"#, false), ] { let json = format!(r#"{{"format":"vectorcraft","version":4,"document":{{"layers":[]}}{members}}}"#); let (contents, compatible) = Contents::::read_with_pdf_preference(json.as_bytes()).unwrap(); assert_eq!(compatible, expected, "{members}"); assert_eq!(contents, Contents::::read(json.as_bytes()).unwrap()); } let array = br#"["drawcraft",1,{"layers":[]}]"#; assert!(!Contents::::read_with_pdf_preference(array).unwrap().1); let mut file = File::new(doc()); file.pdf = Some(Image::inline("application/pdf", &vec![7; 128 << 10])); assert!(Contents::::read_with_pdf_preference(&serde_json::to_vec(&file).unwrap()).unwrap().1); } #[test] fn pdf_preference_read_is_opt_in_and_preserves_envelope_validation() { let invalid = br#"{"format":"vectorcraft","version":4,"document":{"layers":[]},"pdfCompatible":"invalid"}"#; assert!(Contents::::read(invalid).is_ok()); assert!(File::::read(invalid).is_ok()); assert!(Contents::::read_with_pdf_preference(invalid).is_err()); for json in [ br#"{"format":"other","version":4,"document":{"layers":[]}}"#.as_slice(), br#"{"format":"vectorcraft","version":999,"document":{"layers":[]}}"#.as_slice(), br#"{"format":"vectorcraft","version":4}"#.as_slice(), br#"{"format":"vectorcraft","version":4,"version":4,"document":{"layers":[]}}"#.as_slice(), ] { assert_eq!(Contents::::read(json).unwrap_err().to_string(), Contents::::read_with_pdf_preference(json).unwrap_err().to_string()); } } #[test] fn reading_refuses_another_format_and_a_newer_version() { let newer = br#"{"format":"vectorcraft","version":5,"document":0}"#; let other = br#"{"format":"other","version":1,"document":0}"#; for (json, error) in [(&newer[..], Error::TooNew(5)), (other, Error::Format("other".into()))] { let file = File::::read(json); assert!(matches!(&file, Err(ReadError::Envelope(e)) if *e == error), "{file:?}"); let contents = Contents::::read(json); assert!(matches!(&contents, Err(ReadError::Envelope(e)) if *e == error), "{contents:?}"); assert_eq!(contents.err().map(|e| e.to_string()), Some(error.to_string())); } for version in [0, 1, VERSION] { let json = format!(r#"{{"format":"drawcraft","version":{version},"document":0}}"#); assert!(File::::read(json.as_bytes()).is_ok() && Contents::::read(json.as_bytes()).is_ok()); } // A document that does not read is a JSON error, as before the version is looked at. let bad = br#"{"format":"vectorcraft","version":5,"document":7}"#; assert!(matches!(Contents::::read(bad), Err(ReadError::Json(_)))); assert!(Contents::::read(b"\"x\"").is_err_and(|e| e.to_string().contains("expected struct File"))); } #[test] fn file_and_contents_read_a_repeated_generator_preview_or_pdf_and_refuse_other_repeated_members() { // VectorCraft opens a file whose generator, preview or PDF is repeated, and refuses one whose // format, version, document, images or profiles are. `File` keeps the last value, as // serde_json reads a repeated key into a `Value`. let png = r#"{"mime":"image/png","data":"iVBORw0KGgo="}"#; let pdf = r#"{"mime":"application/pdf","data":"JQ=="}"#; let file = |extra: &str| format!(r#"{{"format":"vectorcraft","version":4,{extra}"document":{{"layers":[]}},"images":{{}}}}"#); let value = |json: &str, key: &str| serde_json::from_str::(json).unwrap()[key].clone(); for extra in [ r#""generator":"a","generator":"b","#.to_string(), format!(r#""preview":{png},"preview":{png},"#), format!(r#""pdf":null,"pdf":{pdf},"#), format!(r#""pdf":{pdf},"pdf":"x","#), format!(r#""generator":"a","preview":{png},"generator":7,"preview":"x","pdf":1,"pdf":{pdf},"#), r#""generator":7,"preview":"x","pdf":1,"generator":7,"preview":"x","pdf":1,"#.to_string(), ] { let json = file(&extra); let contents = Contents::::read(json.as_bytes()).unwrap_or_else(|e| panic!("{extra}: {e}")); assert!(contents.document.layers.is_empty(), "{extra}"); let read = File::::read(json.as_bytes()).unwrap_or_else(|e| panic!("{extra}: {e}")); let last = |key: &str| value(&json, key); assert_eq!(read.generator, last("generator").as_str().map(String::from), "{extra}"); assert_eq!(read.preview, serde_json::from_value(last("preview")).ok(), "{extra}"); assert_eq!(read.pdf, serde_json::from_value(last("pdf")).ok(), "{extra}"); } let read = File::::read(file(r#""generator":"a","generator":"b","#).as_bytes()).unwrap(); assert_eq!(read.generator.as_deref(), Some("b")); for extra in [r#""format":"vectorcraft","#, r#""version":4,"#, r#""document":{"layers":[]},"#, r#""images":{},"#, r#""profiles":{},"profiles":{},"#] { let json = file(extra); assert!(File::::read(json.as_bytes()).is_err_and(|e| e.to_string().starts_with("duplicate field")), "{extra}"); assert!(Contents::::read(json.as_bytes()).is_err_and(|e| e.to_string().starts_with("duplicate field")), "{extra}"); } } #[test] fn an_array_holds_format_version_document_images_and_profiles_in_that_order() { // VectorCraft reads a top-level array by position, in this order. let png = r#"{"mime":"image/png","data":"iVBORw0KGgo="}"#; let images = format!(r#"{{"k":{png}}}"#); let profiles = format!(r#"{{"Gray":{png}}}"#); let full = format!(r#"["vectorcraft",3,{{"layers":[]}},{images},{profiles}]"#); for json in [full.clone(), format!(r#"["vectorcraft",3,{{"layers":[]}},{images}]"#), r#"["vectorcraft",3,{"layers":[]}]"#.into()] { let contents = Contents::::read(json.as_bytes()).unwrap_or_else(|e| panic!("{json}: {e}")); let file = File::::read(json.as_bytes()).unwrap_or_else(|e| panic!("{json}: {e}")); assert_eq!( (&file.format, file.version, &file.document, &file.images, &file.profiles), (&contents.format, 3, &contents.document, &contents.images, &contents.profiles) ); assert!(file.generator.is_none() && file.preview.is_none() && file.pdf.is_none()); } let contents = Contents::::read(full.as_bytes()).unwrap(); assert_eq!((contents.images.len(), contents.profiles.len()), (1, 1)); assert!(contents.images.contains_key("k") && contents.profiles.contains_key("Gray")); for json in [r#"["vectorcraft",3,{},{"layers":[]},{}]"#, r#"["vectorcraft",3]"#, r#"["vectorcraft",5,{"layers":[]}]"#] { assert!(Contents::::read(json.as_bytes()).is_err(), "{json}"); assert!(File::::read(json.as_bytes()).is_err(), "{json}"); } } #[test] fn a_files_tail_follows_its_version() { let mut file = File::new(doc()); let pdf = pdf(); assert_eq!(file.tail().image("application/pdf", &pdf, false).blob, Some([0, pdf.len() as u64])); file.version = 2; assert!(file.tail().image("application/pdf", &pdf, false).blob.is_none()); } #[test] fn the_json_is_split_only_at_a_marker_right_after_it() { let with = |json: &[u8], rest: &[u8]| { let mut f = json.to_vec(); f.extend(BLOB_MAGIC); f.extend(rest); f }; for json in [&b"{}"[..], b"[1]", b"\"s\"", b"1", b"{\"a\": [true, null]}"] { let f = with(json, b"xyz"); assert_eq!(split_tail(&f), (json, &b"xyz"[..]), "{json:?}"); } let empty: &[u8] = &[]; let far = with(b"{} x", b"xyz"); assert_eq!(split_tail(&far), (far.as_slice(), empty)); let no_marker = b"{}\0rest".to_vec(); assert_eq!(split_tail(&no_marker), (no_marker.as_slice(), empty)); assert_eq!(split_tail(b"{}"), (&b"{}"[..], empty)); let bad = with(b"{", b"x"); assert_eq!(split_tail(&bad), (bad.as_slice(), empty)); } #[test] fn the_marker_is_found_where_the_writer_puts_it_in_the_first_bytes() { let mut file = br#"{"a":[1]}"#.to_vec(); file.extend(BLOB_MAGIC); file.extend(b"\0rest"); // The first bytes up to the end of the marker are enough. assert_eq!(marker_at(&file), Some(9)); assert_eq!(marker_at(&file[..9 + BLOB_MAGIC.len()]), Some(9)); assert_eq!(marker_at(&file[..9 + BLOB_MAGIC.len() - 1]), None); assert_eq!(split_tail(&file), (&file[..9], &b"\0rest"[..])); for json in [&b"\"s\""[..], b"[]"] { let mut f = json.to_vec(); f.extend(BLOB_MAGIC); assert_eq!(marker_at(&f), Some(json.len()), "{json:?}"); } // After a number, after a space, without its newline or with a NUL before it, the marker is // not where the writer puts it. for f in [&b"1\n\0VCBLOBS\0"[..], b"{} \n\0VCBLOBS\0", b"{}\0VCBLOBS\0", b"{}\0\n\0VCBLOBS\0", b"\n\0VCBLOBS\0", b"{}"] { assert_eq!(marker_at(f), None, "{:?}", String::from_utf8_lossy(f)); } } #[test] fn check_refuses_other_formats_and_newer_versions() { assert_eq!(check(EXTENSION, VERSION), Ok(())); assert_eq!(check(LEGACY_EXTENSION, 1), Ok(())); assert_eq!(check("other", 1), Err(Error::Format("other".into()))); assert_eq!(check(EXTENSION, VERSION + 1), Err(Error::TooNew(VERSION + 1))); assert!(File::::read(br#"{"format":"vectorcraft","version":4}"#).is_err()); let minimal = File::::read(br#"{"format":"vectorcraft","version":4,"document":{"layers":[]}}"#).unwrap(); assert!(minimal.images.is_empty() && minimal.generator.is_none()); } #[test] fn sniff_needs_a_vectorcraft_format_member() { let pdf = pdf(); let (file, tail) = full_file(&pdf); let plain = written(&file, &tail, false); let pretty = written(&file, &tail, true); assert!(sniff(&plain) && sniff(&pretty)); assert!(sniff(b" \n{\"format\" : \"drawcraft\"}")); // Keys and values are compared as JSON reads them, escapes decoded. let esc = |s: &str| s.replace('|', "\\"); let escaped = esc(r#"{"|u0066ormat":"vector|u0063raft","version":4,"document":{"layers":[]}}"#); assert!(sniff(escaped.as_bytes())); assert_eq!(File::::read(escaped.as_bytes()).unwrap().format, EXTENSION); assert!(sniff(esc(r#"{"format":"draw|u0063raft"}"#).as_bytes())); for no in [r#"{"form|u0061t":"other","format":"vectorcraft"}"#, r#"{"format":"vectorcraft|u0021"}"#, r#"{"format":"vectorcraft|"}"#] { assert!(!sniff(esc(no).as_bytes()), "{no}"); } assert!(sniff(br#"{"generator":"format","x":{"format":"other","a":[1,"]"]},"n":-1.5e3,"t":true,"s":"\"format\":","format":"vectorcraft"}"#)); assert!(is_compressed(&gzip(&plain)) && !is_compressed(&plain)); assert!(sniff(&gzip(&plain))); let gz = gzip(&plain); assert!(sniff(&gz[..gz.len() / 2])); for no in [ &br#"{"name":"vectorcraft"}"#[..], br#"{"format":"other","vectorcraft":1}"#, br#"["format","vectorcraft"]"#, br#"{"metadata":{"format":"vectorcraft"},"format":"other"}"#, br#"{"name":"\"format\":\"vectorcraft\""}"#, br#"{"format":["vectorcraft"]}"#, br#"{"format":"vectorcraft"#, b"%PDF-1.7", b"", b"\x1f\x8b", PNG, ] { assert!(!sniff(no), "{:?}", String::from_utf8_lossy(no)); } } #[test] fn base64_is_the_encoding_of_data_members() { assert_eq!(base64::encode(PNG), "iVBORw0KGgo="); assert_eq!(base64::decode("AQID+g==").unwrap(), [1, 2, 3, 250]); }