mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 18:13:39 +00:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # crates/engine/src/file_cmds.rs # crates/engine/src/retouch_cmds.rs # crates/engine/src/vector_cmds.rs # crates/io/src/psd_export.rs # crates/io/src/psd_import.rs # crates/io/src/smart_map.rs # crates/psd/src/layer.rs # crates/raster/src/lib.rs # crates/ui-egui/src/camera_raw_ui.rs # crates/ui-egui/src/canvas.rs # crates/ui-egui/src/filter_dialog.rs # crates/ui-egui/src/gpu_canvas.rs # crates/ui-egui/src/panels.rs # crates/ui-egui/src/retouch_ui.rs # crates/ui-egui/src/transform_tool.rs # crates/ui-egui/src/vector_ui.rs # docs/gaps.md
This commit is contained in:
+1
-300
@@ -673,303 +673,4 @@ pub fn write_v6_folders(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::descriptor::UnicodeString;
|
||||
|
||||
fn sample(id: &str, w: u32, h: u32, depth: u16) -> AbrSample {
|
||||
let bpp = usize::from(depth / 8);
|
||||
let data = (0..w as usize * h as usize * bpp).map(|i| if (i / bpp) % 3 == 0 { 255 } else { (i % 7) as u8 * 30 }).collect();
|
||||
AbrSample { id: id.into(), width: w, height: h, depth, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_patterns_spend_the_samples_budget() {
|
||||
let p = PsdPattern {
|
||||
mode: 3,
|
||||
width: 5,
|
||||
height: 3,
|
||||
name: "p".into(),
|
||||
id: "id".into(),
|
||||
palette: None,
|
||||
depth: 8,
|
||||
channels: vec![vec![1; 15], vec![2; 15], vec![3; 15]],
|
||||
alpha: None,
|
||||
};
|
||||
let abr = write_v6(2, &[sample("s", 10, 10, 8)], &[p.clone(), p], &[], true).unwrap();
|
||||
let parse_within = |left: usize| {
|
||||
let mut out = AbrFile::default();
|
||||
let mut budget = Budget(left);
|
||||
parse_v6_within(&mut Rd::new(&abr[2..]), &mut out, &mut budget).unwrap();
|
||||
(out, budget.0)
|
||||
};
|
||||
// 100 sample bytes plus two 45-byte patterns.
|
||||
let (out, left) = parse_within(190);
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 2, 0));
|
||||
// One byte short: the patterns no longer fit after the sample (they used not to count).
|
||||
let (out, left) = parse_within(189);
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 0, 14));
|
||||
assert!(out.warnings.iter().any(|w| w.contains("embedded patterns unreadable")), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_section_does_not_refund_budget_to_later_sections() {
|
||||
let p = PsdPattern {
|
||||
mode: 3,
|
||||
width: 5,
|
||||
height: 3,
|
||||
name: "p".into(),
|
||||
id: "id".into(),
|
||||
palette: None,
|
||||
depth: 8,
|
||||
channels: vec![vec![1; 15], vec![2; 15], vec![3; 15]],
|
||||
alpha: None,
|
||||
};
|
||||
let valid = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let mut malformed = valid.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
|
||||
let mut body = 2u16.to_be_bytes().to_vec();
|
||||
section(&mut body, b"patt", &malformed);
|
||||
section(&mut body, b"patt", &valid);
|
||||
let mut out = AbrFile::default();
|
||||
let mut budget = Budget(60);
|
||||
parse_v6_within(&mut Rd::new(&body), &mut out, &mut budget).unwrap();
|
||||
|
||||
assert!(out.patterns.is_empty(), "a failed block is discarded atomically");
|
||||
assert_eq!(budget.0, 0, "the failed block's first 45 bytes and later partial 15-byte charge are retained");
|
||||
assert_eq!(out.warnings.iter().filter(|w| w.contains("embedded patterns unreadable")).count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_v2_round_trip_raw_and_rle() {
|
||||
for version in [1u16, 2] {
|
||||
for rle in [false, true] {
|
||||
let brushes = vec![
|
||||
LegacyBrush {
|
||||
name: String::new(),
|
||||
spacing: 25,
|
||||
anti_alias: true,
|
||||
tip: LegacyTip::Computed { diameter: 19, hardness: 80, angle: -30, roundness: 50 },
|
||||
},
|
||||
LegacyBrush {
|
||||
name: if version == 2 { "Leaf ✓".into() } else { String::new() },
|
||||
spacing: 40,
|
||||
anti_alias: false,
|
||||
tip: LegacyTip::Sampled(sample("", 13, 9, 8)),
|
||||
},
|
||||
LegacyBrush { name: String::new(), spacing: 10, anti_alias: true, tip: LegacyTip::Sampled(sample("", 4, 6, 16)) },
|
||||
];
|
||||
let bytes = write_v12(version, &brushes, rle).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.version, version);
|
||||
assert_eq!(f.legacy, brushes, "v{version} rle={rle}");
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_round_trip_both_subversions() {
|
||||
let samples = vec![sample("$abc", 20, 11, 8), sample("$def", 5, 5, 16)];
|
||||
let preset = Descriptor::new("brushPreset")
|
||||
.with("Nm ", Value::Text(UnicodeString::new_nul("Grass")))
|
||||
.with("Brsh", Value::Descriptor(Descriptor::new("sampledBrush").with("sampledData", Value::Text(UnicodeString::new_nul("$abc")))));
|
||||
for sub in [1u16, 2] {
|
||||
for rle in [false, true] {
|
||||
let bytes = write_v6(sub, &samples, &[], std::slice::from_ref(&preset), rle).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!((f.version, f.subversion), (6, sub));
|
||||
assert_eq!(f.samples, samples);
|
||||
assert_eq!(f.presets, vec![preset.clone()]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
}
|
||||
assert_eq!(samples[1].to_unit().len(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_input_errors_without_panicking() {
|
||||
assert!(parse(&[]).is_err());
|
||||
assert!(parse(&[0, 3, 0, 0]).is_err());
|
||||
assert!(parse(&[0, 6, 0, 9]).is_err());
|
||||
// Absurd sample bounds.
|
||||
let mut s = sample("", 2, 2, 8);
|
||||
s.width = MAX_EDGE + 1;
|
||||
assert!(write_v12(1, &[LegacyBrush { name: String::new(), spacing: 1, anti_alias: true, tip: LegacyTip::Sampled(s) }], false).is_err());
|
||||
let good = write_v6(2, &[sample("$a", 30, 30, 8)], &[], &[Descriptor::new("brushPreset")], true).unwrap();
|
||||
for cut in 0..good.len() {
|
||||
let _ = parse(&good[..cut]);
|
||||
}
|
||||
let v2 = write_v12(2, &[LegacyBrush { name: "x".into(), spacing: 1, anti_alias: true, tip: LegacyTip::Sampled(sample("", 9, 9, 16)) }], true).unwrap();
|
||||
for cut in 0..v2.len() {
|
||||
let _ = parse(&v2[..cut]);
|
||||
}
|
||||
// Flip every byte once: never panics.
|
||||
for i in 0..good.len() {
|
||||
let mut b = good.clone();
|
||||
b[i] ^= 0xa5;
|
||||
let _ = parse(&b);
|
||||
}
|
||||
// A huge declared count with no data is an error, not an allocation.
|
||||
assert!(parse(&[0, 2, 0xff, 0xff]).is_err());
|
||||
}
|
||||
|
||||
fn named(n: &str) -> Descriptor {
|
||||
Descriptor::new("brushPreset").with("Nm ", Value::Text(UnicodeString::new_nul(n)))
|
||||
}
|
||||
|
||||
/// Hierarchy tokens: `G(name)` opens, `P` is a preset, `E` closes.
|
||||
enum Tok<'a> {
|
||||
G(&'a str),
|
||||
P,
|
||||
E,
|
||||
}
|
||||
|
||||
/// A v6 file with `n` presets and a hand-made `phry` token stream.
|
||||
fn with_phry(n: usize, toks: &[Tok]) -> Vec<u8> {
|
||||
let presets: Vec<Descriptor> = (0..n).map(|i| named(&format!("B{i}"))).collect();
|
||||
let mut bytes = write_v6(2, &[], &[], &presets, false).unwrap();
|
||||
section(&mut bytes, b"phry", &phry_bytes(toks));
|
||||
bytes
|
||||
}
|
||||
|
||||
/// A `phry` section body holding `toks`.
|
||||
fn phry_bytes(toks: &[Tok]) -> Vec<u8> {
|
||||
let items = toks
|
||||
.iter()
|
||||
.map(|t| {
|
||||
Value::Descriptor(match t {
|
||||
Tok::G(name) => Descriptor::new("Grup")
|
||||
.with("Nm ", Value::Text(UnicodeString::new_nul(name)))
|
||||
.with("zuid", Value::Text(UnicodeString::new_nul("c9ee8fd5-eb7d-924c-9e95-ddc98e40bb6c"))),
|
||||
Tok::P => Descriptor::new("preset"),
|
||||
Tok::E => Descriptor::new("groupEnd"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
VersionedDescriptor::new(Descriptor::new("null").with("hierarchy", Value::List(items))).to_bytes()
|
||||
}
|
||||
|
||||
fn path(p: &[&str]) -> Vec<String> {
|
||||
p.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_flat_folders_like_photoshop_25() {
|
||||
// The layout of Photoshop 25.1's default brushes: four folders, every preset in one.
|
||||
use Tok::*;
|
||||
let f = parse(&with_phry(5, &[G("General"), P, P, E, G("Dry Media"), P, E, G("Wet Media"), P, E, G("Special Effects"), P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["General"]), path(&["General"]), path(&["Dry Media"]), path(&["Wet Media"]), path(&["Special Effects"])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_nested_folders_and_top_level_presets() {
|
||||
use Tok::*;
|
||||
let f = parse(&with_phry(5, &[P, G("Animals"), G("Birds"), P, E, P, G("Fish"), P, E, E, P])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&[]), path(&["Animals", "Birds"]), path(&["Animals"]), path(&["Animals", "Fish"]), path(&[])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
// No folder at all: no hierarchy.
|
||||
let f = parse(&with_phry(2, &[P, P])).unwrap();
|
||||
assert!(f.folders.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_mismatches_become_warnings() {
|
||||
use Tok::*;
|
||||
// A stray groupEnd is ignored; an unclosed folder still holds its presets.
|
||||
let f = parse(&with_phry(2, &[E, G("A"), P, E, E, G("B"), P])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&["B"])]);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("folder end")), "{:?}", f.warnings);
|
||||
// Fewer preset tokens than presets: the rest are at the top level.
|
||||
let f = parse(&with_phry(3, &[G("A"), P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&[]), path(&[])]);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("list 1 brushes but the file holds 3")), "{:?}", f.warnings);
|
||||
// More preset tokens than presets: extras ignored.
|
||||
let f = parse(&with_phry(1, &[G("A"), P, P, P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"])]);
|
||||
assert_eq!(f.presets.len(), 1);
|
||||
assert!(!f.warnings.is_empty());
|
||||
// A depth bomb: folders deeper than the limit merge into their ancestor at the limit.
|
||||
let mut toks: Vec<Tok> = (0..10_000).map(|_| G("deep")).collect();
|
||||
toks.push(P);
|
||||
toks.extend((0..10_000).map(|_| E));
|
||||
toks.extend([G("next"), P, E]);
|
||||
let f = parse(&with_phry(2, &toks)).unwrap();
|
||||
assert_eq!(f.folders[0].len(), MAX_FOLDER_DEPTH);
|
||||
assert_eq!(f.folders[1], path(&["next"]));
|
||||
assert!(f.warnings.iter().any(|w| w.contains("deeper")), "{:?}", f.warnings);
|
||||
// An unreadable hierarchy: imported flat, with a warning.
|
||||
let mut bytes = write_v6(2, &[], &[], &[named("x")], false).unwrap();
|
||||
section(&mut bytes, b"phry", &[0, 0, 0, 16, 0xff, 0xff]);
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert!(f.folders.is_empty() && f.presets.len() == 1);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("phry")), "{:?}", f.warnings);
|
||||
// Truncations and bit flips of a file with a hierarchy never panic.
|
||||
let good = with_phry(3, &[G("A"), P, G("B"), P, E, E, P]);
|
||||
for cut in 0..good.len() {
|
||||
let _ = parse(&good[..cut]);
|
||||
}
|
||||
for i in 0..good.len() {
|
||||
let mut b = good.clone();
|
||||
b[i] ^= 0x5a;
|
||||
let _ = parse(&b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_builds_paths_only_for_the_files_presets() {
|
||||
use Tok::*;
|
||||
// A few-KB section listing 100,000 deep presets for a file with 2: two paths are built.
|
||||
let mut toks: Vec<Tok> = (0..MAX_FOLDER_DEPTH).map(|_| G("folder")).collect();
|
||||
toks.extend((0..100_000).map(|_| P));
|
||||
let mut warnings = Vec::new();
|
||||
let folders = read_phry(&phry_bytes(&toks), 2, &mut warnings).unwrap();
|
||||
assert_eq!(folders.len(), 2);
|
||||
assert_eq!(folders.capacity(), 2);
|
||||
assert_eq!(folders[1].len(), MAX_FOLDER_DEPTH);
|
||||
assert!(warnings.iter().any(|w| w.contains("list 100000 brushes but the file holds 2")), "{warnings:?}");
|
||||
// `phry` before `desc` still pairs with the file's presets.
|
||||
let mut bytes = write_v6(2, &[], &[], &[named("a"), named("b")], false).unwrap();
|
||||
let at = bytes.windows(8).position(|w| w == b"8BIMdesc").unwrap();
|
||||
let mut phry = Vec::new();
|
||||
section(&mut phry, b"phry", &phry_bytes(&[G("A"), P, E, P]));
|
||||
bytes.splice(at..at, phry);
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&[])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_folder_paths_stay_within_their_byte_budget() {
|
||||
use Tok::*;
|
||||
// Thousands of presets under 32 folders of 255-character names would need ~27 MB of paths.
|
||||
let long = "x".repeat(MAX_FOLDER_NAME);
|
||||
let mut toks: Vec<Tok> = (0..MAX_FOLDER_DEPTH).map(|_| G(&long)).collect();
|
||||
toks.extend((0..3_000).map(|_| P));
|
||||
let mut warnings = Vec::new();
|
||||
let folders = read_phry(&phry_bytes(&toks), 3_000, &mut warnings).unwrap();
|
||||
assert_eq!(folders.len(), 3_000);
|
||||
let held: usize = folders.iter().flatten().map(|n| n.len() + size_of::<String>()).sum();
|
||||
assert!(held <= MAX_FOLDER_BYTES, "{held}");
|
||||
assert_eq!(folders[0].len(), MAX_FOLDER_DEPTH);
|
||||
assert!(folders[2_999].is_empty(), "past the budget: top level");
|
||||
assert!(warnings.iter().any(|w| w.contains("too large")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_round_trips_through_the_writer() {
|
||||
let presets: Vec<Descriptor> = (0..6).map(|i| named(&format!("B{i}"))).collect();
|
||||
let folders = vec![path(&[]), path(&["Set", "Ink"]), path(&["Set", "Ink"]), path(&["Set"]), path(&["Other"]), path(&["Set", "Ink"])];
|
||||
let bytes = write_v6_folders(2, &[], &[], &presets, &folders, true).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.presets, presets);
|
||||
assert_eq!(f.folders, folders);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
// No folders: no phry section, same bytes as write_v6.
|
||||
assert_eq!(write_v6_folders(2, &[], &[], &presets, &vec![Vec::new(); 6], true).unwrap(), write_v6(2, &[], &[], &presets, true).unwrap());
|
||||
// Mismatched lengths are an error.
|
||||
assert!(write_v6_folders(2, &[], &[], &presets, &folders[..2], true).is_err());
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
use super::*;
|
||||
use crate::descriptor::UnicodeString;
|
||||
|
||||
fn sample(id: &str, w: u32, h: u32, depth: u16) -> AbrSample {
|
||||
let bpp = usize::from(depth / 8);
|
||||
let data = (0..w as usize * h as usize * bpp).map(|i| if (i / bpp) % 3 == 0 { 255 } else { (i % 7) as u8 * 30 }).collect();
|
||||
AbrSample { id: id.into(), width: w, height: h, depth, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_patterns_spend_the_samples_budget() {
|
||||
let p = PsdPattern {
|
||||
mode: 3,
|
||||
width: 5,
|
||||
height: 3,
|
||||
name: "p".into(),
|
||||
id: "id".into(),
|
||||
palette: None,
|
||||
depth: 8,
|
||||
channels: vec![vec![1; 15], vec![2; 15], vec![3; 15]],
|
||||
alpha: None,
|
||||
};
|
||||
let abr = write_v6(2, &[sample("s", 10, 10, 8)], &[p.clone(), p], &[], true).unwrap();
|
||||
let parse_within = |left: usize| {
|
||||
let mut out = AbrFile::default();
|
||||
let mut budget = Budget(left);
|
||||
parse_v6_within(&mut Rd::new(&abr[2..]), &mut out, &mut budget).unwrap();
|
||||
(out, budget.0)
|
||||
};
|
||||
// 100 sample bytes plus two 45-byte patterns.
|
||||
let (out, left) = parse_within(190);
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 2, 0));
|
||||
// One byte short: the patterns no longer fit after the sample (they used not to count).
|
||||
let (out, left) = parse_within(189);
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 0, 14));
|
||||
assert!(out.warnings.iter().any(|w| w.contains("embedded patterns unreadable")), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_section_does_not_refund_budget_to_later_sections() {
|
||||
let p = PsdPattern {
|
||||
mode: 3,
|
||||
width: 5,
|
||||
height: 3,
|
||||
name: "p".into(),
|
||||
id: "id".into(),
|
||||
palette: None,
|
||||
depth: 8,
|
||||
channels: vec![vec![1; 15], vec![2; 15], vec![3; 15]],
|
||||
alpha: None,
|
||||
};
|
||||
let valid = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let mut malformed = valid.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
|
||||
let mut body = 2u16.to_be_bytes().to_vec();
|
||||
section(&mut body, b"patt", &malformed);
|
||||
section(&mut body, b"patt", &valid);
|
||||
let mut out = AbrFile::default();
|
||||
let mut budget = Budget(60);
|
||||
parse_v6_within(&mut Rd::new(&body), &mut out, &mut budget).unwrap();
|
||||
|
||||
assert!(out.patterns.is_empty(), "a failed block is discarded atomically");
|
||||
assert_eq!(budget.0, 0, "the failed block's first 45 bytes and later partial 15-byte charge are retained");
|
||||
assert_eq!(out.warnings.iter().filter(|w| w.contains("embedded patterns unreadable")).count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_v2_round_trip_raw_and_rle() {
|
||||
for version in [1u16, 2] {
|
||||
for rle in [false, true] {
|
||||
let brushes = vec![
|
||||
LegacyBrush {
|
||||
name: String::new(),
|
||||
spacing: 25,
|
||||
anti_alias: true,
|
||||
tip: LegacyTip::Computed { diameter: 19, hardness: 80, angle: -30, roundness: 50 },
|
||||
},
|
||||
LegacyBrush {
|
||||
name: if version == 2 { "Leaf ✓".into() } else { String::new() },
|
||||
spacing: 40,
|
||||
anti_alias: false,
|
||||
tip: LegacyTip::Sampled(sample("", 13, 9, 8)),
|
||||
},
|
||||
LegacyBrush { name: String::new(), spacing: 10, anti_alias: true, tip: LegacyTip::Sampled(sample("", 4, 6, 16)) },
|
||||
];
|
||||
let bytes = write_v12(version, &brushes, rle).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.version, version);
|
||||
assert_eq!(f.legacy, brushes, "v{version} rle={rle}");
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_round_trip_both_subversions() {
|
||||
let samples = vec![sample("$abc", 20, 11, 8), sample("$def", 5, 5, 16)];
|
||||
let preset = Descriptor::new("brushPreset")
|
||||
.with("Nm ", Value::Text(UnicodeString::new_nul("Grass")))
|
||||
.with("Brsh", Value::Descriptor(Descriptor::new("sampledBrush").with("sampledData", Value::Text(UnicodeString::new_nul("$abc")))));
|
||||
for sub in [1u16, 2] {
|
||||
for rle in [false, true] {
|
||||
let bytes = write_v6(sub, &samples, &[], std::slice::from_ref(&preset), rle).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!((f.version, f.subversion), (6, sub));
|
||||
assert_eq!(f.samples, samples);
|
||||
assert_eq!(f.presets, vec![preset.clone()]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
}
|
||||
assert_eq!(samples[1].to_unit().len(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_input_errors_without_panicking() {
|
||||
assert!(parse(&[]).is_err());
|
||||
assert!(parse(&[0, 3, 0, 0]).is_err());
|
||||
assert!(parse(&[0, 6, 0, 9]).is_err());
|
||||
// Absurd sample bounds.
|
||||
let mut s = sample("", 2, 2, 8);
|
||||
s.width = MAX_EDGE + 1;
|
||||
assert!(write_v12(1, &[LegacyBrush { name: String::new(), spacing: 1, anti_alias: true, tip: LegacyTip::Sampled(s) }], false).is_err());
|
||||
let good = write_v6(2, &[sample("$a", 30, 30, 8)], &[], &[Descriptor::new("brushPreset")], true).unwrap();
|
||||
for cut in 0..good.len() {
|
||||
let _ = parse(&good[..cut]);
|
||||
}
|
||||
let v2 = write_v12(2, &[LegacyBrush { name: "x".into(), spacing: 1, anti_alias: true, tip: LegacyTip::Sampled(sample("", 9, 9, 16)) }], true).unwrap();
|
||||
for cut in 0..v2.len() {
|
||||
let _ = parse(&v2[..cut]);
|
||||
}
|
||||
// Flip every byte once: never panics.
|
||||
for i in 0..good.len() {
|
||||
let mut b = good.clone();
|
||||
b[i] ^= 0xa5;
|
||||
let _ = parse(&b);
|
||||
}
|
||||
// A huge declared count with no data is an error, not an allocation.
|
||||
assert!(parse(&[0, 2, 0xff, 0xff]).is_err());
|
||||
}
|
||||
|
||||
fn named(n: &str) -> Descriptor {
|
||||
Descriptor::new("brushPreset").with("Nm ", Value::Text(UnicodeString::new_nul(n)))
|
||||
}
|
||||
|
||||
/// Hierarchy tokens: `G(name)` opens, `P` is a preset, `E` closes.
|
||||
enum Tok<'a> {
|
||||
G(&'a str),
|
||||
P,
|
||||
E,
|
||||
}
|
||||
|
||||
/// A v6 file with `n` presets and a hand-made `phry` token stream.
|
||||
fn with_phry(n: usize, toks: &[Tok]) -> Vec<u8> {
|
||||
let presets: Vec<Descriptor> = (0..n).map(|i| named(&format!("B{i}"))).collect();
|
||||
let mut bytes = write_v6(2, &[], &[], &presets, false).unwrap();
|
||||
section(&mut bytes, b"phry", &phry_bytes(toks));
|
||||
bytes
|
||||
}
|
||||
|
||||
/// A `phry` section body holding `toks`.
|
||||
fn phry_bytes(toks: &[Tok]) -> Vec<u8> {
|
||||
let items = toks
|
||||
.iter()
|
||||
.map(|t| {
|
||||
Value::Descriptor(match t {
|
||||
Tok::G(name) => Descriptor::new("Grup")
|
||||
.with("Nm ", Value::Text(UnicodeString::new_nul(name)))
|
||||
.with("zuid", Value::Text(UnicodeString::new_nul("c9ee8fd5-eb7d-924c-9e95-ddc98e40bb6c"))),
|
||||
Tok::P => Descriptor::new("preset"),
|
||||
Tok::E => Descriptor::new("groupEnd"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
VersionedDescriptor::new(Descriptor::new("null").with("hierarchy", Value::List(items))).to_bytes()
|
||||
}
|
||||
|
||||
fn path(p: &[&str]) -> Vec<String> {
|
||||
p.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_flat_folders_like_photoshop_25() {
|
||||
// The layout of Photoshop 25.1's default brushes: four folders, every preset in one.
|
||||
use Tok::*;
|
||||
let f = parse(&with_phry(5, &[G("General"), P, P, E, G("Dry Media"), P, E, G("Wet Media"), P, E, G("Special Effects"), P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["General"]), path(&["General"]), path(&["Dry Media"]), path(&["Wet Media"]), path(&["Special Effects"])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_nested_folders_and_top_level_presets() {
|
||||
use Tok::*;
|
||||
let f = parse(&with_phry(5, &[P, G("Animals"), G("Birds"), P, E, P, G("Fish"), P, E, E, P])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&[]), path(&["Animals", "Birds"]), path(&["Animals"]), path(&["Animals", "Fish"]), path(&[])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
// No folder at all: no hierarchy.
|
||||
let f = parse(&with_phry(2, &[P, P])).unwrap();
|
||||
assert!(f.folders.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_mismatches_become_warnings() {
|
||||
use Tok::*;
|
||||
// A stray groupEnd is ignored; an unclosed folder still holds its presets.
|
||||
let f = parse(&with_phry(2, &[E, G("A"), P, E, E, G("B"), P])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&["B"])]);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("folder end")), "{:?}", f.warnings);
|
||||
// Fewer preset tokens than presets: the rest are at the top level.
|
||||
let f = parse(&with_phry(3, &[G("A"), P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&[]), path(&[])]);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("list 1 brushes but the file holds 3")), "{:?}", f.warnings);
|
||||
// More preset tokens than presets: extras ignored.
|
||||
let f = parse(&with_phry(1, &[G("A"), P, P, P, E])).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"])]);
|
||||
assert_eq!(f.presets.len(), 1);
|
||||
assert!(!f.warnings.is_empty());
|
||||
// A depth bomb: folders deeper than the limit merge into their ancestor at the limit.
|
||||
let mut toks: Vec<Tok> = (0..10_000).map(|_| G("deep")).collect();
|
||||
toks.push(P);
|
||||
toks.extend((0..10_000).map(|_| E));
|
||||
toks.extend([G("next"), P, E]);
|
||||
let f = parse(&with_phry(2, &toks)).unwrap();
|
||||
assert_eq!(f.folders[0].len(), MAX_FOLDER_DEPTH);
|
||||
assert_eq!(f.folders[1], path(&["next"]));
|
||||
assert!(f.warnings.iter().any(|w| w.contains("deeper")), "{:?}", f.warnings);
|
||||
// An unreadable hierarchy: imported flat, with a warning.
|
||||
let mut bytes = write_v6(2, &[], &[], &[named("x")], false).unwrap();
|
||||
section(&mut bytes, b"phry", &[0, 0, 0, 16, 0xff, 0xff]);
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert!(f.folders.is_empty() && f.presets.len() == 1);
|
||||
assert!(f.warnings.iter().any(|w| w.contains("phry")), "{:?}", f.warnings);
|
||||
// Truncations and bit flips of a file with a hierarchy never panic.
|
||||
let good = with_phry(3, &[G("A"), P, G("B"), P, E, E, P]);
|
||||
for cut in 0..good.len() {
|
||||
let _ = parse(&good[..cut]);
|
||||
}
|
||||
for i in 0..good.len() {
|
||||
let mut b = good.clone();
|
||||
b[i] ^= 0x5a;
|
||||
let _ = parse(&b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_builds_paths_only_for_the_files_presets() {
|
||||
use Tok::*;
|
||||
// A few-KB section listing 100,000 deep presets for a file with 2: two paths are built.
|
||||
let mut toks: Vec<Tok> = (0..MAX_FOLDER_DEPTH).map(|_| G("folder")).collect();
|
||||
toks.extend((0..100_000).map(|_| P));
|
||||
let mut warnings = Vec::new();
|
||||
let folders = read_phry(&phry_bytes(&toks), 2, &mut warnings).unwrap();
|
||||
assert_eq!(folders.len(), 2);
|
||||
assert_eq!(folders.capacity(), 2);
|
||||
assert_eq!(folders[1].len(), MAX_FOLDER_DEPTH);
|
||||
assert!(warnings.iter().any(|w| w.contains("list 100000 brushes but the file holds 2")), "{warnings:?}");
|
||||
// `phry` before `desc` still pairs with the file's presets.
|
||||
let mut bytes = write_v6(2, &[], &[], &[named("a"), named("b")], false).unwrap();
|
||||
let at = bytes.windows(8).position(|w| w == b"8BIMdesc").unwrap();
|
||||
let mut phry = Vec::new();
|
||||
section(&mut phry, b"phry", &phry_bytes(&[G("A"), P, E, P]));
|
||||
bytes.splice(at..at, phry);
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.folders, vec![path(&["A"]), path(&[])]);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_folder_paths_stay_within_their_byte_budget() {
|
||||
use Tok::*;
|
||||
// Thousands of presets under 32 folders of 255-character names would need ~27 MB of paths.
|
||||
let long = "x".repeat(MAX_FOLDER_NAME);
|
||||
let mut toks: Vec<Tok> = (0..MAX_FOLDER_DEPTH).map(|_| G(&long)).collect();
|
||||
toks.extend((0..3_000).map(|_| P));
|
||||
let mut warnings = Vec::new();
|
||||
let folders = read_phry(&phry_bytes(&toks), 3_000, &mut warnings).unwrap();
|
||||
assert_eq!(folders.len(), 3_000);
|
||||
let held: usize = folders.iter().flatten().map(|n| n.len() + size_of::<String>()).sum();
|
||||
assert!(held <= MAX_FOLDER_BYTES, "{held}");
|
||||
assert_eq!(folders[0].len(), MAX_FOLDER_DEPTH);
|
||||
assert!(folders[2_999].is_empty(), "past the budget: top level");
|
||||
assert!(warnings.iter().any(|w| w.contains("too large")), "{warnings:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phry_round_trips_through_the_writer() {
|
||||
let presets: Vec<Descriptor> = (0..6).map(|i| named(&format!("B{i}"))).collect();
|
||||
let folders = vec![path(&[]), path(&["Set", "Ink"]), path(&["Set", "Ink"]), path(&["Set"]), path(&["Other"]), path(&["Set", "Ink"])];
|
||||
let bytes = write_v6_folders(2, &[], &[], &presets, &folders, true).unwrap();
|
||||
let f = parse(&bytes).unwrap();
|
||||
assert_eq!(f.presets, presets);
|
||||
assert_eq!(f.folders, folders);
|
||||
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
|
||||
// No folders: no phry section, same bytes as write_v6.
|
||||
assert_eq!(write_v6_folders(2, &[], &[], &presets, &vec![Vec::new(); 6], true).unwrap(), write_v6(2, &[], &[], &presets, true).unwrap());
|
||||
// Mismatched lengths are an error.
|
||||
assert!(write_v6_folders(2, &[], &[], &presets, &folders[..2], true).is_err());
|
||||
}
|
||||
@@ -539,361 +539,7 @@ pub fn unpredict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn checked_zip_validates_trailer_size_and_padding_budget() {
|
||||
let layout = PlaneLayout { planes: 1, width: 3, height: 1, depth: 8, version: Version::Psd };
|
||||
for compression in [Compression::Zip, Compression::ZipPrediction] {
|
||||
let data = encode_planes(compression, &[1, 2, 3], &layout).unwrap();
|
||||
for cut in 0..data.len() {
|
||||
assert!(decode_planes(compression, &data[..cut], &layout).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(compression, &data[..cut], &layout).is_err());
|
||||
}
|
||||
let mut corrupt = data;
|
||||
*corrupt.last_mut().unwrap() ^= 1;
|
||||
assert!(decode_planes(compression, &corrupt, &layout).is_err());
|
||||
assert!(validate_planes(compression, &corrupt, &layout).is_err());
|
||||
}
|
||||
let padded = zip_compress(&[1, 2, 3, 0]);
|
||||
assert_eq!(decode_planes(Compression::Zip, &padded, &layout).unwrap(), [1, 2, 3]);
|
||||
let bomb = zip_compress(&vec![0; (1 << 20) + 4]);
|
||||
assert!(decode_planes(Compression::Zip, &bomb, &layout).is_err());
|
||||
assert!(validate_planes(Compression::Zip, &bomb, &layout).is_err());
|
||||
}
|
||||
|
||||
fn layout(planes: usize, width: usize, height: usize, depth: u16, version: Version) -> PlaneLayout {
|
||||
PlaneLayout { planes, width, height, depth, version }
|
||||
}
|
||||
|
||||
fn pb_roundtrip(src: &[u8]) -> Vec<u8> {
|
||||
let enc = packbits::encode_vec(src);
|
||||
let dec = packbits::decode(&enc, src.len()).unwrap();
|
||||
assert_eq!(dec, src, "encoded: {enc:?}");
|
||||
enc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_absurd_declared_size_is_an_error() {
|
||||
let z = zip_compress(&[7u8; 64]);
|
||||
for expected in [1usize << 40, usize::MAX / 2, usize::MAX] {
|
||||
assert!(zip_decompress(&z, expected).is_err(), "expected {expected}");
|
||||
}
|
||||
// Through decode_planes: a tiny ZIP channel claiming a huge (but capped) plane.
|
||||
let l = layout(1, 1 << 16, 1 << 15, 8, Version::Psb);
|
||||
assert!(decode_planes(Compression::Zip, &z, &l).is_err());
|
||||
assert!(decode_planes(Compression::ZipPrediction, &z, &l).is_err());
|
||||
// Past MAX_DECODED_BYTES.
|
||||
let l = layout(4, 1 << 20, 1 << 20, 32, Version::Psb);
|
||||
assert!(decode_planes(Compression::Zip, &z, &l).is_err());
|
||||
// Garbage that is not zlib at all.
|
||||
assert!(zip_decompress(&[0xff; 16], 1 << 30).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_decompress_exact_and_truncated() {
|
||||
let src: Vec<u8> = (0..3_000_000u32).map(|i| (i % 251) as u8).collect();
|
||||
let z = zip_compress(&src);
|
||||
assert_eq!(zip_decompress(&z, src.len()).unwrap(), src);
|
||||
// Asking for fewer bytes returns exactly that many.
|
||||
assert_eq!(zip_decompress(&z, 1000).unwrap(), &src[..1000]);
|
||||
assert!(zip_decompress(&z, src.len() + 1).is_err());
|
||||
assert!(zip_decompress(&z, 0).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_empty() {
|
||||
assert!(pb_roundtrip(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_single() {
|
||||
assert_eq!(pb_roundtrip(&[7]), vec![0, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_of_two() {
|
||||
assert_eq!(pb_roundtrip(&[5, 5]), vec![0xff, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_128() {
|
||||
let src = vec![9u8; 128];
|
||||
assert_eq!(pb_roundtrip(&src), vec![0x81, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_129() {
|
||||
let src = vec![9u8; 129];
|
||||
assert_eq!(pb_roundtrip(&src), vec![0x81, 9, 0, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_130_and_256() {
|
||||
pb_roundtrip(&[1u8; 130]);
|
||||
let enc = pb_roundtrip(&[1u8; 256]);
|
||||
assert_eq!(enc, vec![0x81, 1, 0x81, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_literal_128_and_129() {
|
||||
let src: Vec<u8> = (0..128u32).map(|i| i as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
assert_eq!(enc[0], 127);
|
||||
assert_eq!(enc.len(), 129);
|
||||
let src: Vec<u8> = (0..129u32).map(|i| i as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
assert_eq!(enc[0], 127);
|
||||
assert_eq!(enc[129], 0);
|
||||
assert_eq!(enc.len(), 131);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_literal_run_boundary() {
|
||||
pb_roundtrip(&[1, 2, 3, 3, 3, 3, 4, 5]);
|
||||
pb_roundtrip(&[1, 1, 2, 2, 3, 3]);
|
||||
pb_roundtrip(&[1, 2, 2, 3]);
|
||||
pb_roundtrip(&[0, 0, 0, 1, 0, 0, 0]);
|
||||
let enc = pb_roundtrip(&[1, 2, 3, 3, 3]);
|
||||
assert_eq!(enc, vec![1, 1, 2, 0xfe, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_never_emits_noop() {
|
||||
for n in 0..300usize {
|
||||
let src: Vec<u8> = (0..n).map(|i| ((i / 3) % 5) as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
// Walk the packets to find headers.
|
||||
let mut i = 0;
|
||||
while i < enc.len() {
|
||||
let h = enc[i] as i8;
|
||||
assert_ne!(h, -128);
|
||||
i += if h >= 0 { h as usize + 2 } else { 2 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_decode_noop_skipped() {
|
||||
assert_eq!(packbits::decode(&[0x80, 0, 42], 1).unwrap(), vec![42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_decode_errors() {
|
||||
assert!(packbits::decode(&[], 1).is_err());
|
||||
assert!(packbits::decode(&[2, 1, 2], 3).is_err()); // literal truncated
|
||||
assert!(packbits::decode(&[0xfe], 3).is_err()); // run truncated
|
||||
assert!(packbits::decode(&[0xfd, 1], 3).is_err()); // run overflows (4 > 3)
|
||||
assert!(packbits::decode(&[3, 1, 2, 3, 4], 3).is_err()); // literal overflows
|
||||
}
|
||||
|
||||
/// `out.len() + expected` past `usize::MAX` used to panic in debug builds and, in release,
|
||||
/// wrap and return `Ok` having decoded nothing.
|
||||
#[test]
|
||||
fn packbits_decode_into_output_length_overflow_is_an_error() {
|
||||
let mut out = vec![7u8];
|
||||
let r = packbits::decode_into(&[0x00, 0xaa], usize::MAX, &mut out);
|
||||
assert!(matches!(r, Err(PsdError::LimitExceeded(_))), "{r:?}");
|
||||
assert_eq!(out, [7]);
|
||||
// The largest `expected` that fits still decodes up to the input, then reports it short.
|
||||
let r = packbits::decode_into(&[0x00, 0xaa], usize::MAX - 1, &mut out);
|
||||
assert!(matches!(r, Err(PsdError::Decompress(_))), "{r:?}");
|
||||
assert_eq!(out, [7, 0xaa]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_zero_expected_ignores_input() {
|
||||
assert_eq!(packbits::decode(&[0xff, 1], 0).unwrap(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compression_values() {
|
||||
for v in 0..6 {
|
||||
assert_eq!(Compression::from_u16(v).as_u16(), v);
|
||||
}
|
||||
assert_eq!(Compression::from_u16(9), Compression::Unknown(9));
|
||||
}
|
||||
|
||||
fn sample_data(l: &PlaneLayout) -> Vec<u8> {
|
||||
let n = l.decoded_len().unwrap();
|
||||
(0..n).map(|i| ((i * 7 + i / 5) % 251) as u8).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_codecs_roundtrip_all_depths() {
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
for depth in [1u16, 8, 16, 32] {
|
||||
for (w, h) in [(0, 0), (1, 1), (3, 2), (17, 5), (0, 3), (4, 0)] {
|
||||
for planes in [1, 3] {
|
||||
let l = layout(planes, w, h, depth, version);
|
||||
let data = sample_data(&l);
|
||||
for c in Compression::ALL {
|
||||
let enc = encode_planes(c, &data, &l).unwrap();
|
||||
validate_planes(c, &enc, &l).unwrap();
|
||||
let dec = decode_planes(c, &enc, &l).unwrap();
|
||||
assert_eq!(dec, data, "{c:?} {depth} {w}x{h} {version:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_count_width_psd_vs_psb() {
|
||||
let data = vec![0u8; 4];
|
||||
let psd = encode_planes(Compression::Rle, &data, &layout(1, 2, 2, 8, Version::Psd)).unwrap();
|
||||
let psb = encode_planes(Compression::Rle, &data, &layout(1, 2, 2, 8, Version::Psb)).unwrap();
|
||||
assert_eq!(psd, vec![0, 2, 0, 2, 0xff, 0, 0xff, 0]);
|
||||
assert_eq!(psb, vec![0, 0, 0, 2, 0, 0, 0, 2, 0xff, 0, 0xff, 0]);
|
||||
assert!(decode_planes(Compression::Rle, &psd, &layout(1, 2, 2, 8, Version::Psb)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_odd_width() {
|
||||
let l = layout(1, 7, 3, 16, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Rle, &data, &l).unwrap();
|
||||
assert_eq!(decode_planes(Compression::Rle, &enc, &l).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_truncated_fails() {
|
||||
let l = layout(1, 10, 10, 8, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Rle, &data, &l).unwrap();
|
||||
for cut in 0..enc.len() {
|
||||
assert!(decode_planes(Compression::Rle, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(Compression::Rle, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_truncated_fails() {
|
||||
let l = layout(1, 10, 10, 8, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Zip, &data, &l).unwrap();
|
||||
for cut in 0..enc.len() - 4 {
|
||||
assert!(decode_planes(Compression::Zip, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(Compression::Zip, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2926: a ZIP stream declaring more than `MAX_DECODED_BYTES` is not inflated at parse
|
||||
/// time. Garbage zlib data proves it: decompressing it would fail, skipping it passes, and
|
||||
/// decoding the layout still reports the size limit.
|
||||
#[test]
|
||||
fn zip_over_budget_is_not_decompressed_at_parse() {
|
||||
let l = layout(4, 300_000, 300_000, 8, Version::Psb);
|
||||
assert!(l.total_bytes().unwrap() > MAX_DECODED_BYTES);
|
||||
let garbage = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03];
|
||||
for c in [Compression::Zip, Compression::ZipPrediction] {
|
||||
validate_planes(c, &garbage, &l).unwrap();
|
||||
assert!(matches!(decode_planes(c, &garbage, &l), Err(PsdError::LimitExceeded(_))), "{c:?}");
|
||||
}
|
||||
// Within the budget the stream is still checked.
|
||||
let small = layout(1, 4, 4, 8, Version::Psd);
|
||||
assert!(validate_planes(Compression::Zip, &garbage, &small).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_short_fails() {
|
||||
let l = layout(2, 3, 3, 8, Version::Psd);
|
||||
assert!(decode_planes(Compression::Raw, &[0; 17], &l).is_err());
|
||||
assert!(decode_planes(Compression::Raw, &[0; 18], &l).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_compression_errors() {
|
||||
let l = layout(1, 1, 1, 8, Version::Psd);
|
||||
assert!(decode_planes(Compression::Unknown(7), &[0], &l).is_err());
|
||||
assert!(encode_planes(Compression::Unknown(7), &[0], &l).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rejects_wrong_size() {
|
||||
let l = layout(1, 2, 2, 8, Version::Psd);
|
||||
assert!(encode_planes(Compression::Raw, &[0; 3], &l).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_len_limits() {
|
||||
let l = layout(56, 300_000, 300_000, 32, Version::Psb);
|
||||
assert!(matches!(l.decoded_len(), Err(PsdError::LimitExceeded(_))));
|
||||
assert!(decode_planes(Compression::Rle, &[0; 16], &l).is_err());
|
||||
}
|
||||
|
||||
/// Real PSBs go past 2 GiB in one decode (#375): a 30000² RGB merged image and a 33000²
|
||||
/// 16-bit layer channel. Only checks the limit, so nothing is allocated.
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn decoded_len_allows_large_psb_planes() {
|
||||
assert_eq!(layout(3, 30_000, 30_000, 8, Version::Psb).decoded_len().unwrap(), 2_700_000_000);
|
||||
assert_eq!(layout(1, 33_000, 33_000, 16, Version::Psb).decoded_len().unwrap(), 2_178_000_000);
|
||||
assert!(layout(4, 40_000, 40_000, 16, Version::Psb).decoded_len().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_8bit_known_vector() {
|
||||
let l = layout(1, 4, 1, 8, Version::Psd);
|
||||
let mut v = vec![10, 12, 11, 255];
|
||||
predict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![10, 2, 255, 244]);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![10, 12, 11, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_16bit_known_vector() {
|
||||
let l = layout(1, 3, 1, 16, Version::Psd);
|
||||
let mut v = vec![0x01, 0x00, 0x01, 0x05, 0x00, 0x00];
|
||||
predict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![0x01, 0x00, 0x00, 0x05, 0xfe, 0xfb]);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![0x01, 0x00, 0x01, 0x05, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_32bit_plane_shuffle() {
|
||||
// Two samples: AABBCCDD and 11223344 → planes [AA,11][BB,22][CC,33][DD,44]
|
||||
let l = layout(1, 2, 1, 32, Version::Psd);
|
||||
let orig = vec![0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44];
|
||||
let mut v = orig.clone();
|
||||
predict(&mut v, &l).unwrap();
|
||||
let planes = [0xAAu8, 0x11, 0xBB, 0x22, 0xCC, 0x33, 0xDD, 0x44];
|
||||
let mut expect = planes;
|
||||
for i in (1..8).rev() {
|
||||
expect[i] = expect[i].wrapping_sub(expect[i - 1]);
|
||||
}
|
||||
assert_eq!(v, expect);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, orig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_32bit_floats_roundtrip() {
|
||||
let l = layout(1, 5, 2, 32, Version::Psd);
|
||||
let mut v = Vec::new();
|
||||
for i in 0..10 {
|
||||
v.extend_from_slice(&(i as f32 * 0.37 - 1.0).to_be_bytes());
|
||||
}
|
||||
let enc = encode_planes(Compression::ZipPrediction, &v, &l).unwrap();
|
||||
assert_eq!(decode_planes(Compression::ZipPrediction, &enc, &l).unwrap(), v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_bomb_is_bounded() {
|
||||
let big = zip_compress(&vec![0u8; 1 << 20]);
|
||||
let out = zip_decompress(&big, 10).unwrap();
|
||||
assert_eq!(out.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_garbage_errors() {
|
||||
assert!(zip_decompress(&[1, 2, 3, 4], 4).is_err());
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod parallel_rle_equivalence {
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn checked_zip_validates_trailer_size_and_padding_budget() {
|
||||
let layout = PlaneLayout { planes: 1, width: 3, height: 1, depth: 8, version: Version::Psd };
|
||||
for compression in [Compression::Zip, Compression::ZipPrediction] {
|
||||
let data = encode_planes(compression, &[1, 2, 3], &layout).unwrap();
|
||||
for cut in 0..data.len() {
|
||||
assert!(decode_planes(compression, &data[..cut], &layout).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(compression, &data[..cut], &layout).is_err());
|
||||
}
|
||||
let mut corrupt = data;
|
||||
*corrupt.last_mut().unwrap() ^= 1;
|
||||
assert!(decode_planes(compression, &corrupt, &layout).is_err());
|
||||
assert!(validate_planes(compression, &corrupt, &layout).is_err());
|
||||
}
|
||||
let padded = zip_compress(&[1, 2, 3, 0]);
|
||||
assert_eq!(decode_planes(Compression::Zip, &padded, &layout).unwrap(), [1, 2, 3]);
|
||||
let bomb = zip_compress(&vec![0; (1 << 20) + 4]);
|
||||
assert!(decode_planes(Compression::Zip, &bomb, &layout).is_err());
|
||||
assert!(validate_planes(Compression::Zip, &bomb, &layout).is_err());
|
||||
}
|
||||
|
||||
fn layout(planes: usize, width: usize, height: usize, depth: u16, version: Version) -> PlaneLayout {
|
||||
PlaneLayout { planes, width, height, depth, version }
|
||||
}
|
||||
|
||||
fn pb_roundtrip(src: &[u8]) -> Vec<u8> {
|
||||
let enc = packbits::encode_vec(src);
|
||||
let dec = packbits::decode(&enc, src.len()).unwrap();
|
||||
assert_eq!(dec, src, "encoded: {enc:?}");
|
||||
enc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_absurd_declared_size_is_an_error() {
|
||||
let z = zip_compress(&[7u8; 64]);
|
||||
for expected in [1usize << 40, usize::MAX / 2, usize::MAX] {
|
||||
assert!(zip_decompress(&z, expected).is_err(), "expected {expected}");
|
||||
}
|
||||
// Through decode_planes: a tiny ZIP channel claiming a huge (but capped) plane.
|
||||
let l = layout(1, 1 << 16, 1 << 15, 8, Version::Psb);
|
||||
assert!(decode_planes(Compression::Zip, &z, &l).is_err());
|
||||
assert!(decode_planes(Compression::ZipPrediction, &z, &l).is_err());
|
||||
// Past MAX_DECODED_BYTES.
|
||||
let l = layout(4, 1 << 20, 1 << 20, 32, Version::Psb);
|
||||
assert!(decode_planes(Compression::Zip, &z, &l).is_err());
|
||||
// Garbage that is not zlib at all.
|
||||
assert!(zip_decompress(&[0xff; 16], 1 << 30).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_decompress_exact_and_truncated() {
|
||||
let src: Vec<u8> = (0..3_000_000u32).map(|i| (i % 251) as u8).collect();
|
||||
let z = zip_compress(&src);
|
||||
assert_eq!(zip_decompress(&z, src.len()).unwrap(), src);
|
||||
// Asking for fewer bytes returns exactly that many.
|
||||
assert_eq!(zip_decompress(&z, 1000).unwrap(), &src[..1000]);
|
||||
assert!(zip_decompress(&z, src.len() + 1).is_err());
|
||||
assert!(zip_decompress(&z, 0).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_empty() {
|
||||
assert!(pb_roundtrip(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_single() {
|
||||
assert_eq!(pb_roundtrip(&[7]), vec![0, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_of_two() {
|
||||
assert_eq!(pb_roundtrip(&[5, 5]), vec![0xff, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_128() {
|
||||
let src = vec![9u8; 128];
|
||||
assert_eq!(pb_roundtrip(&src), vec![0x81, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_129() {
|
||||
let src = vec![9u8; 129];
|
||||
assert_eq!(pb_roundtrip(&src), vec![0x81, 9, 0, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_run_130_and_256() {
|
||||
pb_roundtrip(&[1u8; 130]);
|
||||
let enc = pb_roundtrip(&[1u8; 256]);
|
||||
assert_eq!(enc, vec![0x81, 1, 0x81, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_literal_128_and_129() {
|
||||
let src: Vec<u8> = (0..128u32).map(|i| i as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
assert_eq!(enc[0], 127);
|
||||
assert_eq!(enc.len(), 129);
|
||||
let src: Vec<u8> = (0..129u32).map(|i| i as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
assert_eq!(enc[0], 127);
|
||||
assert_eq!(enc[129], 0);
|
||||
assert_eq!(enc.len(), 131);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_literal_run_boundary() {
|
||||
pb_roundtrip(&[1, 2, 3, 3, 3, 3, 4, 5]);
|
||||
pb_roundtrip(&[1, 1, 2, 2, 3, 3]);
|
||||
pb_roundtrip(&[1, 2, 2, 3]);
|
||||
pb_roundtrip(&[0, 0, 0, 1, 0, 0, 0]);
|
||||
let enc = pb_roundtrip(&[1, 2, 3, 3, 3]);
|
||||
assert_eq!(enc, vec![1, 1, 2, 0xfe, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_never_emits_noop() {
|
||||
for n in 0..300usize {
|
||||
let src: Vec<u8> = (0..n).map(|i| ((i / 3) % 5) as u8).collect();
|
||||
let enc = pb_roundtrip(&src);
|
||||
// Walk the packets to find headers.
|
||||
let mut i = 0;
|
||||
while i < enc.len() {
|
||||
let h = enc[i] as i8;
|
||||
assert_ne!(h, -128);
|
||||
i += if h >= 0 { h as usize + 2 } else { 2 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_decode_noop_skipped() {
|
||||
assert_eq!(packbits::decode(&[0x80, 0, 42], 1).unwrap(), vec![42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_decode_errors() {
|
||||
assert!(packbits::decode(&[], 1).is_err());
|
||||
assert!(packbits::decode(&[2, 1, 2], 3).is_err()); // literal truncated
|
||||
assert!(packbits::decode(&[0xfe], 3).is_err()); // run truncated
|
||||
assert!(packbits::decode(&[0xfd, 1], 3).is_err()); // run overflows (4 > 3)
|
||||
assert!(packbits::decode(&[3, 1, 2, 3, 4], 3).is_err()); // literal overflows
|
||||
}
|
||||
|
||||
/// `out.len() + expected` past `usize::MAX` used to panic in debug builds and, in release,
|
||||
/// wrap and return `Ok` having decoded nothing.
|
||||
#[test]
|
||||
fn packbits_decode_into_output_length_overflow_is_an_error() {
|
||||
let mut out = vec![7u8];
|
||||
let r = packbits::decode_into(&[0x00, 0xaa], usize::MAX, &mut out);
|
||||
assert!(matches!(r, Err(PsdError::LimitExceeded(_))), "{r:?}");
|
||||
assert_eq!(out, [7]);
|
||||
// The largest `expected` that fits still decodes up to the input, then reports it short.
|
||||
let r = packbits::decode_into(&[0x00, 0xaa], usize::MAX - 1, &mut out);
|
||||
assert!(matches!(r, Err(PsdError::Decompress(_))), "{r:?}");
|
||||
assert_eq!(out, [7, 0xaa]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packbits_zero_expected_ignores_input() {
|
||||
assert_eq!(packbits::decode(&[0xff, 1], 0).unwrap(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compression_values() {
|
||||
for v in 0..6 {
|
||||
assert_eq!(Compression::from_u16(v).as_u16(), v);
|
||||
}
|
||||
assert_eq!(Compression::from_u16(9), Compression::Unknown(9));
|
||||
}
|
||||
|
||||
fn sample_data(l: &PlaneLayout) -> Vec<u8> {
|
||||
let n = l.decoded_len().unwrap();
|
||||
(0..n).map(|i| ((i * 7 + i / 5) % 251) as u8).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_codecs_roundtrip_all_depths() {
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
for depth in [1u16, 8, 16, 32] {
|
||||
for (w, h) in [(0, 0), (1, 1), (3, 2), (17, 5), (0, 3), (4, 0)] {
|
||||
for planes in [1, 3] {
|
||||
let l = layout(planes, w, h, depth, version);
|
||||
let data = sample_data(&l);
|
||||
for c in Compression::ALL {
|
||||
let enc = encode_planes(c, &data, &l).unwrap();
|
||||
validate_planes(c, &enc, &l).unwrap();
|
||||
let dec = decode_planes(c, &enc, &l).unwrap();
|
||||
assert_eq!(dec, data, "{c:?} {depth} {w}x{h} {version:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_count_width_psd_vs_psb() {
|
||||
let data = vec![0u8; 4];
|
||||
let psd = encode_planes(Compression::Rle, &data, &layout(1, 2, 2, 8, Version::Psd)).unwrap();
|
||||
let psb = encode_planes(Compression::Rle, &data, &layout(1, 2, 2, 8, Version::Psb)).unwrap();
|
||||
assert_eq!(psd, vec![0, 2, 0, 2, 0xff, 0, 0xff, 0]);
|
||||
assert_eq!(psb, vec![0, 0, 0, 2, 0, 0, 0, 2, 0xff, 0, 0xff, 0]);
|
||||
assert!(decode_planes(Compression::Rle, &psd, &layout(1, 2, 2, 8, Version::Psb)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_odd_width() {
|
||||
let l = layout(1, 7, 3, 16, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Rle, &data, &l).unwrap();
|
||||
assert_eq!(decode_planes(Compression::Rle, &enc, &l).unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_truncated_fails() {
|
||||
let l = layout(1, 10, 10, 8, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Rle, &data, &l).unwrap();
|
||||
for cut in 0..enc.len() {
|
||||
assert!(decode_planes(Compression::Rle, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(Compression::Rle, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_truncated_fails() {
|
||||
let l = layout(1, 10, 10, 8, Version::Psd);
|
||||
let data = sample_data(&l);
|
||||
let enc = encode_planes(Compression::Zip, &data, &l).unwrap();
|
||||
for cut in 0..enc.len() - 4 {
|
||||
assert!(decode_planes(Compression::Zip, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
assert!(validate_planes(Compression::Zip, &enc[..cut], &l).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2926: a ZIP stream declaring more than `MAX_DECODED_BYTES` is not inflated at parse
|
||||
/// time. Garbage zlib data proves it: decompressing it would fail, skipping it passes, and
|
||||
/// decoding the layout still reports the size limit.
|
||||
#[test]
|
||||
fn zip_over_budget_is_not_decompressed_at_parse() {
|
||||
let l = layout(4, 300_000, 300_000, 8, Version::Psb);
|
||||
assert!(l.total_bytes().unwrap() > MAX_DECODED_BYTES);
|
||||
let garbage = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03];
|
||||
for c in [Compression::Zip, Compression::ZipPrediction] {
|
||||
validate_planes(c, &garbage, &l).unwrap();
|
||||
assert!(matches!(decode_planes(c, &garbage, &l), Err(PsdError::LimitExceeded(_))), "{c:?}");
|
||||
}
|
||||
// Within the budget the stream is still checked.
|
||||
let small = layout(1, 4, 4, 8, Version::Psd);
|
||||
assert!(validate_planes(Compression::Zip, &garbage, &small).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_short_fails() {
|
||||
let l = layout(2, 3, 3, 8, Version::Psd);
|
||||
assert!(decode_planes(Compression::Raw, &[0; 17], &l).is_err());
|
||||
assert!(decode_planes(Compression::Raw, &[0; 18], &l).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_compression_errors() {
|
||||
let l = layout(1, 1, 1, 8, Version::Psd);
|
||||
assert!(decode_planes(Compression::Unknown(7), &[0], &l).is_err());
|
||||
assert!(encode_planes(Compression::Unknown(7), &[0], &l).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_rejects_wrong_size() {
|
||||
let l = layout(1, 2, 2, 8, Version::Psd);
|
||||
assert!(encode_planes(Compression::Raw, &[0; 3], &l).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_len_limits() {
|
||||
let l = layout(56, 300_000, 300_000, 32, Version::Psb);
|
||||
assert!(matches!(l.decoded_len(), Err(PsdError::LimitExceeded(_))));
|
||||
assert!(decode_planes(Compression::Rle, &[0; 16], &l).is_err());
|
||||
}
|
||||
|
||||
/// Real PSBs go past 2 GiB in one decode (#375): a 30000² RGB merged image and a 33000²
|
||||
/// 16-bit layer channel. Only checks the limit, so nothing is allocated.
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn decoded_len_allows_large_psb_planes() {
|
||||
assert_eq!(layout(3, 30_000, 30_000, 8, Version::Psb).decoded_len().unwrap(), 2_700_000_000);
|
||||
assert_eq!(layout(1, 33_000, 33_000, 16, Version::Psb).decoded_len().unwrap(), 2_178_000_000);
|
||||
assert!(layout(4, 40_000, 40_000, 16, Version::Psb).decoded_len().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_8bit_known_vector() {
|
||||
let l = layout(1, 4, 1, 8, Version::Psd);
|
||||
let mut v = vec![10, 12, 11, 255];
|
||||
predict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![10, 2, 255, 244]);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![10, 12, 11, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_16bit_known_vector() {
|
||||
let l = layout(1, 3, 1, 16, Version::Psd);
|
||||
let mut v = vec![0x01, 0x00, 0x01, 0x05, 0x00, 0x00];
|
||||
predict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![0x01, 0x00, 0x00, 0x05, 0xfe, 0xfb]);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, vec![0x01, 0x00, 0x01, 0x05, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_32bit_plane_shuffle() {
|
||||
// Two samples: AABBCCDD and 11223344 → planes [AA,11][BB,22][CC,33][DD,44]
|
||||
let l = layout(1, 2, 1, 32, Version::Psd);
|
||||
let orig = vec![0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44];
|
||||
let mut v = orig.clone();
|
||||
predict(&mut v, &l).unwrap();
|
||||
let planes = [0xAAu8, 0x11, 0xBB, 0x22, 0xCC, 0x33, 0xDD, 0x44];
|
||||
let mut expect = planes;
|
||||
for i in (1..8).rev() {
|
||||
expect[i] = expect[i].wrapping_sub(expect[i - 1]);
|
||||
}
|
||||
assert_eq!(v, expect);
|
||||
unpredict(&mut v, &l).unwrap();
|
||||
assert_eq!(v, orig);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prediction_32bit_floats_roundtrip() {
|
||||
let l = layout(1, 5, 2, 32, Version::Psd);
|
||||
let mut v = Vec::new();
|
||||
for i in 0..10 {
|
||||
v.extend_from_slice(&(i as f32 * 0.37 - 1.0).to_be_bytes());
|
||||
}
|
||||
let enc = encode_planes(Compression::ZipPrediction, &v, &l).unwrap();
|
||||
assert_eq!(decode_planes(Compression::ZipPrediction, &enc, &l).unwrap(), v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_bomb_is_bounded() {
|
||||
let big = zip_compress(&vec![0u8; 1 << 20]);
|
||||
let out = zip_decompress(&big, 10).unwrap();
|
||||
assert_eq!(out.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_garbage_errors() {
|
||||
assert!(zip_decompress(&[1, 2, 3, 4], 4).is_err());
|
||||
}
|
||||
+1
-381
@@ -833,384 +833,4 @@ impl LayerInfo {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_record(version: Version) -> LayerRecord {
|
||||
let rect = Rect::from_xywh(-3, 2, 5, 4);
|
||||
let px: Vec<u8> = (0..20).collect();
|
||||
let mut channels = Vec::new();
|
||||
for (id, comp) in [(-1, Compression::Rle), (0, Compression::Raw), (1, Compression::Zip), (2, Compression::ZipPrediction)] {
|
||||
channels.push(ChannelData::encode(id, comp, &px, 5, 4, 8, version).unwrap());
|
||||
}
|
||||
let mask = LayerMask::new(Rect::from_xywh(0, 0, 2, 2), 255, LayerMask::FLAG_RELATIVE);
|
||||
channels.push(ChannelData::encode(-2, Compression::Rle, &[1, 2, 3, 4], 2, 2, 8, version).unwrap());
|
||||
LayerRecord {
|
||||
rect,
|
||||
channels,
|
||||
blend_mode: BlendMode::Screen,
|
||||
opacity: 200,
|
||||
clipping: 1,
|
||||
flags: LayerFlags(LayerFlags::HIDDEN),
|
||||
mask: MaskData::Mask(mask),
|
||||
blending_ranges: BlendingRanges::full(3),
|
||||
name: b"abc".to_vec(),
|
||||
blocks: vec![TaggedBlock::unicode_name("\u{e4}bc"), TaggedBlock::layer_id(5)],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn rt(info: &LayerInfo, version: Version) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, version).unwrap();
|
||||
let back = LayerInfo::read_body(&out, version).unwrap();
|
||||
assert_eq!(&back, info);
|
||||
let mut again = Vec::new();
|
||||
back.write_body(&mut again, version).unwrap();
|
||||
assert_eq!(again, out);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_layer_info() {
|
||||
let out = rt(&LayerInfo::default(), Version::Psd);
|
||||
assert_eq!(out, vec![0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_roundtrip_psd_psb() {
|
||||
for v in [Version::Psd, Version::Psb] {
|
||||
let info = LayerInfo { merged_alpha: true, layers: vec![sample_record(v), LayerRecord::default()], padding: None };
|
||||
let out = rt(&info, v);
|
||||
assert_eq!(out.len() % 2, 0);
|
||||
assert_eq!(&out[..2], &(-2i16).to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psb_channel_lengths_are_8_bytes() {
|
||||
let mut rec = LayerRecord::default();
|
||||
rec.channels.push(ChannelData::new(0, Some(Compression::Raw), vec![]));
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
let psd = rt(&info, Version::Psd);
|
||||
let psb = rt(&info, Version::Psb);
|
||||
assert_eq!(psb.len(), psd.len() + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_decode_via_record() {
|
||||
let rec = sample_record(Version::Psd);
|
||||
let px: Vec<u8> = (0..20).collect();
|
||||
for id in [-1, 0, 1, 2] {
|
||||
assert_eq!(rec.decode_channel(id, 8, Version::Psd).unwrap(), px);
|
||||
}
|
||||
assert_eq!(rec.decode_channel(-2, 8, Version::Psd).unwrap(), vec![1, 2, 3, 4]);
|
||||
assert!(rec.decode_channel(7, 8, Version::Psd).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_accessors() {
|
||||
let rec = sample_record(Version::Psd);
|
||||
assert_eq!(rec.name(), "\u{e4}bc");
|
||||
assert!(!rec.is_visible());
|
||||
assert_eq!(rec.layer_id(), Some(5));
|
||||
assert_eq!(rec.fill_opacity(), 255);
|
||||
assert_eq!(rec.section_type(), SectionType::Other);
|
||||
assert!(rec.layer_mask().is_some());
|
||||
assert_eq!(rec.channel_rect(-2), Rect::from_xywh(0, 0, 2, 2));
|
||||
assert_eq!(rec.channel_rect(0), rec.rect);
|
||||
let mut plain = LayerRecord { name: b"legacy".to_vec(), ..Default::default() };
|
||||
assert_eq!(plain.name(), "legacy");
|
||||
plain.blocks.push(TaggedBlock::fill_opacity(10));
|
||||
assert_eq!(plain.fill_opacity(), 10);
|
||||
plain.block_mut(b"iOpa").unwrap().data[0] = 11;
|
||||
assert_eq!(plain.fill_opacity(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_variants_roundtrip() {
|
||||
let base = LayerMask::new(Rect::from_xywh(1, 2, 3, 4), 0, 0);
|
||||
let with_real = LayerMask { real: Some(RealMask { flags: 1, background: 255, rect: Rect::from_xywh(0, 0, 9, 9) }), trailing: vec![], ..base.clone() };
|
||||
let with_params = LayerMask {
|
||||
flags: LayerMask::FLAG_PARAMETERS,
|
||||
parameters: Some(MaskParameters {
|
||||
flags: 0b1111,
|
||||
user_density: Some(128),
|
||||
user_feather: Some(2.5),
|
||||
vector_density: Some(64),
|
||||
vector_feather: Some(0.5),
|
||||
}),
|
||||
real: Some(RealMask::default()),
|
||||
trailing: vec![0, 0],
|
||||
..base.clone()
|
||||
};
|
||||
let partial_params = LayerMask {
|
||||
flags: LayerMask::FLAG_PARAMETERS,
|
||||
parameters: Some(MaskParameters { flags: 0b0010, user_feather: Some(1.0), ..Default::default() }),
|
||||
trailing: vec![],
|
||||
..base.clone()
|
||||
};
|
||||
for m in [base, with_real, with_params, partial_params] {
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(LayerMask::parse(&out).unwrap(), m);
|
||||
}
|
||||
}
|
||||
|
||||
/// Photoshop stores the real-mask fields before the mask parameters (psd-tools
|
||||
/// mask-density-layervectormask.psd): user + vector mask with both densities set.
|
||||
#[test]
|
||||
fn real_mask_before_parameters_photoshop_layout() {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(15, -1, 18, 10).write(&mut d);
|
||||
d.extend([0, LayerMask::FLAG_PARAMETERS | 8]);
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 32, 8).write(&mut d);
|
||||
d.extend([0b0101, 64, 64, 0]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert_eq!(m.real.map(|r| r.rect), Some(Rect::from_xywh(0, 0, 32, 8)));
|
||||
assert_eq!(m.real.map(|r| r.background), Some(255));
|
||||
let p = m.parameters.unwrap();
|
||||
assert_eq!((p.user_density, p.vector_density), (Some(64), Some(64)));
|
||||
assert_eq!(m.trailing, vec![0]);
|
||||
assert!(m.real_first);
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
|
||||
/// A density-only record in the spec's order (#949): the parameter flags byte at 18 says one
|
||||
/// value byte, so the spec's reading excludes the 38-byte length only if the real mask is
|
||||
/// ignored, while byte 36 (inside the real rectangle's `right`) is 0 and makes Photoshop's
|
||||
/// reading fit too. Photoshop's reading puts the density (64) where a 0-or-255 background
|
||||
/// belongs, so the spec's order wins and the density survives.
|
||||
#[test]
|
||||
fn density_only_spec_layout_is_not_read_as_photoshop_order() {
|
||||
for (density, right) in [(64u8, 1u32), (200, 1), (64, 300), (1, 7)] {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0b0001, density]);
|
||||
d.extend([0, 0]);
|
||||
Rect::from_xywh(0, 0, right, 1).write(&mut d);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(!m.real_first, "density {density}, right {right}: {m:?}");
|
||||
assert_eq!(m.parameters.unwrap().user_density, Some(density));
|
||||
assert_eq!(m.real, Some(RealMask { flags: 0, background: 0, rect: Rect::from_xywh(0, 0, right, 1) }));
|
||||
assert!(m.trailing.is_empty());
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
// Photoshop's order with the same lengths still reads as Photoshop's order.
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([0b0001, 64]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(m.real_first, "{m:?}");
|
||||
assert_eq!(m.parameters.unwrap().user_density, Some(64));
|
||||
assert_eq!(m.real.map(|r| r.background), Some(255));
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
|
||||
/// The spec's order (parameters, then real fields) still parses and stays byte-stable.
|
||||
#[test]
|
||||
fn real_mask_after_parameters_spec_layout() {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(1, 2, 3, 4).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0b0011, 200]);
|
||||
d.extend(1.5f64.to_be_bytes());
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 3, 3).write(&mut d);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(!m.real_first);
|
||||
assert_eq!(m.real.map(|r| r.rect), Some(Rect::from_xywh(0, 0, 3, 3)));
|
||||
assert_eq!(m.parameters.unwrap().user_feather, Some(1.5));
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
// Parameters alone (no real mask) keep reading in the spec's order.
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(1, 2, 3, 4).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS, 0b1010]);
|
||||
d.extend(1.0f64.to_be_bytes());
|
||||
d.extend(2.0f64.to_be_bytes());
|
||||
d.extend([0, 0]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert_eq!(m.real, None);
|
||||
assert_eq!(m.parameters.map(|p| (p.user_feather, p.vector_feather)), Some((Some(1.0), Some(2.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_sizes() {
|
||||
let mut out = Vec::new();
|
||||
LayerMask::new(Rect::default(), 0, 0).write(&mut out);
|
||||
assert_eq!(out.len(), 20);
|
||||
let m = LayerMask { real: Some(RealMask::default()), trailing: vec![], ..LayerMask::new(Rect::default(), 0, 0) };
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out.len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_mask_kept_raw() {
|
||||
let mut rec = LayerRecord { mask: MaskData::Raw(vec![1, 2, 3]), ..Default::default() };
|
||||
rec.channels.clear();
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
rt(&info, Version::Psd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_helpers() {
|
||||
let mut f = LayerFlags::default();
|
||||
f.set_hidden(true);
|
||||
f.set_transparency_protected(true);
|
||||
assert!(f.hidden() && f.transparency_protected());
|
||||
f.set_pixel_data_irrelevant(true);
|
||||
assert!(f.bit4_useful() && f.pixel_data_irrelevant());
|
||||
f.set_hidden(false);
|
||||
assert!(!f.hidden());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blending_ranges_parse() {
|
||||
let br = BlendingRanges::full(3);
|
||||
assert_eq!(br.data.len(), 32);
|
||||
let r = br.ranges();
|
||||
assert_eq!(r.len(), 4);
|
||||
assert_eq!(r[0].source, [0, 0, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_helpers() {
|
||||
let r = Rect::from_xywh(-5, -6, 10, 3);
|
||||
assert_eq!((r.width(), r.height()), (10, 3));
|
||||
assert_eq!(r.size().unwrap(), (10, 3));
|
||||
assert!(!r.is_empty());
|
||||
let bad = Rect { top: 5, left: 0, bottom: 0, right: 1 };
|
||||
assert!(bad.size().is_err());
|
||||
assert!(bad.is_empty());
|
||||
let huge = Rect { top: 0, left: i32::MIN, bottom: 1, right: i32::MAX };
|
||||
assert!(huge.size().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_errors() {
|
||||
let info = LayerInfo { layers: vec![sample_record(Version::Psd)], ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, Version::Psd).unwrap();
|
||||
// The final byte may be the (optional) even padding; stop before it.
|
||||
for cut in 0..out.len() - 1 {
|
||||
assert!(LayerInfo::read_body(&out[..cut], Version::Psd).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_blend_signature() {
|
||||
let info = LayerInfo { layers: vec![LayerRecord::default()], ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, Version::Psd).unwrap();
|
||||
// count(2) + rect(16) + nch(2) = 20 → signature
|
||||
out[20] = b'X';
|
||||
assert!(matches!(LayerInfo::read_body(&out, Version::Psd), Err(PsdError::InvalidSignature { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_layers_rejected() {
|
||||
let out = 1000i16.to_be_bytes();
|
||||
assert!(LayerInfo::read_body(&out, Version::Psd).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_channel() {
|
||||
let mut rec = LayerRecord::default();
|
||||
rec.channels.push(ChannelData::new(0, None, vec![]));
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
rt(&info, Version::Psd);
|
||||
assert_eq!(info.layers[0].decode_channel(0, 8, Version::Psd).unwrap(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
/// The computed length matches the bytes written for every mask form, long names, custom block
|
||||
/// padding, long-length blocks and trailing bytes.
|
||||
#[test]
|
||||
fn unpadded_len_matches_the_written_body() {
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
let mut layers = vec![sample_record(version), LayerRecord::default()];
|
||||
for (params, real, real_first) in [(true, true, true), (true, true, false), (true, false, true), (false, true, true)] {
|
||||
let mut mask = LayerMask::new(Rect::from_xywh(1, 2, 3, 4), 0, LayerMask::FLAG_PARAMETERS);
|
||||
mask.parameters = params.then_some(MaskParameters {
|
||||
flags: 0x0f,
|
||||
user_density: Some(1),
|
||||
user_feather: Some(2.5),
|
||||
vector_density: Some(3),
|
||||
vector_feather: None,
|
||||
});
|
||||
mask.real = real.then_some(RealMask { flags: 0, background: 255, rect: Rect::from_xywh(0, 0, 1, 1) });
|
||||
mask.real_first = real_first;
|
||||
mask.trailing = vec![0; 3];
|
||||
let mut rec = sample_record(version);
|
||||
rec.mask = MaskData::Mask(mask);
|
||||
rec.name = vec![b'n'; 300];
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"lsct", data: vec![1; 5], padding: Some(vec![0; 3]) });
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"Lr16", data: vec![2; 7], padding: None });
|
||||
rec.extra_trailing = vec![9; 2];
|
||||
layers.push(rec);
|
||||
}
|
||||
layers.push(LayerRecord { mask: MaskData::Raw(vec![5; 11]), name: b"ab".to_vec(), ..Default::default() });
|
||||
let info = LayerInfo { layers, ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_unpadded(&mut out, version).unwrap();
|
||||
assert_eq!(info.unpadded_len(version).unwrap(), out.len() as u64, "{version:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Padding without cloning the layers writes the same bytes as `pad_to` on a clone, and the
|
||||
/// channel data length hint is exact.
|
||||
#[test]
|
||||
fn unpadded_write_matches_pad_to_on_a_clone() {
|
||||
use crate::{ColorMode, Compression, testgen};
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
for depth in [8u16, 16, 32] {
|
||||
for compression in [Compression::Raw, Compression::Rle] {
|
||||
let file = testgen::layered(version, ColorMode::Rgb, depth, compression);
|
||||
let mut info = file.layer_info.clone().unwrap();
|
||||
for align in [1u64, 2, 4, 8] {
|
||||
let mut cloned = info.clone();
|
||||
cloned.pad_to(version, align).unwrap();
|
||||
let mut want = Vec::new();
|
||||
cloned.write_body(&mut want, version).unwrap();
|
||||
let pad = info.pad_len(version, align).unwrap();
|
||||
let mut got = Vec::new();
|
||||
info.write_unpadded(&mut got, version).unwrap();
|
||||
assert!(got.len() >= info.channel_data_len());
|
||||
got.resize(got.len() + pad, 0);
|
||||
assert_eq!(got, want, "{version:?} {depth} {compression:?} align {align}");
|
||||
}
|
||||
info.padding = Some(vec![7, 7]);
|
||||
let mut with_padding = Vec::new();
|
||||
info.write_body(&mut with_padding, version).unwrap();
|
||||
assert!(with_padding.ends_with(&[7, 7]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nondefault_padding_kept() {
|
||||
let info = LayerInfo { padding: Some(vec![0, 0]), ..Default::default() };
|
||||
let out = rt(&info, Version::Psd);
|
||||
assert_eq!(out.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_blend_and_blocks_passthrough() {
|
||||
let rec = LayerRecord { blend_mode: BlendMode::Unknown(*b"wxyz"), blocks: vec![TaggedBlock::new(*b"Zzzz", vec![1, 2, 3, 4, 5])], ..Default::default() };
|
||||
rt(&LayerInfo { layers: vec![rec], ..Default::default() }, Version::Psd);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
use super::*;
|
||||
|
||||
fn sample_record(version: Version) -> LayerRecord {
|
||||
let rect = Rect::from_xywh(-3, 2, 5, 4);
|
||||
let px: Vec<u8> = (0..20).collect();
|
||||
let mut channels = Vec::new();
|
||||
for (id, comp) in [(-1, Compression::Rle), (0, Compression::Raw), (1, Compression::Zip), (2, Compression::ZipPrediction)] {
|
||||
channels.push(ChannelData::encode(id, comp, &px, 5, 4, 8, version).unwrap());
|
||||
}
|
||||
let mask = LayerMask::new(Rect::from_xywh(0, 0, 2, 2), 255, LayerMask::FLAG_RELATIVE);
|
||||
channels.push(ChannelData::encode(-2, Compression::Rle, &[1, 2, 3, 4], 2, 2, 8, version).unwrap());
|
||||
LayerRecord {
|
||||
rect,
|
||||
channels,
|
||||
blend_mode: BlendMode::Screen,
|
||||
opacity: 200,
|
||||
clipping: 1,
|
||||
flags: LayerFlags(LayerFlags::HIDDEN),
|
||||
mask: MaskData::Mask(mask),
|
||||
blending_ranges: BlendingRanges::full(3),
|
||||
name: b"abc".to_vec(),
|
||||
blocks: vec![TaggedBlock::unicode_name("\u{e4}bc"), TaggedBlock::layer_id(5)],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn rt(info: &LayerInfo, version: Version) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, version).unwrap();
|
||||
let back = LayerInfo::read_body(&out, version).unwrap();
|
||||
assert_eq!(&back, info);
|
||||
let mut again = Vec::new();
|
||||
back.write_body(&mut again, version).unwrap();
|
||||
assert_eq!(again, out);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_layer_info() {
|
||||
let out = rt(&LayerInfo::default(), Version::Psd);
|
||||
assert_eq!(out, vec![0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_roundtrip_psd_psb() {
|
||||
for v in [Version::Psd, Version::Psb] {
|
||||
let info = LayerInfo { merged_alpha: true, layers: vec![sample_record(v), LayerRecord::default()], padding: None };
|
||||
let out = rt(&info, v);
|
||||
assert_eq!(out.len() % 2, 0);
|
||||
assert_eq!(&out[..2], &(-2i16).to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psb_channel_lengths_are_8_bytes() {
|
||||
let mut rec = LayerRecord::default();
|
||||
rec.channels.push(ChannelData::new(0, Some(Compression::Raw), vec![]));
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
let psd = rt(&info, Version::Psd);
|
||||
let psb = rt(&info, Version::Psb);
|
||||
assert_eq!(psb.len(), psd.len() + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_decode_via_record() {
|
||||
let rec = sample_record(Version::Psd);
|
||||
let px: Vec<u8> = (0..20).collect();
|
||||
for id in [-1, 0, 1, 2] {
|
||||
assert_eq!(rec.decode_channel(id, 8, Version::Psd).unwrap(), px);
|
||||
}
|
||||
assert_eq!(rec.decode_channel(-2, 8, Version::Psd).unwrap(), vec![1, 2, 3, 4]);
|
||||
assert!(rec.decode_channel(7, 8, Version::Psd).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_accessors() {
|
||||
let rec = sample_record(Version::Psd);
|
||||
assert_eq!(rec.name(), "\u{e4}bc");
|
||||
assert!(!rec.is_visible());
|
||||
assert_eq!(rec.layer_id(), Some(5));
|
||||
assert_eq!(rec.fill_opacity(), 255);
|
||||
assert_eq!(rec.section_type(), SectionType::Other);
|
||||
assert!(rec.layer_mask().is_some());
|
||||
assert_eq!(rec.channel_rect(-2), Rect::from_xywh(0, 0, 2, 2));
|
||||
assert_eq!(rec.channel_rect(0), rec.rect);
|
||||
let mut plain = LayerRecord { name: b"legacy".to_vec(), ..Default::default() };
|
||||
assert_eq!(plain.name(), "legacy");
|
||||
plain.blocks.push(TaggedBlock::fill_opacity(10));
|
||||
assert_eq!(plain.fill_opacity(), 10);
|
||||
plain.block_mut(b"iOpa").unwrap().data[0] = 11;
|
||||
assert_eq!(plain.fill_opacity(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_variants_roundtrip() {
|
||||
let base = LayerMask::new(Rect::from_xywh(1, 2, 3, 4), 0, 0);
|
||||
let with_real = LayerMask { real: Some(RealMask { flags: 1, background: 255, rect: Rect::from_xywh(0, 0, 9, 9) }), trailing: vec![], ..base.clone() };
|
||||
let with_params = LayerMask {
|
||||
flags: LayerMask::FLAG_PARAMETERS,
|
||||
parameters: Some(MaskParameters {
|
||||
flags: 0b1111,
|
||||
user_density: Some(128),
|
||||
user_feather: Some(2.5),
|
||||
vector_density: Some(64),
|
||||
vector_feather: Some(0.5),
|
||||
}),
|
||||
real: Some(RealMask::default()),
|
||||
trailing: vec![0, 0],
|
||||
..base.clone()
|
||||
};
|
||||
let partial_params = LayerMask {
|
||||
flags: LayerMask::FLAG_PARAMETERS,
|
||||
parameters: Some(MaskParameters { flags: 0b0010, user_feather: Some(1.0), ..Default::default() }),
|
||||
trailing: vec![],
|
||||
..base.clone()
|
||||
};
|
||||
for m in [base, with_real, with_params, partial_params] {
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(LayerMask::parse(&out).unwrap(), m);
|
||||
}
|
||||
}
|
||||
|
||||
/// Photoshop stores the real-mask fields before the mask parameters (psd-tools
|
||||
/// mask-density-layervectormask.psd): user + vector mask with both densities set.
|
||||
#[test]
|
||||
fn real_mask_before_parameters_photoshop_layout() {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(15, -1, 18, 10).write(&mut d);
|
||||
d.extend([0, LayerMask::FLAG_PARAMETERS | 8]);
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 32, 8).write(&mut d);
|
||||
d.extend([0b0101, 64, 64, 0]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert_eq!(m.real.map(|r| r.rect), Some(Rect::from_xywh(0, 0, 32, 8)));
|
||||
assert_eq!(m.real.map(|r| r.background), Some(255));
|
||||
let p = m.parameters.unwrap();
|
||||
assert_eq!((p.user_density, p.vector_density), (Some(64), Some(64)));
|
||||
assert_eq!(m.trailing, vec![0]);
|
||||
assert!(m.real_first);
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
|
||||
/// A density-only record in the spec's order (#949): the parameter flags byte at 18 says one
|
||||
/// value byte, so the spec's reading excludes the 38-byte length only if the real mask is
|
||||
/// ignored, while byte 36 (inside the real rectangle's `right`) is 0 and makes Photoshop's
|
||||
/// reading fit too. Photoshop's reading puts the density (64) where a 0-or-255 background
|
||||
/// belongs, so the spec's order wins and the density survives.
|
||||
#[test]
|
||||
fn density_only_spec_layout_is_not_read_as_photoshop_order() {
|
||||
for (density, right) in [(64u8, 1u32), (200, 1), (64, 300), (1, 7)] {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0b0001, density]);
|
||||
d.extend([0, 0]);
|
||||
Rect::from_xywh(0, 0, right, 1).write(&mut d);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(!m.real_first, "density {density}, right {right}: {m:?}");
|
||||
assert_eq!(m.parameters.unwrap().user_density, Some(density));
|
||||
assert_eq!(m.real, Some(RealMask { flags: 0, background: 0, rect: Rect::from_xywh(0, 0, right, 1) }));
|
||||
assert!(m.trailing.is_empty());
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
// Photoshop's order with the same lengths still reads as Photoshop's order.
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 1, 1).write(&mut d);
|
||||
d.extend([0b0001, 64]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(m.real_first, "{m:?}");
|
||||
assert_eq!(m.parameters.unwrap().user_density, Some(64));
|
||||
assert_eq!(m.real.map(|r| r.background), Some(255));
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
}
|
||||
|
||||
/// The spec's order (parameters, then real fields) still parses and stays byte-stable.
|
||||
#[test]
|
||||
fn real_mask_after_parameters_spec_layout() {
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(1, 2, 3, 4).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS]);
|
||||
d.extend([0b0011, 200]);
|
||||
d.extend(1.5f64.to_be_bytes());
|
||||
d.extend([0, 255]);
|
||||
Rect::from_xywh(0, 0, 3, 3).write(&mut d);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert!(!m.real_first);
|
||||
assert_eq!(m.real.map(|r| r.rect), Some(Rect::from_xywh(0, 0, 3, 3)));
|
||||
assert_eq!(m.parameters.unwrap().user_feather, Some(1.5));
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out, d);
|
||||
// Parameters alone (no real mask) keep reading in the spec's order.
|
||||
let mut d = Vec::new();
|
||||
Rect::from_xywh(1, 2, 3, 4).write(&mut d);
|
||||
d.extend([255, LayerMask::FLAG_PARAMETERS, 0b1010]);
|
||||
d.extend(1.0f64.to_be_bytes());
|
||||
d.extend(2.0f64.to_be_bytes());
|
||||
d.extend([0, 0]);
|
||||
let m = LayerMask::parse(&d).unwrap();
|
||||
assert_eq!(m.real, None);
|
||||
assert_eq!(m.parameters.map(|p| (p.user_feather, p.vector_feather)), Some((Some(1.0), Some(2.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_sizes() {
|
||||
let mut out = Vec::new();
|
||||
LayerMask::new(Rect::default(), 0, 0).write(&mut out);
|
||||
assert_eq!(out.len(), 20);
|
||||
let m = LayerMask { real: Some(RealMask::default()), trailing: vec![], ..LayerMask::new(Rect::default(), 0, 0) };
|
||||
let mut out = Vec::new();
|
||||
m.write(&mut out);
|
||||
assert_eq!(out.len(), 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_mask_kept_raw() {
|
||||
let mut rec = LayerRecord { mask: MaskData::Raw(vec![1, 2, 3]), ..Default::default() };
|
||||
rec.channels.clear();
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
rt(&info, Version::Psd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_helpers() {
|
||||
let mut f = LayerFlags::default();
|
||||
f.set_hidden(true);
|
||||
f.set_transparency_protected(true);
|
||||
assert!(f.hidden() && f.transparency_protected());
|
||||
f.set_pixel_data_irrelevant(true);
|
||||
assert!(f.bit4_useful() && f.pixel_data_irrelevant());
|
||||
f.set_hidden(false);
|
||||
assert!(!f.hidden());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blending_ranges_parse() {
|
||||
let br = BlendingRanges::full(3);
|
||||
assert_eq!(br.data.len(), 32);
|
||||
let r = br.ranges();
|
||||
assert_eq!(r.len(), 4);
|
||||
assert_eq!(r[0].source, [0, 0, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_helpers() {
|
||||
let r = Rect::from_xywh(-5, -6, 10, 3);
|
||||
assert_eq!((r.width(), r.height()), (10, 3));
|
||||
assert_eq!(r.size().unwrap(), (10, 3));
|
||||
assert!(!r.is_empty());
|
||||
let bad = Rect { top: 5, left: 0, bottom: 0, right: 1 };
|
||||
assert!(bad.size().is_err());
|
||||
assert!(bad.is_empty());
|
||||
let huge = Rect { top: 0, left: i32::MIN, bottom: 1, right: i32::MAX };
|
||||
assert!(huge.size().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_errors() {
|
||||
let info = LayerInfo { layers: vec![sample_record(Version::Psd)], ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, Version::Psd).unwrap();
|
||||
// The final byte may be the (optional) even padding; stop before it.
|
||||
for cut in 0..out.len() - 1 {
|
||||
assert!(LayerInfo::read_body(&out[..cut], Version::Psd).is_err(), "cut {cut}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_blend_signature() {
|
||||
let info = LayerInfo { layers: vec![LayerRecord::default()], ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_body(&mut out, Version::Psd).unwrap();
|
||||
// count(2) + rect(16) + nch(2) = 20 → signature
|
||||
out[20] = b'X';
|
||||
assert!(matches!(LayerInfo::read_body(&out, Version::Psd), Err(PsdError::InvalidSignature { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_layers_rejected() {
|
||||
let out = 1000i16.to_be_bytes();
|
||||
assert!(LayerInfo::read_body(&out, Version::Psd).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_channel() {
|
||||
let mut rec = LayerRecord::default();
|
||||
rec.channels.push(ChannelData::new(0, None, vec![]));
|
||||
let info = LayerInfo { layers: vec![rec], ..Default::default() };
|
||||
rt(&info, Version::Psd);
|
||||
assert_eq!(info.layers[0].decode_channel(0, 8, Version::Psd).unwrap(), Vec::<u8>::new());
|
||||
}
|
||||
|
||||
/// The computed length matches the bytes written for every mask form, long names, custom block
|
||||
/// padding, long-length blocks and trailing bytes.
|
||||
#[test]
|
||||
fn unpadded_len_matches_the_written_body() {
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
let mut layers = vec![sample_record(version), LayerRecord::default()];
|
||||
for (params, real, real_first) in [(true, true, true), (true, true, false), (true, false, true), (false, true, true)] {
|
||||
let mut mask = LayerMask::new(Rect::from_xywh(1, 2, 3, 4), 0, LayerMask::FLAG_PARAMETERS);
|
||||
mask.parameters =
|
||||
params.then_some(MaskParameters { flags: 0x0f, user_density: Some(1), user_feather: Some(2.5), vector_density: Some(3), vector_feather: None });
|
||||
mask.real = real.then_some(RealMask { flags: 0, background: 255, rect: Rect::from_xywh(0, 0, 1, 1) });
|
||||
mask.real_first = real_first;
|
||||
mask.trailing = vec![0; 3];
|
||||
let mut rec = sample_record(version);
|
||||
rec.mask = MaskData::Mask(mask);
|
||||
rec.name = vec![b'n'; 300];
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"lsct", data: vec![1; 5], padding: Some(vec![0; 3]) });
|
||||
rec.blocks.push(TaggedBlock { signature: *b"8BIM", key: *b"Lr16", data: vec![2; 7], padding: None });
|
||||
rec.extra_trailing = vec![9; 2];
|
||||
layers.push(rec);
|
||||
}
|
||||
layers.push(LayerRecord { mask: MaskData::Raw(vec![5; 11]), name: b"ab".to_vec(), ..Default::default() });
|
||||
let info = LayerInfo { layers, ..Default::default() };
|
||||
let mut out = Vec::new();
|
||||
info.write_unpadded(&mut out, version).unwrap();
|
||||
assert_eq!(info.unpadded_len(version).unwrap(), out.len() as u64, "{version:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Padding without cloning the layers writes the same bytes as `pad_to` on a clone, and the
|
||||
/// channel data length hint is exact.
|
||||
#[test]
|
||||
fn unpadded_write_matches_pad_to_on_a_clone() {
|
||||
use crate::{ColorMode, Compression, testgen};
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
for depth in [8u16, 16, 32] {
|
||||
for compression in [Compression::Raw, Compression::Rle] {
|
||||
let file = testgen::layered(version, ColorMode::Rgb, depth, compression);
|
||||
let mut info = file.layer_info.clone().unwrap();
|
||||
for align in [1u64, 2, 4, 8] {
|
||||
let mut cloned = info.clone();
|
||||
cloned.pad_to(version, align).unwrap();
|
||||
let mut want = Vec::new();
|
||||
cloned.write_body(&mut want, version).unwrap();
|
||||
let pad = info.pad_len(version, align).unwrap();
|
||||
let mut got = Vec::new();
|
||||
info.write_unpadded(&mut got, version).unwrap();
|
||||
assert!(got.len() >= info.channel_data_len());
|
||||
got.resize(got.len() + pad, 0);
|
||||
assert_eq!(got, want, "{version:?} {depth} {compression:?} align {align}");
|
||||
}
|
||||
info.padding = Some(vec![7, 7]);
|
||||
let mut with_padding = Vec::new();
|
||||
info.write_body(&mut with_padding, version).unwrap();
|
||||
assert!(with_padding.ends_with(&[7, 7]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nondefault_padding_kept() {
|
||||
let info = LayerInfo { padding: Some(vec![0, 0]), ..Default::default() };
|
||||
let out = rt(&info, Version::Psd);
|
||||
assert_eq!(out.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_blend_and_blocks_passthrough() {
|
||||
let rec = LayerRecord { blend_mode: BlendMode::Unknown(*b"wxyz"), blocks: vec![TaggedBlock::new(*b"Zzzz", vec![1, 2, 3, 4, 5])], ..Default::default() };
|
||||
rt(&LayerInfo { layers: vec![rec], ..Default::default() }, Version::Psd);
|
||||
}
|
||||
+1
-354
@@ -575,357 +575,4 @@ pub fn write_pat_file(patterns: &[PsdPattern]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pat(depth: u16, alpha: bool) -> PsdPattern {
|
||||
let (w, h) = (5u32, 3u32);
|
||||
let bpp = usize::from(depth / 8);
|
||||
let plane = |s: u8| (0..w as usize * h as usize * bpp).map(|i| (i as u8).wrapping_mul(7).wrapping_add(s)).collect::<Vec<u8>>();
|
||||
PsdPattern {
|
||||
mode: 3,
|
||||
width: w,
|
||||
height: h,
|
||||
name: "Test ✓".into(),
|
||||
id: "0bd2d3ba-1234-11d4-8f8f-aabbccddeeff".into(),
|
||||
palette: None,
|
||||
depth,
|
||||
channels: vec![plane(1), plane(2), plane(3)],
|
||||
alpha: alpha.then(|| plane(9)),
|
||||
}
|
||||
}
|
||||
|
||||
/// #9318 review: a name the reader refuses (more than 65,536 UTF-16 units with its NUL) is
|
||||
/// refused on write, so a saved library can't become a file that loads nothing.
|
||||
#[test]
|
||||
fn a_name_the_reader_refuses_is_refused_on_write() {
|
||||
let long = PsdPattern { name: "a".repeat(65_536), ..pat(8, false) };
|
||||
assert!(matches!(write_pat_file(&[long]), Err(PsdError::LimitExceeded(_))));
|
||||
let longest = PsdPattern { name: "a".repeat(65_535), ..pat(8, false) };
|
||||
assert_eq!(parse_pat_file(&write_pat_file(std::slice::from_ref(&longest)).unwrap()).unwrap(), [longest]);
|
||||
}
|
||||
|
||||
/// #9318 review: more patterns than a `.pat` holds are refused on write, and a file claiming
|
||||
/// more is refused on read rather than silently cut to the first 100,000.
|
||||
#[test]
|
||||
fn more_patterns_than_a_pat_file_holds_are_refused() {
|
||||
let many = vec![pat(8, false); MAX_PAT_PATTERNS + 1];
|
||||
assert!(matches!(write_pat_file(&many), Err(PsdError::LimitExceeded(_))));
|
||||
let mut header = b"8BPT".to_vec();
|
||||
header.extend_from_slice(&1u16.to_be_bytes());
|
||||
header.extend_from_slice(&(MAX_PAT_PATTERNS as u32 + 1).to_be_bytes());
|
||||
assert!(matches!(parse_pat_file(&header), Err(PsdError::LimitExceeded(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_and_pat_file_round_trip_at_every_depth() {
|
||||
for depth in [8, 16, 32] {
|
||||
for alpha in [false, true] {
|
||||
let ps = vec![pat(depth, alpha), PsdPattern { name: "Second".into(), ..pat(depth, false) }];
|
||||
let blk = write_pattern_block(&ps).unwrap();
|
||||
assert_eq!(blk.len() % 4, 0);
|
||||
assert_eq!(parse_pattern_block(&blk).unwrap(), ps);
|
||||
let f = write_pat_file(&ps).unwrap();
|
||||
assert_eq!(parse_pat_file(&f).unwrap(), ps);
|
||||
// Re-serialising is byte-stable.
|
||||
assert_eq!(write_pattern_block(&parse_pattern_block(&blk).unwrap()).unwrap(), blk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets of the virtual memory arrays of a one-pattern block (each at its "written" flag),
|
||||
/// with whether each is written, and the end of the array list.
|
||||
fn vma_slots(block: &[u8], p: &PsdPattern) -> (Vec<(usize, bool)>, usize) {
|
||||
let rect = pattern_vma_rect_offset(4, p);
|
||||
let end = rect + u32::from_be_bytes(block[rect - 4..rect].try_into().unwrap()) as usize;
|
||||
let (mut at, mut slots) = (rect + 20, Vec::new());
|
||||
while at < end {
|
||||
let written = u32::from_be_bytes(block[at..at + 4].try_into().unwrap()) != 0;
|
||||
slots.push((at, written));
|
||||
at += 4;
|
||||
if written {
|
||||
at += 4 + u32::from_be_bytes(block[at..at + 4].try_into().unwrap()) as usize;
|
||||
}
|
||||
}
|
||||
(slots, end)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparency_goes_in_the_sheet_mask_array_like_photoshop() {
|
||||
// #8054: transparency in the user mask array (slot 24) made Photoshop refuse the file;
|
||||
// Photoshop keeps it in the sheet mask array (slot 25).
|
||||
for depth in [8, 16, 32] {
|
||||
let p = pat(depth, true);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let (slots, end) = vma_slots(&block, &p);
|
||||
let written: Vec<usize> = slots.iter().enumerate().filter(|(_, s)| s.1).map(|(i, _)| i).collect();
|
||||
assert_eq!((slots.len(), written), (26, vec![0, 1, 2, 25]), "{depth}-bit");
|
||||
assert!(!block_has_user_mask(&block));
|
||||
// The layout older PhotoCraft builds wrote still opens, and is detected.
|
||||
block[slots[24].0..end].rotate_left(4);
|
||||
let (slots, _) = vma_slots(&block, &p);
|
||||
assert!(slots[24].1 && !slots[25].1);
|
||||
assert!(block_has_user_mask(&block));
|
||||
assert_eq!(parse_pattern_block(&block).unwrap(), vec![p]);
|
||||
}
|
||||
assert!(!block_has_user_mask(&write_pattern_block(&[pat(8, false)]).unwrap()));
|
||||
for junk in [&[][..], &[0, 0, 0, 9, 1], &[0xff; 64]] {
|
||||
assert!(!block_has_user_mask(junk));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_input_errors_without_panicking() {
|
||||
let blk = write_pattern_block(&[pat(8, true)]).unwrap();
|
||||
for cut in [3, 10, 40, blk.len() - 5] {
|
||||
let _ = parse_pattern_block(&blk[..cut]);
|
||||
}
|
||||
assert!(parse_pat_file(b"8BPS\0\x01").is_err());
|
||||
assert!(parse_pattern_block(&[]).unwrap().is_empty());
|
||||
}
|
||||
|
||||
fn pattern_vma_rect_offset(pattern_start: usize, p: &PsdPattern) -> usize {
|
||||
let name_units = p.name.encode_utf16().count() + 1;
|
||||
pattern_start + 16 + name_units * 2 + 1 + p.id.len() + if p.mode == 2 { 768 } else { 0 } + 8
|
||||
}
|
||||
|
||||
fn set_i32(bytes: &mut [u8], offset: usize, value: i32) {
|
||||
bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_pattern_rectangles_are_rejected_through_all_import_routes() {
|
||||
let p = pat(8, false);
|
||||
|
||||
// A subtraction of the hostile endpoints overflowed before rectangle validation.
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 4, i32::MIN);
|
||||
set_i32(&mut global, vma + 12, i32::MAX);
|
||||
assert!(parse_pattern_block(&global).is_err());
|
||||
|
||||
let mut standalone = write_pat_file(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(10, &p);
|
||||
set_i32(&mut standalone, vma + 4, i32::MIN);
|
||||
set_i32(&mut standalone, vma + 12, i32::MAX);
|
||||
assert!(parse_pat_file(&standalone).is_err());
|
||||
|
||||
let mut abr = crate::abr::write_v6(
|
||||
1,
|
||||
&[crate::abr::AbrSample { id: String::new(), width: 1, height: 1, depth: 8, data: vec![0] }],
|
||||
std::slice::from_ref(&p),
|
||||
&[],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let section = abr.windows(4).position(|w| w == b"patt").unwrap();
|
||||
let vma = pattern_vma_rect_offset(section + 8, &p) + 4;
|
||||
set_i32(&mut abr, vma + 4, i32::MIN);
|
||||
set_i32(&mut abr, vma + 12, i32::MAX);
|
||||
let parsed = crate::abr::parse(&abr).unwrap();
|
||||
assert!(parsed.patterns.is_empty());
|
||||
assert!(parsed.warnings.iter().any(|warning| warning.contains("embedded patterns unreadable")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huge_pattern_rect_with_tiny_channel_payload_is_rejected() {
|
||||
// RGB32 needs 10.8 GB; CMYK16 plus alpha needs 9 GB. Both exceed the
|
||||
// existing pattern budget, but their first plane fits on 64-bit targets.
|
||||
// The complete metadata must be checked before allocating that first plane.
|
||||
huge_pattern_rect_is_rejected(pat(32, false));
|
||||
let mut cmyk = pat(16, true);
|
||||
cmyk.mode = 4;
|
||||
cmyk.channels.push(vec![0; 5 * 3 * 2]);
|
||||
huge_pattern_rect_is_rejected(cmyk);
|
||||
}
|
||||
|
||||
fn huge_pattern_rect_is_rejected(p: PsdPattern) {
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 8, 30_000);
|
||||
set_i32(&mut global, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pattern_block(&global), Err(PsdError::LimitExceeded(_))));
|
||||
|
||||
let mut standalone = write_pat_file(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(10, &p);
|
||||
set_i32(&mut standalone, vma + 8, 30_000);
|
||||
set_i32(&mut standalone, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pat_file(&standalone), Err(PsdError::LimitExceeded(_))));
|
||||
|
||||
let mut abr = crate::abr::write_v6(
|
||||
1,
|
||||
&[crate::abr::AbrSample { id: String::new(), width: 1, height: 1, depth: 8, data: vec![0] }],
|
||||
std::slice::from_ref(&p),
|
||||
&[],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let section = abr.windows(4).position(|w| w == b"patt").unwrap();
|
||||
let vma = pattern_vma_rect_offset(section + 8, &p) + 4;
|
||||
set_i32(&mut abr, vma + 8, 30_000);
|
||||
set_i32(&mut abr, vma + 12, 30_000);
|
||||
let parsed = crate::abr::parse(&abr).unwrap();
|
||||
assert!(parsed.patterns.is_empty());
|
||||
assert!(parsed.warnings.iter().any(|warning| warning.contains("embedded patterns unreadable")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_channel_rectangles_are_placed_inside_the_pattern() {
|
||||
for depth in [8, 16, 32] {
|
||||
let original = pat(depth, true);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&original)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &original);
|
||||
set_i32(&mut block, vma, -2);
|
||||
set_i32(&mut block, vma + 4, -3);
|
||||
set_i32(&mut block, vma + 8, 4);
|
||||
set_i32(&mut block, vma + 12, 5);
|
||||
|
||||
let parsed = parse_pattern_block(&block).unwrap();
|
||||
let parsed = parsed.first().unwrap();
|
||||
assert_eq!((parsed.width, parsed.height), (8, 6));
|
||||
let bytes_per_sample = usize::from(depth / 8);
|
||||
for (actual, expected) in parsed.channels.iter().zip(&original.channels).chain(parsed.alpha.iter().zip(original.alpha.iter())) {
|
||||
for y in 0..6usize {
|
||||
for x in 0..8usize {
|
||||
let at = (y * 8 + x) * bytes_per_sample;
|
||||
let actual_sample = actual.get(at..at + bytes_per_sample).unwrap();
|
||||
if (2..5).contains(&y) && (3..8).contains(&x) {
|
||||
let source_at = ((y - 2) * 5 + (x - 3)) * bytes_per_sample;
|
||||
assert_eq!(actual_sample, expected.get(source_at..source_at + bytes_per_sample).unwrap());
|
||||
} else {
|
||||
assert!(actual_sample.iter().all(|&sample| sample == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_bit_offset_channel_placement_preserves_msb_first_pixels() {
|
||||
let source = PlaneLayout { planes: 1, width: 5, height: 2, depth: 1, version: Version::Psd };
|
||||
let destination = PlaneLayout { planes: 1, width: 8, height: 4, depth: 1, version: Version::Psd };
|
||||
let placed = place_channel_plane(vec![0b1010_1000, 0b0101_0000], &source, 2, 1, &destination).unwrap();
|
||||
assert_eq!(placed, [0, 0b0010_1010, 0b0001_0100, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_size_channels_reuse_the_decoded_allocation() {
|
||||
for depth in [1, 8, 16, 32] {
|
||||
let layout = PlaneLayout { planes: 1, width: 5, height: 3, depth, version: Version::Psd };
|
||||
let plane = vec![0x55; layout.decoded_len().unwrap()];
|
||||
let original = plane.as_ptr();
|
||||
let placed = place_channel_plane(plane, &layout, 0, 0, &layout).unwrap();
|
||||
assert_eq!(placed.as_ptr(), original);
|
||||
let expected = if depth == 1 { 0x50 } else { 0x55 };
|
||||
assert!(placed.iter().all(|&byte| byte == expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_channel_padding_within_the_existing_budget_is_allowed() {
|
||||
let mut p = pat(8, false);
|
||||
p.mode = 1;
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
// More than 64 MiB of implicit padding is valid: the fix must not introduce
|
||||
// a smaller, padding-specific format limit to avoid the oversized RGB case.
|
||||
set_i32(&mut block, vma + 8, 8193);
|
||||
set_i32(&mut block, vma + 12, 8193);
|
||||
let parsed = parse_pattern_block(&block).unwrap();
|
||||
let plane = &parsed[0].channels[0];
|
||||
assert_eq!(plane.len(), 8193 * 8193);
|
||||
for y in 0..3 {
|
||||
assert_eq!(&plane[y * 8193..y * 8193 + 5], &p.channels[0][y * 5..y * 5 + 5]);
|
||||
}
|
||||
assert_eq!(plane[5], 0);
|
||||
assert_eq!(plane[plane.len() - 1], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padded_plane_allocation_failure_is_an_error() {
|
||||
// Capacity overflow is deterministic and never asks the OS for huge memory.
|
||||
assert!(matches!(zeroed_pattern_plane(usize::MAX), Err(PsdError::LimitExceeded("not enough memory for the padded pattern channel"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_channels_in_a_huge_pattern_rect_are_not_zero_padded_without_bound() {
|
||||
// 3 channels of 5x3 padded to 30000x30000 would allocate 2.7 GB of zeros.
|
||||
let p = pat(8, false);
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 8, 30_000);
|
||||
set_i32(&mut global, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pattern_block(&global), Err(PsdError::LimitExceeded("padded pattern channels exceed MAX_PADDED_PATTERN_BYTES"))));
|
||||
}
|
||||
|
||||
/// A one-channel gray pattern whose 5x3 channel is padded into a `size`x`size` rectangle.
|
||||
fn padded_gray_block(size: i32) -> Vec<u8> {
|
||||
let mut p = pat(8, false);
|
||||
p.mode = 1;
|
||||
p.channels.truncate(1);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut block, vma + 8, size);
|
||||
set_i32(&mut block, vma + 12, size);
|
||||
block
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_block_keeps_partial_charge_for_the_next_block() {
|
||||
let one = padded_gray_block(100);
|
||||
let mut malformed = one.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(parse_pattern_block_with(&malformed, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_budget_is_shared_by_every_pattern_of_a_parse() {
|
||||
// Each pattern pads 100x100 = 10000 bytes; a 15000-byte budget fits one, not two. Before
|
||||
// the fix each pattern got a fresh allowance, so N tiny patterns committed N times the cap.
|
||||
let one = padded_gray_block(100);
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert_eq!(parse_pattern_block_with(&one, &mut budget).unwrap().len(), 1);
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
|
||||
let two = [one.clone(), one.clone()].concat();
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(matches!(parse_pattern_block_with(&two, &mut budget), Err(PsdError::LimitExceeded(_))));
|
||||
// Charges are monotonic: the first pattern's 10000 bytes stay charged even when the second
|
||||
// record fails.
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
|
||||
// Two blocks of one document share a budget too.
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_ok());
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_at_different_depths_are_rejected() {
|
||||
// The second colour channel claims 8 bits in a 16-bit pattern. It used to be accepted with
|
||||
// the pattern's depth taken from the last channel, so the result couldn't be written back.
|
||||
let p = pat(16, false);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let first = pattern_vma_rect_offset(4, &p) + 20;
|
||||
let first_len = u32::from_be_bytes(block[first + 4..first + 8].try_into().unwrap()) as usize;
|
||||
let second = first + 8 + first_len;
|
||||
block[second + 8..second + 12].copy_from_slice(&8u32.to_be_bytes());
|
||||
block[second + 28..second + 30].copy_from_slice(&8u16.to_be_bytes());
|
||||
let err = parse_pattern_block(&block).unwrap_err().to_string();
|
||||
assert!(err.contains("different depths"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_size_pattern_rectangles_are_rejected() {
|
||||
let p = pat(8, false);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut block, vma + 8, 0);
|
||||
assert!(parse_pattern_block(&block).is_err());
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
use super::*;
|
||||
|
||||
fn pat(depth: u16, alpha: bool) -> PsdPattern {
|
||||
let (w, h) = (5u32, 3u32);
|
||||
let bpp = usize::from(depth / 8);
|
||||
let plane = |s: u8| (0..w as usize * h as usize * bpp).map(|i| (i as u8).wrapping_mul(7).wrapping_add(s)).collect::<Vec<u8>>();
|
||||
PsdPattern {
|
||||
mode: 3,
|
||||
width: w,
|
||||
height: h,
|
||||
name: "Test ✓".into(),
|
||||
id: "0bd2d3ba-1234-11d4-8f8f-aabbccddeeff".into(),
|
||||
palette: None,
|
||||
depth,
|
||||
channels: vec![plane(1), plane(2), plane(3)],
|
||||
alpha: alpha.then(|| plane(9)),
|
||||
}
|
||||
}
|
||||
|
||||
/// #9318 review: a name the reader refuses (more than 65,536 UTF-16 units with its NUL) is
|
||||
/// refused on write, so a saved library can't become a file that loads nothing.
|
||||
#[test]
|
||||
fn a_name_the_reader_refuses_is_refused_on_write() {
|
||||
let long = PsdPattern { name: "a".repeat(65_536), ..pat(8, false) };
|
||||
assert!(matches!(write_pat_file(&[long]), Err(PsdError::LimitExceeded(_))));
|
||||
let longest = PsdPattern { name: "a".repeat(65_535), ..pat(8, false) };
|
||||
assert_eq!(parse_pat_file(&write_pat_file(std::slice::from_ref(&longest)).unwrap()).unwrap(), [longest]);
|
||||
}
|
||||
|
||||
/// #9318 review: more patterns than a `.pat` holds are refused on write, and a file claiming
|
||||
/// more is refused on read rather than silently cut to the first 100,000.
|
||||
#[test]
|
||||
fn more_patterns_than_a_pat_file_holds_are_refused() {
|
||||
let many = vec![pat(8, false); MAX_PAT_PATTERNS + 1];
|
||||
assert!(matches!(write_pat_file(&many), Err(PsdError::LimitExceeded(_))));
|
||||
let mut header = b"8BPT".to_vec();
|
||||
header.extend_from_slice(&1u16.to_be_bytes());
|
||||
header.extend_from_slice(&(MAX_PAT_PATTERNS as u32 + 1).to_be_bytes());
|
||||
assert!(matches!(parse_pat_file(&header), Err(PsdError::LimitExceeded(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_and_pat_file_round_trip_at_every_depth() {
|
||||
for depth in [8, 16, 32] {
|
||||
for alpha in [false, true] {
|
||||
let ps = vec![pat(depth, alpha), PsdPattern { name: "Second".into(), ..pat(depth, false) }];
|
||||
let blk = write_pattern_block(&ps).unwrap();
|
||||
assert_eq!(blk.len() % 4, 0);
|
||||
assert_eq!(parse_pattern_block(&blk).unwrap(), ps);
|
||||
let f = write_pat_file(&ps).unwrap();
|
||||
assert_eq!(parse_pat_file(&f).unwrap(), ps);
|
||||
// Re-serialising is byte-stable.
|
||||
assert_eq!(write_pattern_block(&parse_pattern_block(&blk).unwrap()).unwrap(), blk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets of the virtual memory arrays of a one-pattern block (each at its "written" flag),
|
||||
/// with whether each is written, and the end of the array list.
|
||||
fn vma_slots(block: &[u8], p: &PsdPattern) -> (Vec<(usize, bool)>, usize) {
|
||||
let rect = pattern_vma_rect_offset(4, p);
|
||||
let end = rect + u32::from_be_bytes(block[rect - 4..rect].try_into().unwrap()) as usize;
|
||||
let (mut at, mut slots) = (rect + 20, Vec::new());
|
||||
while at < end {
|
||||
let written = u32::from_be_bytes(block[at..at + 4].try_into().unwrap()) != 0;
|
||||
slots.push((at, written));
|
||||
at += 4;
|
||||
if written {
|
||||
at += 4 + u32::from_be_bytes(block[at..at + 4].try_into().unwrap()) as usize;
|
||||
}
|
||||
}
|
||||
(slots, end)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparency_goes_in_the_sheet_mask_array_like_photoshop() {
|
||||
// #8054: transparency in the user mask array (slot 24) made Photoshop refuse the file;
|
||||
// Photoshop keeps it in the sheet mask array (slot 25).
|
||||
for depth in [8, 16, 32] {
|
||||
let p = pat(depth, true);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let (slots, end) = vma_slots(&block, &p);
|
||||
let written: Vec<usize> = slots.iter().enumerate().filter(|(_, s)| s.1).map(|(i, _)| i).collect();
|
||||
assert_eq!((slots.len(), written), (26, vec![0, 1, 2, 25]), "{depth}-bit");
|
||||
assert!(!block_has_user_mask(&block));
|
||||
// The layout older PhotoCraft builds wrote still opens, and is detected.
|
||||
block[slots[24].0..end].rotate_left(4);
|
||||
let (slots, _) = vma_slots(&block, &p);
|
||||
assert!(slots[24].1 && !slots[25].1);
|
||||
assert!(block_has_user_mask(&block));
|
||||
assert_eq!(parse_pattern_block(&block).unwrap(), vec![p]);
|
||||
}
|
||||
assert!(!block_has_user_mask(&write_pattern_block(&[pat(8, false)]).unwrap()));
|
||||
for junk in [&[][..], &[0, 0, 0, 9, 1], &[0xff; 64]] {
|
||||
assert!(!block_has_user_mask(junk));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_input_errors_without_panicking() {
|
||||
let blk = write_pattern_block(&[pat(8, true)]).unwrap();
|
||||
for cut in [3, 10, 40, blk.len() - 5] {
|
||||
let _ = parse_pattern_block(&blk[..cut]);
|
||||
}
|
||||
assert!(parse_pat_file(b"8BPS\0\x01").is_err());
|
||||
assert!(parse_pattern_block(&[]).unwrap().is_empty());
|
||||
}
|
||||
|
||||
fn pattern_vma_rect_offset(pattern_start: usize, p: &PsdPattern) -> usize {
|
||||
let name_units = p.name.encode_utf16().count() + 1;
|
||||
pattern_start + 16 + name_units * 2 + 1 + p.id.len() + if p.mode == 2 { 768 } else { 0 } + 8
|
||||
}
|
||||
|
||||
fn set_i32(bytes: &mut [u8], offset: usize, value: i32) {
|
||||
bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_pattern_rectangles_are_rejected_through_all_import_routes() {
|
||||
let p = pat(8, false);
|
||||
|
||||
// A subtraction of the hostile endpoints overflowed before rectangle validation.
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 4, i32::MIN);
|
||||
set_i32(&mut global, vma + 12, i32::MAX);
|
||||
assert!(parse_pattern_block(&global).is_err());
|
||||
|
||||
let mut standalone = write_pat_file(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(10, &p);
|
||||
set_i32(&mut standalone, vma + 4, i32::MIN);
|
||||
set_i32(&mut standalone, vma + 12, i32::MAX);
|
||||
assert!(parse_pat_file(&standalone).is_err());
|
||||
|
||||
let mut abr = crate::abr::write_v6(
|
||||
1,
|
||||
&[crate::abr::AbrSample { id: String::new(), width: 1, height: 1, depth: 8, data: vec![0] }],
|
||||
std::slice::from_ref(&p),
|
||||
&[],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let section = abr.windows(4).position(|w| w == b"patt").unwrap();
|
||||
let vma = pattern_vma_rect_offset(section + 8, &p) + 4;
|
||||
set_i32(&mut abr, vma + 4, i32::MIN);
|
||||
set_i32(&mut abr, vma + 12, i32::MAX);
|
||||
let parsed = crate::abr::parse(&abr).unwrap();
|
||||
assert!(parsed.patterns.is_empty());
|
||||
assert!(parsed.warnings.iter().any(|warning| warning.contains("embedded patterns unreadable")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huge_pattern_rect_with_tiny_channel_payload_is_rejected() {
|
||||
// RGB32 needs 10.8 GB; CMYK16 plus alpha needs 9 GB. Both exceed the
|
||||
// existing pattern budget, but their first plane fits on 64-bit targets.
|
||||
// The complete metadata must be checked before allocating that first plane.
|
||||
huge_pattern_rect_is_rejected(pat(32, false));
|
||||
let mut cmyk = pat(16, true);
|
||||
cmyk.mode = 4;
|
||||
cmyk.channels.push(vec![0; 5 * 3 * 2]);
|
||||
huge_pattern_rect_is_rejected(cmyk);
|
||||
}
|
||||
|
||||
fn huge_pattern_rect_is_rejected(p: PsdPattern) {
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 8, 30_000);
|
||||
set_i32(&mut global, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pattern_block(&global), Err(PsdError::LimitExceeded(_))));
|
||||
|
||||
let mut standalone = write_pat_file(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(10, &p);
|
||||
set_i32(&mut standalone, vma + 8, 30_000);
|
||||
set_i32(&mut standalone, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pat_file(&standalone), Err(PsdError::LimitExceeded(_))));
|
||||
|
||||
let mut abr = crate::abr::write_v6(
|
||||
1,
|
||||
&[crate::abr::AbrSample { id: String::new(), width: 1, height: 1, depth: 8, data: vec![0] }],
|
||||
std::slice::from_ref(&p),
|
||||
&[],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let section = abr.windows(4).position(|w| w == b"patt").unwrap();
|
||||
let vma = pattern_vma_rect_offset(section + 8, &p) + 4;
|
||||
set_i32(&mut abr, vma + 8, 30_000);
|
||||
set_i32(&mut abr, vma + 12, 30_000);
|
||||
let parsed = crate::abr::parse(&abr).unwrap();
|
||||
assert!(parsed.patterns.is_empty());
|
||||
assert!(parsed.warnings.iter().any(|warning| warning.contains("embedded patterns unreadable")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_channel_rectangles_are_placed_inside_the_pattern() {
|
||||
for depth in [8, 16, 32] {
|
||||
let original = pat(depth, true);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&original)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &original);
|
||||
set_i32(&mut block, vma, -2);
|
||||
set_i32(&mut block, vma + 4, -3);
|
||||
set_i32(&mut block, vma + 8, 4);
|
||||
set_i32(&mut block, vma + 12, 5);
|
||||
|
||||
let parsed = parse_pattern_block(&block).unwrap();
|
||||
let parsed = parsed.first().unwrap();
|
||||
assert_eq!((parsed.width, parsed.height), (8, 6));
|
||||
let bytes_per_sample = usize::from(depth / 8);
|
||||
for (actual, expected) in parsed.channels.iter().zip(&original.channels).chain(parsed.alpha.iter().zip(original.alpha.iter())) {
|
||||
for y in 0..6usize {
|
||||
for x in 0..8usize {
|
||||
let at = (y * 8 + x) * bytes_per_sample;
|
||||
let actual_sample = actual.get(at..at + bytes_per_sample).unwrap();
|
||||
if (2..5).contains(&y) && (3..8).contains(&x) {
|
||||
let source_at = ((y - 2) * 5 + (x - 3)) * bytes_per_sample;
|
||||
assert_eq!(actual_sample, expected.get(source_at..source_at + bytes_per_sample).unwrap());
|
||||
} else {
|
||||
assert!(actual_sample.iter().all(|&sample| sample == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_bit_offset_channel_placement_preserves_msb_first_pixels() {
|
||||
let source = PlaneLayout { planes: 1, width: 5, height: 2, depth: 1, version: Version::Psd };
|
||||
let destination = PlaneLayout { planes: 1, width: 8, height: 4, depth: 1, version: Version::Psd };
|
||||
let placed = place_channel_plane(vec![0b1010_1000, 0b0101_0000], &source, 2, 1, &destination).unwrap();
|
||||
assert_eq!(placed, [0, 0b0010_1010, 0b0001_0100, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_size_channels_reuse_the_decoded_allocation() {
|
||||
for depth in [1, 8, 16, 32] {
|
||||
let layout = PlaneLayout { planes: 1, width: 5, height: 3, depth, version: Version::Psd };
|
||||
let plane = vec![0x55; layout.decoded_len().unwrap()];
|
||||
let original = plane.as_ptr();
|
||||
let placed = place_channel_plane(plane, &layout, 0, 0, &layout).unwrap();
|
||||
assert_eq!(placed.as_ptr(), original);
|
||||
let expected = if depth == 1 { 0x50 } else { 0x55 };
|
||||
assert!(placed.iter().all(|&byte| byte == expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_channel_padding_within_the_existing_budget_is_allowed() {
|
||||
let mut p = pat(8, false);
|
||||
p.mode = 1;
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
// More than 64 MiB of implicit padding is valid: the fix must not introduce
|
||||
// a smaller, padding-specific format limit to avoid the oversized RGB case.
|
||||
set_i32(&mut block, vma + 8, 8193);
|
||||
set_i32(&mut block, vma + 12, 8193);
|
||||
let parsed = parse_pattern_block(&block).unwrap();
|
||||
let plane = &parsed[0].channels[0];
|
||||
assert_eq!(plane.len(), 8193 * 8193);
|
||||
for y in 0..3 {
|
||||
assert_eq!(&plane[y * 8193..y * 8193 + 5], &p.channels[0][y * 5..y * 5 + 5]);
|
||||
}
|
||||
assert_eq!(plane[5], 0);
|
||||
assert_eq!(plane[plane.len() - 1], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padded_plane_allocation_failure_is_an_error() {
|
||||
// Capacity overflow is deterministic and never asks the OS for huge memory.
|
||||
assert!(matches!(zeroed_pattern_plane(usize::MAX), Err(PsdError::LimitExceeded("not enough memory for the padded pattern channel"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_channels_in_a_huge_pattern_rect_are_not_zero_padded_without_bound() {
|
||||
// 3 channels of 5x3 padded to 30000x30000 would allocate 2.7 GB of zeros.
|
||||
let p = pat(8, false);
|
||||
let mut global = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut global, vma + 8, 30_000);
|
||||
set_i32(&mut global, vma + 12, 30_000);
|
||||
assert!(matches!(parse_pattern_block(&global), Err(PsdError::LimitExceeded("padded pattern channels exceed MAX_PADDED_PATTERN_BYTES"))));
|
||||
}
|
||||
|
||||
/// A one-channel gray pattern whose 5x3 channel is padded into a `size`x`size` rectangle.
|
||||
fn padded_gray_block(size: i32) -> Vec<u8> {
|
||||
let mut p = pat(8, false);
|
||||
p.mode = 1;
|
||||
p.channels.truncate(1);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut block, vma + 8, size);
|
||||
set_i32(&mut block, vma + 12, size);
|
||||
block
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_block_keeps_partial_charge_for_the_next_block() {
|
||||
let one = padded_gray_block(100);
|
||||
let mut malformed = one.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(parse_pattern_block_with(&malformed, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_budget_is_shared_by_every_pattern_of_a_parse() {
|
||||
// Each pattern pads 100x100 = 10000 bytes; a 15000-byte budget fits one, not two. Before
|
||||
// the fix each pattern got a fresh allowance, so N tiny patterns committed N times the cap.
|
||||
let one = padded_gray_block(100);
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert_eq!(parse_pattern_block_with(&one, &mut budget).unwrap().len(), 1);
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
|
||||
let two = [one.clone(), one.clone()].concat();
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(matches!(parse_pattern_block_with(&two, &mut budget), Err(PsdError::LimitExceeded(_))));
|
||||
// Charges are monotonic: the first pattern's 10000 bytes stay charged even when the second
|
||||
// record fails.
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
|
||||
// Two blocks of one document share a budget too.
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_ok());
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_at_different_depths_are_rejected() {
|
||||
// The second colour channel claims 8 bits in a 16-bit pattern. It used to be accepted with
|
||||
// the pattern's depth taken from the last channel, so the result couldn't be written back.
|
||||
let p = pat(16, false);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let first = pattern_vma_rect_offset(4, &p) + 20;
|
||||
let first_len = u32::from_be_bytes(block[first + 4..first + 8].try_into().unwrap()) as usize;
|
||||
let second = first + 8 + first_len;
|
||||
block[second + 8..second + 12].copy_from_slice(&8u32.to_be_bytes());
|
||||
block[second + 28..second + 30].copy_from_slice(&8u16.to_be_bytes());
|
||||
let err = parse_pattern_block(&block).unwrap_err().to_string();
|
||||
assert!(err.contains("different depths"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_size_pattern_rectangles_are_rejected() {
|
||||
let p = pat(8, false);
|
||||
let mut block = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let vma = pattern_vma_rect_offset(4, &p);
|
||||
set_i32(&mut block, vma + 8, 0);
|
||||
assert!(parse_pattern_block(&block).is_err());
|
||||
}
|
||||
+2
-2400
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user