mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
PhotoCraft's `photocraft-psd` crate (crates/psd at storytold/photocraft 63b641b55e54bf8f5c83efd977fdb16a2c255431) as `craft-psd`: the lossless PSD/PSB model, reader, writer and builder, descriptors, the Photoshop resource files that share its primitives (.abr, .asl, .aco, .act, .ase, .grd, .pat, .atn, .csh, .shc) and the Photoshop blocks of a layered TIFF. The crate had no dependency on any other PhotoCraft crate. Code is unchanged apart from the crate name, craft-libs' rustfmt width, comments that named other PhotoCraft crates, issue references (now `photocraft#N`), and the corpus tests' directory (`CRAFT_PSD_CORPUS` or `corpus/` at the workspace root). Tests, proptests, fuzz targets and fixtures move with it. Part of #778, #786.
102 lines
4.1 KiB
Rust
102 lines
4.1 KiB
Rust
use craft_psd::PsdError;
|
|
use craft_psd::csh;
|
|
use craft_psd::path::{PathData, PsdKnot, PsdSubpath};
|
|
|
|
fn fixture(names: &[&str]) -> Vec<u8> {
|
|
let mut bytes = b"cush".to_vec();
|
|
bytes.extend_from_slice(&2u32.to_be_bytes());
|
|
bytes.extend_from_slice(&(names.len() as u32).to_be_bytes());
|
|
for name in names {
|
|
let units: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
|
bytes.extend_from_slice(&(units.len() as u32).to_be_bytes());
|
|
for unit in units {
|
|
bytes.extend_from_slice(&unit.to_be_bytes());
|
|
}
|
|
while !bytes.len().is_multiple_of(4) {
|
|
bytes.push(0);
|
|
}
|
|
bytes.extend_from_slice(&1u32.to_be_bytes());
|
|
let mut body = vec![2, b'i', b'd'];
|
|
for bound in [-10i32, -20, 90, 180] {
|
|
body.extend_from_slice(&bound.to_be_bytes());
|
|
}
|
|
let knot = PsdKnot { linked: true, pre: (0.125, 0.25), anchor: (0.5, 0.5), post: (0.75, 0.875) };
|
|
let path = PathData::from_subpaths(&[PsdSubpath { closed: false, operation: 0, knots: vec![knot] }], false);
|
|
body.extend_from_slice(&path.to_bytes());
|
|
while !body.len().is_multiple_of(4) {
|
|
body.push(0);
|
|
}
|
|
bytes.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
|
bytes.extend_from_slice(&body);
|
|
}
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
fn reads_multiple_names_padding_signed_bounds_and_cubic_controls() {
|
|
let bytes = fixture(&["Even", "Odd", "形😀"]);
|
|
let shapes = csh::parse(&bytes).unwrap();
|
|
assert_eq!(shapes.iter().map(|s| s.name.as_str()).collect::<Vec<_>>(), ["Even", "Odd", "形😀"]);
|
|
for shape in shapes {
|
|
assert_eq!(shape.bounds, [-10, -20, 90, 180]);
|
|
assert_eq!(shape.id, "id");
|
|
let subpaths = shape.path.subpaths();
|
|
assert_eq!(subpaths.len(), 1);
|
|
assert!(!subpaths[0].closed);
|
|
let knot = subpaths[0].knots[0];
|
|
assert!(knot.linked);
|
|
assert_eq!(knot.pre, (0.125, 0.25));
|
|
assert_eq!(knot.anchor, (0.5, 0.5));
|
|
assert_eq!(knot.post, (0.75, 0.875));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_every_truncated_prefix_without_panicking() {
|
|
let bytes = fixture(&["One", "Two"]);
|
|
for end in 0..bytes.len() {
|
|
assert!(csh::parse(&bytes[..end]).is_err(), "accepted truncated prefix {end}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unsupported_headers_excessive_counts_and_invalid_unicode() {
|
|
let bytes = fixture(&["A"]);
|
|
for (offset, value) in [(4, 3u32), (8, u32::MAX), (12, u32::MAX), (24, u32::MAX)] {
|
|
let mut broken = bytes.clone();
|
|
broken[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
|
|
assert!(csh::parse(&broken).is_err(), "offset {offset}");
|
|
}
|
|
// The count and name limits answer before the reader runs out of bytes, so a removed limit
|
|
// cannot hide behind a truncation error. One past each limit is refused by the limit itself.
|
|
for (offset, value, limit) in [
|
|
(8, u32::MAX, "CSH shape count"),
|
|
(8, csh::MAX_SHAPES as u32 + 1, "CSH shape count"),
|
|
(12, u32::MAX, "CSH name length"),
|
|
(12, csh::MAX_NAME_UNITS as u32 + 1, "CSH name length"),
|
|
] {
|
|
let mut broken = bytes.clone();
|
|
broken[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
|
|
assert_eq!(csh::parse(&broken), Err(PsdError::LimitExceeded(limit)), "offset {offset} value {value}");
|
|
}
|
|
let mut broken = bytes.clone();
|
|
broken[16..18].copy_from_slice(&0xd800u16.to_be_bytes());
|
|
assert!(csh::parse(&broken).is_err());
|
|
let mut trailing = bytes;
|
|
trailing.push(0);
|
|
assert!(csh::parse(&trailing).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_knots_and_mismatched_open_closed_records() {
|
|
let bytes = fixture(&["A"]);
|
|
// Header/name/version/length = 28; id = 3; bounds = 16; two fill records = 52.
|
|
let length_record = 28 + 3 + 16 + 52;
|
|
let mut missing = bytes.clone();
|
|
missing[length_record + 2..length_record + 4].copy_from_slice(&2u16.to_be_bytes());
|
|
assert!(csh::parse(&missing).is_err());
|
|
let mut mismatched = bytes;
|
|
mismatched[length_record..length_record + 2].copy_from_slice(&0u16.to_be_bytes());
|
|
assert!(csh::parse(&mismatched).is_err());
|
|
}
|