Files
vectorcraft/crates/format/tests/it/native_v4.rs
T
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00

90 lines
4.1 KiB
Rust

//! `.vectorcraft` v4: large data after the JSON (images, the PDF, profiles), small files still plain
//! JSON, older versions inline, damaged offsets refused.
use std::sync::Arc;
use serde_json::Value;
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
use vectorcraft_format::{BLOB_MAGIC, FormatError, INLINE_MAX, SaveOptions, load, load_file, pdf_content, save_with};
use vectorcraft_geom::Affine;
/// A document showing image `key` of `len` bytes.
fn doc_with_image(key: &str, len: usize) -> Document {
let mut d = Document::new(100.0, 100.0);
let bytes: Vec<u8> = (0..len).map(|i| (i * 7 % 251) as u8).collect();
d.images.insert(key.into(), ImageBlob::new("image/png", bytes));
let l = d.layers[0].id;
let id = d.alloc_id();
let im = ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
d
}
fn has_tail(bytes: &[u8]) -> bool {
bytes.windows(BLOB_MAGIC.len()).any(|w| w == BLOB_MAGIC)
}
#[test]
fn large_data_goes_after_the_json_and_reads_back() {
let d = doc_with_image("big", INLINE_MAX + 10);
let pdf = vec![b'%'; INLINE_MAX * 2];
for compress in [false, true] {
let o = SaveOptions { compress, pdf: Some(pdf.clone()), ..SaveOptions::default() };
let bytes = save_with(&d, &o).unwrap();
if !compress {
assert!(has_tail(&bytes), "a binary section");
// The JSON before it holds no base64 copy of the image.
assert!(bytes.len() < INLINE_MAX + 10 + pdf.len() + (32 << 10), "{} bytes", bytes.len());
}
let back = load(&bytes).unwrap();
assert_eq!(back.images["big"].bytes, d.images["big"].bytes, "compress {compress}");
assert_eq!(pdf_content(&bytes).unwrap(), pdf);
}
}
#[test]
fn small_files_stay_plain_json_and_older_versions_inline() {
let small = save_with(&doc_with_image("small", 100), &SaveOptions::default()).unwrap();
assert!(!has_tail(&small));
let v: Value = serde_json::from_slice(&small).unwrap();
assert_eq!(v["version"], 4);
let big = doc_with_image("big", INLINE_MAX + 10);
let v3 = save_with(&big, &SaveOptions { version: 3, ..SaveOptions::default() }).unwrap();
assert!(!has_tail(&v3), "version 3 keeps everything in the JSON");
let v: Value = serde_json::from_slice(&v3).unwrap();
assert_eq!((v["format"].as_str(), v["version"].as_u64()), (Some("vectorcraft"), Some(3)));
assert_eq!(load(&v3).unwrap().images["big"].bytes, big.images["big"].bytes);
}
/// `json_part` (what exporter plug-ins are given): the whole JSON value, without the data after
/// it; a file without such data is its JSON already.
#[test]
fn json_part_is_the_json_without_the_data_after_it() {
let big = save_with(&doc_with_image("big", INLINE_MAX + 10), &SaveOptions::default()).unwrap();
let json = vectorcraft_format::json_part(&big);
assert!(json.len() < big.len() - INLINE_MAX && !has_tail(json));
let v: Value = serde_json::from_slice(json).unwrap();
assert!(v["images"]["big"].get("blob").is_some() && v["images"]["big"].get("data").is_none(), "{}", v["images"]);
let small = save_with(&doc_with_image("small", 100), &SaveOptions::default()).unwrap();
assert_eq!(vectorcraft_format::json_part(&small), &small[..]);
}
#[test]
fn damaged_offsets_are_refused_not_a_crash() {
let d = doc_with_image("big", INLINE_MAX + 10);
let bytes = save_with(&d, &SaveOptions::default()).unwrap();
// The data cut short.
let cut = &bytes[..bytes.len() - 100];
assert!(matches!(load_file(cut), Err(FormatError::BadImage(_))));
// An offset past the end.
let text = String::from_utf8_lossy(&bytes);
let at = text.find("\"blob\":[").unwrap();
let mut damaged = bytes.clone();
damaged.splice(at..at + 8, b"\"blob\":[9".iter().copied());
assert!(load_file(&damaged).is_err());
// A missing section.
let json_only = &bytes[..bytes.windows(BLOB_MAGIC.len()).position(|w| w == BLOB_MAGIC).unwrap()];
assert!(load_file(json_only).is_err());
let _ = Arc::new(());
}