mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
274 lines
13 KiB
Rust
274 lines
13 KiB
Rust
//! Synthetic PhotoCraft ICC payloads: the declared bytes are opaque, content-addressed blobs.
|
|
#![cfg(feature = "pcraft-pixels")]
|
|
|
|
mod common;
|
|
|
|
use common::{deflated, store, zip};
|
|
use craft_doc::{Error, Limits, pcraft_icc_profile};
|
|
|
|
/// Blob identity is over the decoded payload, not its compressed representation.
|
|
fn hash(bytes: &[u8]) -> String {
|
|
blake3::hash(bytes).to_hex().to_string()
|
|
}
|
|
|
|
/// The producer's streaming encoder omits a declared content size.
|
|
fn frame(bytes: &[u8]) -> Vec<u8> {
|
|
ruzstd::encoding::compress_to_vec(bytes, ruzstd::encoding::CompressionLevel::Fastest)
|
|
}
|
|
|
|
/// A minimal checked envelope around the document chosen by each regression.
|
|
fn manifest(document: &str) -> String {
|
|
format!(r#"{{"format_version":1,"generator":"synthetic","document":{document}}}"#)
|
|
}
|
|
|
|
/// A profile read needs no canvas or layer metadata.
|
|
fn named_manifest(hash: &str) -> String {
|
|
manifest(&format!(r#"{{"icc_profile":"{hash}"}}"#))
|
|
}
|
|
|
|
/// Store a blob at its content address without assuming its bytes are a valid ICC profile.
|
|
fn bundle(payload: &[u8]) -> Vec<u8> {
|
|
let name = craft_pcraft_format::blob_path(&hash(payload)).unwrap();
|
|
zip(&[store("manifest.json", named_manifest(&hash(payload)).as_bytes()), store(&name, &frame(payload))])
|
|
}
|
|
|
|
#[test]
|
|
/// Native absence is missing or null; declared blobs remain opaque, including empty ones.
|
|
fn absent_and_null_are_none_but_declared_payloads_are_returned_without_semantic_checks() {
|
|
for document in ["{}", r#"{"icc_profile":null}"#] {
|
|
assert_eq!(pcraft_icc_profile(&zip(&[store("manifest.json", manifest(document).as_bytes())]), &Limits::default()), Ok(None));
|
|
}
|
|
for payload in [&b"not an ICC header"[..], &b""[..], &[0, 1, 2, 3][..]] {
|
|
assert_eq!(pcraft_icc_profile(&bundle(payload), &Limits::default()), Ok(Some(payload.to_vec())));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Either selected ZIP entry may use the producer's supported outer compression method.
|
|
fn store_and_deflate_entries_use_the_same_bounded_blob_reader() {
|
|
let payload = b"synthetic profile bytes";
|
|
let h = hash(payload);
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
let z = frame(payload);
|
|
for archive in
|
|
[zip(&[store("manifest.json", m.as_bytes()), deflated(&path, &z)]), zip(&[deflated("manifest.json", m.as_bytes()), store(&path, &z)])]
|
|
{
|
|
assert_eq!(pcraft_icc_profile(&archive, &Limits::default()), Ok(Some(payload.to_vec())));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Bad references and non-object documents are errors rather than an absent profile.
|
|
fn invalid_references_types_and_duplicate_profile_members_are_malformed() {
|
|
let h = hash(b"p");
|
|
for document in [
|
|
r#"{"icc_profile":""}"#.to_owned(),
|
|
format!(r#"{{"icc_profile":"{}"}}"#, h.to_uppercase()),
|
|
r#"{"icc_profile":"../manifest.json"}"#.to_owned(),
|
|
r#"{"icc_profile":3}"#.to_owned(),
|
|
r#"{"icc_profile":[]}"#.to_owned(),
|
|
r#"{"icc_profile":null,"icc_profile":null}"#.to_owned(),
|
|
format!(r#"{{"icc_profile":"{h}","icc_profile":null}}"#),
|
|
"[]".to_owned(),
|
|
"null".to_owned(),
|
|
] {
|
|
let archive = zip(&[store("manifest.json", manifest(&document).as_bytes())]);
|
|
assert!(matches!(pcraft_icc_profile(&archive, &Limits::default()), Err(Error::Malformed(_))), "{document}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// A declared payload must exist, match its content address and occupy exactly one frame.
|
|
fn missing_wrong_hash_or_damaged_declared_blobs_are_errors() {
|
|
let h = hash(b"wanted");
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
let valid = frame(b"wanted");
|
|
let mut trailing = valid.clone();
|
|
trailing.push(0);
|
|
let mut multiple = valid.clone();
|
|
multiple.extend_from_slice(&frame(b"second"));
|
|
for archive in [
|
|
zip(&[store("manifest.json", m.as_bytes())]),
|
|
zip(&[store("manifest.json", m.as_bytes()), store(&path, &frame(b"other"))]),
|
|
zip(&[store("manifest.json", m.as_bytes()), store(&path, b"not zstd")]),
|
|
zip(&[store("manifest.json", m.as_bytes()), store(&path, &valid[..valid.len() - 1])]),
|
|
zip(&[store("manifest.json", m.as_bytes()), store(&path, &trailing)]),
|
|
zip(&[store("manifest.json", m.as_bytes()), store(&path, &multiple)]),
|
|
] {
|
|
assert!(matches!(pcraft_icc_profile(&archive, &Limits::default()), Err(Error::Malformed(_))));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Unused previews and tile payloads cannot prevent reading an independently valid profile.
|
|
fn unrelated_payloads_are_never_read_and_document_metadata_is_skipped() {
|
|
let payload = b"profile";
|
|
let h = hash(payload);
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = format!(
|
|
r#"{{"format_version":1,"generator":"synthetic","thumbnail":"thumb.png","composite":"composite/preview.png",
|
|
"document":{{"icc_profile":"{h}","layers":[{{"content":{{"surface":{{"tiles":[{{"hash":"invalid"}}]}}}}}}],"future":[{{"ignored":true}}]}}}}"#
|
|
);
|
|
let archive = zip(&[store("manifest.json", m.as_bytes()), store(&path, &frame(payload)), store("tiles/invalid.zst", b"broken tile")]);
|
|
assert_eq!(pcraft_icc_profile(&archive, &Limits::default()), Ok(Some(payload.to_vec())));
|
|
}
|
|
|
|
#[test]
|
|
/// Accepted work is exactly the manifest plus selected frame plus decoded payload.
|
|
fn manifest_frame_and_decoded_profile_have_exact_total_and_entry_boundaries() {
|
|
let payload = vec![7; 32 << 10];
|
|
let h = hash(&payload);
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
let z = frame(&payload);
|
|
let archive = zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]);
|
|
let mut limits = Limits::default();
|
|
limits.max_json = m.len() as u64;
|
|
limits.max_entry = payload.len() as u64;
|
|
limits.max_total = (m.len() + z.len() + payload.len()) as u64;
|
|
assert_eq!(pcraft_icc_profile(&archive, &limits), Ok(Some(payload.clone())));
|
|
limits.max_total -= 1;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
limits.max_total += 1;
|
|
limits.max_entry -= 1;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
limits.max_entry += 1;
|
|
limits.max_json -= 1;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
let absent = manifest("{}");
|
|
limits.max_json = absent.len() as u64;
|
|
limits.max_total = absent.len() as u64;
|
|
assert_eq!(pcraft_icc_profile(&zip(&[store("manifest.json", absent.as_bytes())]), &limits), Ok(None));
|
|
limits.max_total -= 1;
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", absent.as_bytes())]), &limits), Err(Error::LimitExceeded(_))));
|
|
}
|
|
|
|
#[test]
|
|
/// The frame's ZIP size must fail admission even if its bytes would also fail decoding.
|
|
fn an_oversized_frame_is_refused_before_the_zstd_decoder() {
|
|
let m = named_manifest(&hash(b"p"));
|
|
let path = craft_pcraft_format::blob_path(&hash(b"p")).unwrap();
|
|
let mut limits = Limits::default();
|
|
limits.max_entry = m.len() as u64;
|
|
let archive = zip(&[store("manifest.json", m.as_bytes()), store(&path, &vec![0; m.len() + 1])]);
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
}
|
|
|
|
#[test]
|
|
/// Skipping document values must not let a small typed head evade the caller's item cap.
|
|
fn structural_item_limits_include_skipped_fields_before_the_profile_head() {
|
|
let archive = bundle(b"p");
|
|
let mut limits = Limits::default();
|
|
limits.max_items = 4; // root object, its two commas and document object
|
|
assert_eq!(pcraft_icc_profile(&archive, &limits), Ok(Some(b"p".to_vec())));
|
|
limits.max_items = 3;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
let m = manifest(r#"{"icc_profile":null,"ignored":[0,0,0,0,0]}"#);
|
|
let archive = zip(&[store("manifest.json", m.as_bytes())]);
|
|
limits.max_items = 4;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
limits.max_items = 0;
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", manifest("{}").as_bytes())]), &limits), Err(Error::LimitExceeded(_))));
|
|
}
|
|
|
|
#[test]
|
|
/// Absence of a profile cannot bypass kind, envelope, format-version or nesting checks.
|
|
fn versions_kinds_envelope_and_depth_are_checked_even_without_a_profile() {
|
|
assert_eq!(pcraft_icc_profile(b"not a bundle", &Limits::default()), Err(Error::Unrecognized));
|
|
assert!(matches!(pcraft_icc_profile(common::vectorcraft_json(4, "").as_bytes(), &Limits::default()), Err(Error::Unsupported(_))));
|
|
let m = manifest("{}").replace("\"format_version\":1", "\"format_version\":2");
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes())]), &Limits::default()), Err(Error::TooNew { .. })));
|
|
for m in [
|
|
r#"{"format_version":1,"generator":"a","generator":"b","document":{}}"#,
|
|
r#"{"format_version":1,"generator":"a","document":{},"thumbnail":"elsewhere"}"#,
|
|
] {
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes())]), &Limits::default()), Err(Error::Malformed(_))));
|
|
}
|
|
let m = manifest(r#"{"ignored":[[[0]]]}"#);
|
|
let mut limits = Limits::default();
|
|
limits.max_depth = 3;
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes())]), &limits), Err(Error::LimitExceeded(_))));
|
|
}
|
|
|
|
#[test]
|
|
/// Header-window limits and streamed output admission remain effective without a size hint.
|
|
fn excessive_window_and_size_less_expansion_are_limits() {
|
|
let payload = vec![0; 64 << 10];
|
|
let h = hash(&payload);
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
let z = frame(&payload);
|
|
// Producer frames omit their content size; output admission must work without that hint.
|
|
assert_eq!(z[4] & 0xc0, 0);
|
|
let mut limits = Limits::default();
|
|
limits.max_entry = 1024;
|
|
assert!(matches!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]), &limits), Err(Error::LimitExceeded(_))));
|
|
let mut large_window = z.clone();
|
|
assert_eq!(large_window[4] & 0x20, 0);
|
|
large_window[5] = 14 << 3; // 16 MiB
|
|
assert!(matches!(
|
|
pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &large_window)]), &Limits::default()),
|
|
Err(Error::LimitExceeded(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
/// Nonzero size hints permit early refusal but never replace checking actual decoded bytes.
|
|
fn declared_nonzero_content_sizes_are_checked_before_and_after_decoding() {
|
|
let h = hash(b"p");
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
// A single-segment frame declaring 4096 bytes, but holding one raw byte.
|
|
let mut z = vec![0x28, 0xb5, 0x2f, 0xfd, 0x60];
|
|
z.extend_from_slice(&(4096u16 - 256).to_le_bytes());
|
|
z.extend_from_slice(&[9, 0, 0, b'p']);
|
|
let archive = zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]);
|
|
let mut limits = Limits::default();
|
|
limits.max_entry = 1024;
|
|
assert!(matches!(pcraft_icc_profile(&archive, &limits), Err(Error::LimitExceeded(_))));
|
|
assert!(matches!(pcraft_icc_profile(&archive, &Limits::default()), Err(Error::Malformed(_))));
|
|
let z = [0x28, 0xb5, 0x2f, 0xfd, 0x20, 1, 9, 0, 0, b'p'];
|
|
assert_eq!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]), &limits), Ok(Some(b"p".to_vec())));
|
|
}
|
|
|
|
#[test]
|
|
/// A present zero size permits an empty payload, while an omitted size permits streamed data.
|
|
fn explicit_zero_content_sizes_match_only_empty_payloads() {
|
|
// A two-byte FCS has an offset of 256, so zero uses one, four or eight bytes.
|
|
for (descriptor, width, single) in [(0x80, 4, false), (0xc0, 8, false), (0x20, 1, true), (0xa0, 4, true), (0xe0, 8, true)] {
|
|
for payload in [b"p".as_slice(), b"".as_slice()] {
|
|
let h = hash(payload);
|
|
let path = craft_pcraft_format::blob_path(&h).unwrap();
|
|
let m = named_manifest(&h);
|
|
let mut z = vec![0x28, 0xb5, 0x2f, 0xfd, descriptor];
|
|
if !single {
|
|
z.push(0); // 1 KiB window
|
|
}
|
|
z.extend(std::iter::repeat_n(0, width));
|
|
z.extend_from_slice(&[if payload.is_empty() { 1 } else { 9 }, 0, 0]);
|
|
z.extend_from_slice(payload);
|
|
let result = pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]), &Limits::default());
|
|
if payload.is_empty() {
|
|
assert_eq!(result, Ok(Some(Vec::new())), "descriptor {descriptor:#x}");
|
|
} else {
|
|
assert!(matches!(result, Err(Error::Malformed(_))), "descriptor {descriptor:#x}: {result:?}");
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(pcraft_icc_profile(&bundle(b"p"), &Limits::default()), Ok(Some(b"p".to_vec())), "unknown content size still streams");
|
|
}
|
|
|
|
#[test]
|
|
/// Damaged envelopes and frames fail through the public accessor without escaping a panic.
|
|
fn truncations_and_mutations_do_not_panic() {
|
|
let archive = bundle(b"synthetic profile");
|
|
for i in 0..archive.len() {
|
|
assert!(pcraft_icc_profile(&archive[..i], &Limits::default()).is_err());
|
|
let mut changed = archive.clone();
|
|
changed[i] ^= 0xff;
|
|
let _ = pcraft_icc_profile(&changed, &Limits::default());
|
|
}
|
|
}
|