mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
PSD integration test: deep groups + NaN text, damaged layer section, huge header under a small budget, on a 2 MiB stack
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Brandon Thomas
co-authored by
Claude Sonnet 5.5
parent
f2d481ce20
commit
5f4b3aee86
@@ -1494,6 +1494,8 @@ mod memory_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_integration;
|
||||
#[cfg(test)]
|
||||
mod tests_opt;
|
||||
|
||||
/// Test-only call counters (per thread), to prove what the optimised paths avoid doing.
|
||||
|
||||
@@ -265,7 +265,7 @@ fn pixels_match_the_one_buffer_per_channel_reference() {
|
||||
}
|
||||
|
||||
/// A copy of `bytes` with a different size, depth and merged-image section (`comp`, then `body`).
|
||||
fn with_merged(bytes: &[u8], size: Option<(u32, u32)>, depth: Option<u16>, comp: u16, body: &[u8]) -> Vec<u8> {
|
||||
pub(crate) fn with_merged(bytes: &[u8], size: Option<(u32, u32)>, depth: Option<u16>, comp: u16, body: &[u8]) -> Vec<u8> {
|
||||
let offset = Psd::parse(bytes.to_vec()).unwrap().merged_offset;
|
||||
let mut v = bytes[..offset].to_vec();
|
||||
if let Some((w, h)) = size {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//! The hardening of the Photoshop reader working together: one hostile file (groups nested past
|
||||
//! the limit, a NaN text transform), a damaged layer section, and a tiny file claiming a huge
|
||||
//! image. Each ends in a clean result or a clean error, on a 2 MiB stack, never a panic.
|
||||
|
||||
use super::memory_tests::with_merged;
|
||||
use super::write::*;
|
||||
use super::*;
|
||||
|
||||
/// Run `f` on a 2 MiB thread; a panic or stack overflow in it fails the test.
|
||||
fn on_small_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
std::thread::Builder::new().stack_size(2 << 20).spawn(f).expect("spawn").join().expect("no panic")
|
||||
}
|
||||
|
||||
fn depth_of(nodes: &[Node]) -> usize {
|
||||
nodes.iter().map(|n| if let Node::Group { children, .. } = n { 1 + depth_of(children) } else { 0 }).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hostile_file_with_deep_groups_and_a_nan_text_value_loads_and_decodes() {
|
||||
let n = 2 * MAX_GROUP_DEPTH + 36;
|
||||
let mut d = WDoc::new(8, 8);
|
||||
// File order is bottom first: the section ends open the nest, the group headers close it.
|
||||
d.layers = (0..n).map(|_| WLayer::group_end()).collect();
|
||||
for i in 0..n {
|
||||
d.layers.push(WLayer::solid(&format!("L{i}"), Rect::new(0, 0, 2, 2), [1.0, 0.0, 0.0, 1.0]));
|
||||
d.layers.push(WLayer::group(&format!("G{i}"), true, *b"norm"));
|
||||
}
|
||||
d.layers.push(WLayer::empty("Title", vec![text_block("x", [f64::NAN, f64::INFINITY], "Inter", 12.0, [0.0; 3], 0)]));
|
||||
let bytes = write(&d);
|
||||
let (depth, flattened, finite, ok) = on_small_stack(move || {
|
||||
let p = Psd::parse(bytes).expect("parses");
|
||||
let (tree, flattened) = p.tree_report();
|
||||
let title = p.layers.iter().find(|l| l.name == "Title").and_then(|l| l.text.as_ref());
|
||||
let finite = title.is_some_and(|t| t.transform.iter().all(|v| v.is_finite()));
|
||||
// Every layer and the merged image decode to a result or an error, and results are finite.
|
||||
let mut ok = p.composite().map_or(true, |px| px.data.iter().flatten().all(|v| v.is_finite()));
|
||||
for i in 0..p.layers.len() {
|
||||
ok &= p.layer_pixels(i, false).map_or(true, |px| px.data.iter().flatten().all(|v| v.is_finite()));
|
||||
}
|
||||
(depth_of(&tree), flattened, finite, ok)
|
||||
});
|
||||
assert!(depth <= MAX_GROUP_DEPTH, "{depth}");
|
||||
assert!(flattened > 0);
|
||||
assert!(finite, "the NaN transform fell back to the identity");
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_layer_section_falls_back_to_the_merged_image() {
|
||||
let mut d = WDoc::new(30, 20);
|
||||
d.layers = vec![
|
||||
WLayer::solid("Back", Rect::new(0, 0, 30, 20), [0.0, 0.0, 1.0, 1.0]),
|
||||
WLayer::solid("Box", Rect::new(2, 2, 8, 8), [1.0, 0.0, 0.0, 1.0]).with_blend(b"mul "),
|
||||
];
|
||||
let mut bytes = write(&d);
|
||||
let at = bytes.windows(8).position(|w| w == b"8BIMmul ").expect("blend signature");
|
||||
bytes[at] = b'X';
|
||||
let (layers, warnings, size, composite_ok) = on_small_stack(move || {
|
||||
let p = Psd::parse(bytes).expect("lenient");
|
||||
(p.layers.len(), p.warnings.clone(), (p.width, p.height), p.composite().is_ok())
|
||||
});
|
||||
assert_eq!(layers, 0);
|
||||
assert_eq!(warnings.len(), 1, "{warnings:?}");
|
||||
assert!(warnings[0].contains("Layers could not be read"));
|
||||
assert_eq!(size, (30, 20));
|
||||
assert!(composite_ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tiny_file_claiming_a_huge_image_is_refused_within_the_budget() {
|
||||
let mut small = WDoc::new(2, 2);
|
||||
small.rle = false;
|
||||
small.layers = vec![WLayer::solid("L", Rect::new(0, 0, 2, 2), [1.0, 0.0, 0.0, 1.0])];
|
||||
let base = write(&small);
|
||||
let stream = miniz_oxide::deflate::compress_to_vec_zlib(&vec![0u8; 1 << 20], 9);
|
||||
for (w, h, comp) in [(16384u32, 16384u32, 2u16), (16384, 16384, 3), (4096, 4096, 2), (4096, 4096, 3), (16384, 16384, 1), (16384, 16384, 0)] {
|
||||
let bytes = with_merged(&base, Some((w, h)), None, comp, &stream);
|
||||
assert!(bytes.len() < 8192);
|
||||
let outcome = on_small_stack(move || {
|
||||
let p = Psd::parse(bytes).expect("header and layers parse");
|
||||
// A small test budget: one MiB, far under what the claimed image needs.
|
||||
let merged = p.composite_with_budget(1 << 20).map(|_| ()).map_err(|e| e.to_string());
|
||||
let layer = p.layer_pixels_with_budget(0, true, 1 << 20).map(|_| ()).map_err(|e| e.to_string());
|
||||
let default_budget = p.composite().map(|_| ()).map_err(|e| e.to_string());
|
||||
(merged, layer, default_budget)
|
||||
});
|
||||
let (merged, layer, default_budget) = outcome;
|
||||
assert!(merged.as_ref().is_err_and(|e| e.contains("memory budget")), "{w}x{h} comp {comp}: {merged:?}");
|
||||
assert!(layer.as_ref().is_err_and(|e| e.contains("memory budget")), "{w}x{h} comp {comp}: {layer:?}");
|
||||
if w == 16384 {
|
||||
assert!(default_budget.as_ref().is_err_and(|e| e.contains("memory budget")), "comp {comp}: {default_budget:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user