Files
Dylan M. Taylor fa26a2a4c6 Add craft-pdn: Paint.NET PDN3 reader and a data-only MS-NRBF reader, from PhotoCraft (#968)
The format half of PhotoCraft's Paint.NET import (crates/io/src/pdn.rs and pdn/nrbf.rs at
abeb16c96): the PDN3 header, the .NET binary serialization graph, the layer list and blend
modes, and the chunked gzip pixel blocks. Surfaces, blend-mode mapping and warnings stay in
the app.
2026-11-11 19:30:55 -05:00

445 lines
18 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
use std::io::Write;
enum Field {
Int(i32),
Long(i64),
Byte(u8),
Bool(bool),
Ref(i32),
}
struct Writer(Vec<u8>);
impl Writer {
fn int(&mut self, n: i32) {
self.0.extend(n.to_le_bytes());
}
fn string(&mut self, s: &str) {
let mut len = s.len();
while len >= 128 {
self.0.push((len as u8 & 127) | 128);
len >>= 7;
}
self.0.push(len as u8);
self.0.extend(s.as_bytes());
}
fn object(&mut self, id: i32, class: &str, fields: &[(&str, Field)]) {
self.0.push(5);
self.int(id);
self.string(class);
self.int(fields.len() as i32);
for (name, _) in fields {
self.string(name);
}
for (_, f) in fields {
self.0.push(if matches!(f, Field::Ref(_)) { 2 } else { 0 });
}
for (_, f) in fields {
match f {
Field::Int(_) => self.0.push(8),
Field::Long(_) => self.0.push(9),
Field::Byte(_) => self.0.push(2),
Field::Bool(_) => self.0.push(1),
Field::Ref(_) => {}
}
}
self.int(1);
for (_, f) in fields {
match f {
Field::Int(n) => self.int(*n),
Field::Long(n) => self.0.extend(n.to_le_bytes()),
Field::Byte(n) => self.0.push(*n),
Field::Bool(b) => self.0.push(u8::from(*b)),
Field::Ref(id) => {
self.0.push(9);
self.int(*id);
}
}
}
}
}
const GO: &dyn Fn() -> bool = &|| true;
/// Parses and decodes everything, as an application would.
fn read_all(bytes: &[u8]) -> Result<(Vec<Layer>, Vec<Surface>)> {
let doc = Document::parse(bytes, GO)?;
let mut surfaces = Vec::new();
doc.read_surfaces(GO, &mut |i, s| {
assert_eq!(i, surfaces.len());
surfaces.push(s);
})?;
assert_eq!(surfaces.iter().map(|s| s.data.len()).sum::<usize>(), doc.decoded_bytes());
Ok((doc.layers, surfaces))
}
/// Independent synthetic PDN writer: padded rows, forward references, a root cycle,
/// spare ArrayList capacity, and reversed gzip/raw chunks. There is no production PDN writer.
fn fixture(modes: &[i32], legacy: bool, compressed: bool) -> Vec<u8> {
use Field::*;
let mut w = Writer(b"PDN3\0\0\0\0\x01".to_vec());
w.0.push(0);
for n in [1, -1, 1, 0] {
w.int(n);
}
w.0.push(12);
w.int(1);
w.string("PaintDotNet.Data");
w.object(1, "PaintDotNet.Document", &[("width", Int(2)), ("height", Int(2)), ("layers", Ref(2))]);
w.object(2, "PaintDotNet.LayerList", &[("parent", Ref(1)), ("ArrayList+_size", Int(modes.len() as i32)), ("ArrayList+_items", Ref(3))]);
w.0.push(16);
w.int(3);
w.int(modes.len() as i32 + 2);
for i in 0..modes.len() {
w.0.push(9);
w.int(16 + i as i32 * 10);
}
w.0.extend([13, 2]);
for (i, mode) in modes.iter().enumerate() {
let id = 16 + i as i32 * 10;
w.object(
id,
"PaintDotNet.BitmapLayer",
&[
("Layer+width", Int(2)),
("Layer+height", Int(2)),
("surface", Ref(id + 1)),
("Layer+properties", Ref(id + 2)),
("properties", Ref(id + 3)),
],
);
w.object(id + 1, "PaintDotNet.Surface", &[("width", Int(2)), ("height", Int(2)), ("stride", Int(12)), ("scan0", Ref(id + 7))]);
let mut fields =
vec![("name", Ref(id + 4)), ("opacity", Byte(if i == 0 { 255 } else { 128 })), ("visible", Bool(i != 1)), ("isBackground", Bool(i == 0))];
if !legacy {
fields.push(("blendMode", Ref(id + 5)));
}
w.object(id + 2, "PaintDotNet.Layer+LayerProperties", &fields);
w.object(id + 3, "PaintDotNet.BitmapLayer+BitmapLayerProperties", &[("blendOp", Ref(id + 6))]);
w.0.push(6);
w.int(id + 4);
w.string(&format!("Läyer {i}"));
w.object(id + 5, "PaintDotNet.LayerBlendMode", &[("value__", Int(*mode))]);
w.object(id + 6, &format!("PaintDotNet.UserBlendOps+{}BlendOp", BlendMode::ALL.get(*mode as usize).map_or("Unknown", |m| m.name())), &[]);
w.object(id + 7, "PaintDotNet.MemoryBlock", &[("length64", Long(24)), ("hasParent", Bool(false)), ("deferred", Bool(true))]);
}
w.0.push(11);
for _ in modes {
w.0.push(if compressed { 0 } else { 1 });
w.0.extend(12u32.to_be_bytes());
for index in [1u32, 0] {
let raw = if index == 0 {
vec![30, 20, 10, 255, 60, 50, 40, 128, 99, 99, 99, 99]
} else {
vec![90, 80, 70, 64, 120, 110, 100, 0, 99, 99, 99, 99]
};
let data = if compressed {
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
enc.write_all(&raw).unwrap();
enc.finish().unwrap()
} else {
raw
};
w.0.extend(index.to_be_bytes());
w.0.extend((data.len() as u32).to_be_bytes());
w.0.extend(data);
}
}
w.0
}
#[test]
fn all_blend_modes_and_layer_properties_are_read() {
let modes: Vec<_> = (0..14).collect();
for legacy in [false, true] {
for compressed in [false, true] {
let bytes = fixture(&modes, legacy, compressed);
assert!(is_pdn(&bytes));
let doc = Document::parse(&bytes, GO).unwrap();
assert_eq!((doc.width, doc.height, doc.layers.len(), doc.surface_count(), doc.has_user_metadata), (2, 2, 14, 14, false));
assert_eq!(doc.decoded_bytes(), 14 * 24);
let (layers, surfaces) = read_all(&bytes).unwrap();
for (i, layer) in layers.iter().enumerate() {
assert_eq!(
layer,
&Layer {
name: format!("Läyer {i}"),
visible: i != 1,
opacity: if i == 0 { 255 } else { 128 },
blend: BlendMode::ALL[i],
surface: i
}
);
// Rows are padded to the stride (12 bytes for 2 pixels); the padding is cut off.
let surface = &surfaces[layer.surface];
assert_eq!((surface.width, surface.stride, surface.data.len()), (2, 12, 24));
let rows: Vec<&[u8]> = surface.rows().collect();
assert_eq!(rows, [&[30, 20, 10, 255, 60, 50, 40, 128][..], &[90, 80, 70, 64, 120, 110, 100, 0]], "B, G, R, A");
}
}
}
}
#[test]
fn truncation_unknown_modes_and_corrupt_chunks_fail_without_panicking() {
let raw = fixture(&[0], false, false);
for len in 0..raw.len() {
assert!(read_all(&raw[..len]).is_err(), "length {len}");
}
assert!(read_all(&raw).is_ok());
let mut trailing = raw.clone();
trailing.push(0);
assert_eq!(read_all(&trailing).err(), Some(Error::Invalid("unexpected trailing data".into())));
for legacy in [false, true] {
assert!(Document::parse(&fixture(&[999], legacy, false), GO).is_err());
assert!(Document::parse(&fixture(&[14], legacy, false), GO).is_err());
}
let mut corrupt = raw.clone();
let block = corrupt.len() - 45;
// Zero chunk size.
corrupt[block + 1..block + 5].fill(0);
assert!(read_all(&corrupt).is_err());
let mut duplicate = raw.clone();
// Both row chunks claim index 1.
duplicate[block + 25..block + 29].copy_from_slice(&1u32.to_be_bytes());
assert!(read_all(&duplicate).is_err());
let mut version = raw.clone();
version[block] = 2;
assert!(read_all(&version).is_err());
let mut zipped = fixture(&[0], false, true);
let last = zipped.len() - 1;
zipped[last] ^= 1;
assert!(Document::parse(&zipped, GO).is_ok() && read_all(&zipped).is_err(), "gzip checksum");
}
#[test]
fn reading_can_be_cancelled_during_parsing_and_pixel_decode() {
use std::cell::Cell;
let bytes = fixture(&[0, 1], false, true);
for limit in [0, 10] {
let checks = Cell::new(0);
let keep_going = || {
checks.set(checks.get() + 1);
checks.get() <= limit
};
assert_eq!(Document::parse(&bytes, &keep_going).err(), Some(Error::Cancelled), "{limit}");
}
let doc = Document::parse(&bytes, GO).unwrap();
let seen = Cell::new(0);
let result = doc.read_surfaces(&|| seen.get() == 0, &mut |_, _| seen.set(seen.get() + 1));
assert_eq!((result, seen.get()), (Err(Error::Cancelled), 1), "cancelled after the first surface");
}
proptest::proptest! {
#[test]
fn arbitrary_serialized_data_returns_an_error_without_panicking(data in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..4096)) {
let mut bytes = b"PDN3\0\0\0\0\x01".to_vec();
bytes.extend(data);
let _ = read_all(&bytes);
}
#[test]
fn mutated_layered_documents_never_panic(edits in proptest::collection::vec((proptest::prelude::any::<usize>(), proptest::prelude::any::<u8>()), 1..8)) {
let mut bytes = fixture(&[0, 5, 13], false, true);
for (offset, value) in edits {
let index = offset % bytes.len();
bytes[index] = value;
}
let _ = read_all(&bytes);
}
}
#[test]
fn malformed_headers_are_errors() {
for bytes in [b"".as_slice(), b"PDN3", b"PDN3\xff\xff\xff", b"PDN4\0\0\0", b"PDN3\0\0\0\0\x02"] {
assert!(matches!(Document::parse(bytes, GO), Err(Error::Invalid(_))), "{bytes:?}");
}
assert!(!is_pdn(b"PDN4\0\0\0") && !is_pdn(b"PD"));
// The XML header between the signature and the stream is skipped by its 24-bit length.
let plain = fixture(&[0], false, true);
let mut with_header = b"PDN3\x05\0\0<pdn>".to_vec();
with_header.extend_from_slice(&plain[7..]);
assert_eq!(read_all(&with_header), read_all(&plain));
}
/// A stream of `records` after the header record.
fn stream(records: impl FnOnce(&mut Writer)) -> Vec<u8> {
let mut w = Writer(vec![0]);
for n in [1, -1, 1, 0] {
w.int(n);
}
records(&mut w);
w.0
}
#[test]
fn serialized_counts_depth_and_references_are_bounded() {
let array = |w: &mut Writer, id: i32, len: i32| {
w.0.push(16);
w.int(id);
w.int(len);
};
let nested = |depth: i32| {
stream(|w| {
(1..=depth).for_each(|id| array(w, id, 1));
w.0.extend([10, 11]);
})
};
let strings = |total: usize| {
stream(|w| {
for id in 1..=(total >> 20) as i32 {
w.0.push(6);
w.int(id);
w.string(&"s".repeat(1 << 20));
}
w.0.push(11);
})
};
let cases: Vec<(&str, Vec<u8>)> = vec![
("an array over the item limit", stream(|w| array(w, 1, nrbf::MAX_ITEMS as i32 + 1))),
("nesting past the depth limit", nested(nrbf::MAX_DEPTH as i32 + 2)),
("strings past the limit", strings(nrbf::MAX_STRINGS + (1 << 20))),
(
"a duplicate object id",
stream(|w| {
for _ in 0..2 {
w.0.push(6);
w.int(1);
w.string("duplicate");
}
w.0.push(11);
}),
),
(
"a null run longer than its array",
stream(|w| {
array(w, 1, 1);
w.0.extend([13, 2, 11]);
}),
),
(
"an object of an undeclared class",
stream(|w| {
w.0.push(1);
w.int(1);
w.int(999);
}),
),
("no root object", stream(|w| w.0.push(11))),
("an unknown record", stream(|w| w.0.push(99))),
("a negative count", stream(|w| array(w, 1, -1))),
];
for (what, bytes) in cases {
let result = nrbf::parse(&bytes, GO).map(|(g, _)| g.array(&g.root).map(|a| a.len()));
assert!(matches!(result, Err(Error::Invalid(_)) | Ok(Err(Error::Invalid(_)))), "{what}");
}
// An array holding a reference to itself is a legal graph: resolving stops at the array.
let own = stream(|w| {
array(w, 1, 1);
w.0.push(9);
w.int(1);
w.0.push(11);
});
let (graph, _) = nrbf::parse(&own, GO).unwrap();
let items = graph.array(&graph.root).unwrap();
assert_eq!(graph.array(&items[0]).map(<[Value]>::len), Ok(1));
assert!(nrbf::parse(&nested(nrbf::MAX_DEPTH as i32), GO).is_ok());
assert!(nrbf::parse(&strings(nrbf::MAX_STRINGS), GO).is_ok());
}
#[test]
fn a_graph_is_read_by_class_field_and_reference() {
use Field::*;
let mut bytes = stream(|w| {
w.object(
1,
"Root",
&[("count", Int(-7)), ("big", Long(1 << 40)), ("flag", Bool(true)), ("byte", Byte(200)), ("child", Ref(2)), ("text", Ref(3))],
);
w.object(2, "Child", &[("parent", Ref(1))]);
w.0.push(6);
w.int(3);
w.string("héllo");
w.0.push(11);
});
let len = bytes.len();
bytes.extend_from_slice(b"pixels follow");
let (graph, used) = nrbf::parse(&bytes, GO).unwrap();
assert_eq!(used, len, "what follows the stream is left for the caller");
let root = &graph.root;
assert_eq!(graph.class_name(root).unwrap(), "Root");
assert_eq!(graph.integer(graph.field(root, "count").unwrap()), Ok(-7));
assert_eq!(graph.integer(graph.field(root, "big").unwrap()), Ok(1 << 40));
assert_eq!(graph.integer(graph.field(root, "byte").unwrap()), Ok(200));
assert_eq!(graph.boolean(graph.field(root, "flag").unwrap()), Ok(true));
assert_eq!(graph.string(graph.field(root, "text").unwrap()), Ok("héllo"));
let child = graph.field(root, "child").unwrap();
assert_eq!(graph.class_name(graph.field(child, "parent").unwrap()).unwrap(), "Root", "a cycle through objects is fine");
assert!(graph.optional_field(root, "missing").unwrap().is_none() && graph.field(root, "missing").is_err());
assert!(graph.integer(graph.field(root, "flag").unwrap()).is_err() && graph.string(root).is_err() && graph.array(root).is_err());
assert_eq!(graph.objects_of_class("Child").map(|(id, _)| id).collect::<Vec<_>>(), [2]);
match graph.resolve(root).unwrap() {
Value::Object { class, members } => assert_eq!((class.name(), class.member_names().len(), members.len()), ("Root", 6, 6)),
other => panic!("{other:?}"),
}
assert_eq!(nrbf::parse(&bytes, &|| false).err(), Some(Error::Cancelled));
}
#[test]
fn truncated_uncompressed_pixels_are_rejected_before_allocating() {
// A raw (version 1) block of 256 MiB chunks that declares 1 GiB of pixels but holds a few bytes.
let mut reader = Reader { bytes: &[1, 0x10, 0, 0, 0, 0, 0, 0, 0], pos: 0 };
assert_eq!(pixels(&mut reader, 1 << 30, GO).err(), Some(Error::Invalid("uncompressed pixel data is truncated".into())));
}
/// The fixture with `edit` applied to the serialized text: same-length replacements only.
fn edited(from: &[u8], to: &[u8]) -> Vec<u8> {
assert_eq!(from.len(), to.len());
let mut bytes = fixture(&[0, 1], false, false);
let at = bytes.windows(from.len()).position(|w| w == from).unwrap_or_else(|| panic!("{from:?} not in the fixture"));
bytes[at..at + from.len()].copy_from_slice(to);
bytes
}
#[test]
fn documents_this_reader_does_not_cover_are_refused() {
let cases: [(&str, &[u8], &[u8]); 6] = [
("root object is not a Paint.NET document", b"PaintDotNet.Document", b"PaintDotNet.Documenx"),
("only PDN3 bitmap layers are supported", b"PaintDotNet.BitmapLayer\x05", b"PaintDotNet.VectorLayer\x05"),
("invalid bitmap surface", b"PaintDotNet.Surface", b"PaintDotNet.Surfacf"),
("unsupported non-deferred or parented pixel memory", b"\x18\0\0\0\0\0\0\0\0\x01", b"\x18\0\0\0\0\0\0\0\x01\x01"),
("pixel memory length does not match the surface", b"\x18\0\0\0\0\0\0\0\0\x01", b"\x19\0\0\0\0\0\0\0\0\x01"),
("surface stride is too short for BGRA32 pixels", b"\x02\0\0\0\x02\0\0\0\x0c\0\0\0", b"\x02\0\0\0\x02\0\0\0\x07\0\0\0"),
];
for (why, from, to) in cases {
assert_eq!(Document::parse(&edited(from, to), GO).err(), Some(Error::Invalid(why.into())));
}
// The canvas: 2 × 2 in the fixture's Document object, right after its class header.
let canvas = |w: i32, h: i32| {
let mut bytes = fixture(&[0], false, false);
let at = bytes.windows(8).position(|w| w == [2, 0, 0, 0, 2, 0, 0, 0]).unwrap();
bytes[at..at + 4].copy_from_slice(&w.to_le_bytes());
bytes[at + 4..at + 8].copy_from_slice(&h.to_le_bytes());
Document::parse(&bytes, GO).err()
};
let limit = Some(Error::Invalid("canvas is empty or exceeds the 1 GiB pixel limit".into()));
assert_eq!(canvas(0, 2), limit);
assert_eq!(canvas(16385, 16384), limit);
assert_eq!(canvas(-1, 2), Some(Error::Invalid("invalid width".into())));
assert_eq!(
canvas(16384, 16384),
Some(Error::Invalid("layer dimensions do not match the canvas".into())),
"1 GiB exactly passes the canvas check"
);
}
#[test]
fn layer_counts_and_names_are_bounded() {
assert!(Document::parse(&fixture(&[], false, false), GO).is_err(), "no layers");
// 1025 layers: over the limit. The fixture's ids step by ten, so this stays far below i32.
let modes = vec![0; MAX_LAYERS + 1];
assert_eq!(Document::parse(&fixture(&modes, false, false), GO).err(), Some(Error::Invalid("document must have 1..1024 bitmap layers".into())));
assert_eq!(Document::parse(&fixture(&modes[..MAX_LAYERS], false, false), GO).map(|d| d.layers.len()), Ok(MAX_LAYERS));
}