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.
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 19:30:55 -05:00
1 parent ed16993b3b
commit fa26a2a4c6
8 files changed
+1360

No files matched your search

Generated
+9
View File
@@ -1409,6 +1409,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "craft-pdn"
version = "0.1.0"
dependencies = [
"flate2",
"proptest",
"thiserror 2.0.21",
]
[[package]]
name = "craft-penpot"
version = "0.1.0"
+1
View File
@@ -17,6 +17,7 @@ craft-aep = { path = "crates/aep" }
craft-affinity = { path = "crates/affinity" }
craft-ai-core = { path = "crates/ai-core" }
craft-pdf = { path = "crates/pdf", version = "=0.1.0" }
craft-pdn = { path = "crates/pdn" }
craft-plate-test = { path = "crates/plate-test" }
krilla = { version = "=0.8.2", default-features = false, features = ["raster-images"] }
hayro = { version = "=0.7.1", default-features = false }
+1
View File
@@ -34,6 +34,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
- [`craft-ai-core`](crates/ai-core/README.md): the data model and contract for AI in the Craft Apps (connections, models, tools, requests, jobs, consent, history, route spec). Pure, no I/O, wasm-safe. Contract in [docs/ai-shared-layer.md](docs/ai-shared-layer.md).
- [`craft-pdf`](crates/pdf/README.md): independently optional PDF parsing, interpretation, rasterization and guarded authoring. Apps retain layout, fonts, editing and export policy; writer-only consumers do not enable the shared reader.
- [`craft-pdn`](crates/pdn/README.md): Paint.NET documents (`.pdn`, PDN3), read: the layer list, blend modes and chunked gzip pixel blocks, with a data-only MS-NRBF (.NET binary serialization) reader. From PhotoCraft.
- [`craft-aaf`](crates/aaf/README.md): clean-room AAF (Edit Protocol, structured storage) and OMF Interchange 2.0 (Bento) readers and writers over one mob-style composition model with exact times, clip gain, markers, nested compositions and embedded PCM essence. From FilmCraft.
- [`craft-assuan`](crates/assuan/README.md): the Assuan protocol and gpg-agent's operations (key lookup, signing a digest, decrypting a session key) over any `Read + Write` stream, with a bounded canonical S-expression reader. No sockets; everything the agent sends is capped.
- [`craft-affinity`](crates/affinity/README.md): bounded reader for native Affinity documents (`.af`, `.afdesign`, `.afphoto`, `.afpub`): archive, object stream and a neutral document model with layers, artboards, vectors, text, tables, images and masks. Read only; mapping onto an app's document stays in the app.
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "craft-pdn"
description = "Paint.NET documents (.pdn, PDN3): the header, the .NET binary serialization graph (MS-NRBF, data only), the layer list and the chunked gzip pixel blocks, read with bounded sizes."
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
thiserror = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }
+33
View File
@@ -0,0 +1,33 @@
# craft-pdn
Paint.NET documents (`.pdn`), read. Only the PDN3 layout (Paint.NET 3 and 4): the `PDN3`
signature, an XML header (skipped), a .NET binary serialization stream holding a
`PaintDotNet.Document`, then one block of pixels per surface in numbered chunks, each a gzip
stream or raw bytes. The layout follows the reader of the pypdn project (MIT); the serialization
records follow Microsoft's public MS-NRBF specification.
- `is_pdn`: the signature.
- `Document::parse`: canvas size, bitmap layers (name, visibility, opacity 0 to 255, Paint.NET's
own `BlendMode`, from the current `blendMode` field or the older `UserBlendOps` class names)
and which surface each layer shows. Every allocation the pixels need is checked first.
- `Document::read_surfaces`: decodes the pixel blocks in file order, one `Surface` (rows of
B, G, R, A bytes, straight alpha) at a time, with a `keep_going` callback.
- `nrbf`: the data-only MS-NRBF reader on its own: a `Graph` of `Value`s with references kept as
ids. No .NET type is instantiated or run.
Paint.NET's Color Burn and Color Dodge are its own formulas; what an application maps them to is
its own choice. There is no writer.
## Limits
| Limit | Value |
| --- | --- |
| Canvas, and all pixel blocks together | 1 GiB (`MAX_PIXEL_BYTES`) |
| Layers | 1 to 1024 (`MAX_LAYERS`) |
| A layer name; all names | 64 KiB (`MAX_NAME_BYTES`); 4 MiB (`MAX_TOTAL_NAME_BYTES`) |
| Pixel chunks in one block | 1,000,000 |
| NRBF records, items and members | 1,000,000 (`nrbf::MAX_ITEMS`) |
| NRBF string bytes | 64 MiB (`nrbf::MAX_STRINGS`) |
| NRBF nesting and reference chains | 64 (`nrbf::MAX_DEPTH`) |
A raw pixel block shorter than it declares is refused before its buffer is allocated.
+381
View File
@@ -0,0 +1,381 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
//! Paint.NET documents (`.pdn`), read. Only the PDN3 layout: the `PDN3` signature, an XML
//! header (skipped), a .NET binary serialization stream ([`nrbf`]) holding a
//! `PaintDotNet.Document` with its bitmap layers, then one block of pixels per surface, in
//! numbered chunks that are each a gzip stream or raw bytes.
//!
//! The layout follows the reader of the pypdn project (MIT); the serialization records follow
//! Microsoft's public MS-NRBF specification.
//!
//! - [`Document::parse`] reads the structure: canvas size, [`Layer`]s with their name,
//! visibility, opacity and [`BlendMode`], and which pixel block each layer shows. No pixel is
//! decoded, and every allocation the pixels will need is checked first.
//! - [`Document::read_surfaces`] then decodes the pixel blocks in file order, handing each
//! [`Surface`] (rows of B, G, R, A bytes) to a callback.
pub mod nrbf;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::io::Read;
use nrbf::{Graph, Reader, Value};
/// Why a Paint.NET document was not read.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// The file is not a PDN3 document, is damaged, uses something this crate does not read, or
/// exceeds a limit.
#[error("invalid Paint.NET document: {0}")]
Invalid(String),
/// The caller's `keep_going` returned `false`.
#[error("cancelled")]
Cancelled,
}
pub type Result<T> = std::result::Result<T, Error>;
fn invalid(message: impl Into<String>) -> Error {
Error::Invalid(message.into())
}
fn check(keep_going: &dyn Fn() -> bool) -> Result<()> {
if keep_going() { Ok(()) } else { Err(Error::Cancelled) }
}
/// The canvas, and all pixel blocks together, may hold at most this many bytes.
pub const MAX_PIXEL_BYTES: usize = 1 << 30;
/// Layers in one document.
pub const MAX_LAYERS: usize = 1024;
/// Longest layer name, in bytes.
pub const MAX_NAME_BYTES: usize = 64 * 1024;
/// Most bytes of layer names in one document. The graph holds a shared string once; every layer
/// that refers to it gets its own copy.
pub const MAX_TOTAL_NAME_BYTES: usize = 4 * 1024 * 1024;
/// `true` for a PDN3 document (by its signature).
pub fn is_pdn(bytes: &[u8]) -> bool {
bytes.starts_with(b"PDN3")
}
/// A layer's blend mode: Paint.NET's `LayerBlendMode`, in its numeric order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BlendMode {
Normal,
Multiply,
Additive,
/// Paint.NET's own formula, not Photoshop's Color Burn.
ColorBurn,
/// Paint.NET's own formula, not Photoshop's Color Dodge.
ColorDodge,
Reflect,
Glow,
Overlay,
Difference,
Negation,
Lighten,
Darken,
Screen,
Xor,
}
impl BlendMode {
/// Every mode, indexed by its `LayerBlendMode` value.
pub const ALL: [BlendMode; 14] = [
BlendMode::Normal,
BlendMode::Multiply,
BlendMode::Additive,
BlendMode::ColorBurn,
BlendMode::ColorDodge,
BlendMode::Reflect,
BlendMode::Glow,
BlendMode::Overlay,
BlendMode::Difference,
BlendMode::Negation,
BlendMode::Lighten,
BlendMode::Darken,
BlendMode::Screen,
BlendMode::Xor,
];
/// The name older files use in `PaintDotNet.UserBlendOps+<name>BlendOp`.
pub fn name(self) -> &'static str {
match self {
BlendMode::Normal => "Normal",
BlendMode::Multiply => "Multiply",
BlendMode::Additive => "Additive",
BlendMode::ColorBurn => "ColorBurn",
BlendMode::ColorDodge => "ColorDodge",
BlendMode::Reflect => "Reflect",
BlendMode::Glow => "Glow",
BlendMode::Overlay => "Overlay",
BlendMode::Difference => "Difference",
BlendMode::Negation => "Negation",
BlendMode::Lighten => "Lighten",
BlendMode::Darken => "Darken",
BlendMode::Screen => "Screen",
BlendMode::Xor => "Xor",
}
}
}
/// A bitmap layer, bottom first in [`Document::layers`]. Every layer is the size of the canvas.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Layer {
pub name: String,
pub visible: bool,
/// 0 to 255.
pub opacity: u8,
pub blend: BlendMode,
/// Which surface [`Document::read_surfaces`] hands out holds this layer's pixels. Layers may
/// share one.
pub surface: usize,
}
/// One decoded pixel block: `height` rows of `stride` bytes, each starting with `width` pixels
/// of B, G, R, A bytes (straight alpha).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Surface {
pub width: usize,
pub stride: usize,
pub data: Vec<u8>,
}
impl Surface {
/// The rows, top first, each cut to its `width × 4` pixel bytes (B, G, R, A).
pub fn rows(&self) -> impl Iterator<Item = &[u8]> {
self.data.chunks_exact(self.stride.max(1)).filter_map(|row| row.get(..self.width * 4))
}
}
struct Block {
length: usize,
stride: usize,
}
/// The structure of a PDN3 document. Parse first, then read the pixels.
pub struct Document<'a> {
/// Canvas size in pixels.
pub width: usize,
pub height: usize,
/// The layers, bottom first.
pub layers: Vec<Layer>,
/// The document carries metadata items (`userMetadataItems`); they are not read.
pub has_user_metadata: bool,
bytes: &'a [u8],
/// Where the pixel blocks start.
pixels_at: usize,
blocks: Vec<Block>,
}
fn count(graph: &Graph, object: &Value, field: &str) -> Result<usize> {
usize::try_from(graph.integer(graph.field(object, field)?)?).map_err(|_| invalid(format!("invalid {field}")))
}
fn blend(graph: &Graph, layer: &Value, props: &Value) -> Result<BlendMode> {
if let Some(mode) = graph.optional_field(props, "blendMode")? {
let mode = match graph.resolve(mode)? {
Value::Int(n) => *n,
_ => graph.integer(graph.field(mode, "value__")?)?,
};
let mode = usize::try_from(mode).map_err(|_| invalid("invalid blend mode"))?;
return BlendMode::ALL.get(mode).copied().ok_or_else(|| invalid(format!("unsupported blend mode {mode}")));
}
let props = graph.field(layer, "properties")?;
let mode = graph.class_name(graph.field(props, "blendOp")?)?;
let mode = mode
.strip_prefix("PaintDotNet.UserBlendOps+")
.and_then(|s| s.strip_suffix("BlendOp"))
.ok_or_else(|| invalid(format!("unsupported blend operation {mode}")))?;
BlendMode::ALL.iter().copied().find(|m| m.name() == mode).ok_or_else(|| invalid(format!("unsupported blend operation {mode}")))
}
impl<'a> Document<'a> {
/// Reads the header and the serialized document. `keep_going` is asked throughout; `false`
/// ends the read with [`Error::Cancelled`].
pub fn parse(bytes: &'a [u8], keep_going: &dyn Fn() -> bool) -> Result<Document<'a>> {
let mut reader = Reader { bytes, pos: 0 };
if reader.take(4)? != b"PDN3" {
return Err(invalid("unsupported document signature (expected PDN3)"));
}
let header_size = match reader.take(3)? {
[a, b, c] => u32::from_le_bytes([*a, *b, *c, 0]) as usize,
_ => return Err(invalid("invalid header")),
};
reader.take(header_size)?;
if reader.take(2)? != [0, 1] {
return Err(invalid("unsupported PDN3 serialization version"));
}
let graph = nrbf::parse_from(&mut reader, keep_going)?;
let root = &graph.root;
if graph.class_name(root)? != "PaintDotNet.Document" {
return Err(invalid("root object is not a Paint.NET document"));
}
let width = count(&graph, root, "width")?;
let height = count(&graph, root, "height")?;
let row_bytes = width.checked_mul(4).ok_or_else(|| invalid("canvas size overflow"))?;
let canvas_bytes = row_bytes.checked_mul(height).ok_or_else(|| invalid("canvas size overflow"))?;
if width == 0 || height == 0 || width > i32::MAX as usize || height > i32::MAX as usize || canvas_bytes > MAX_PIXEL_BYTES {
return Err(invalid("canvas is empty or exceeds the 1 GiB pixel limit"));
}
let list = graph.field(root, "layers")?;
let items = graph.array(graph.field(list, "ArrayList+_items")?)?;
let len = count(&graph, list, "ArrayList+_size")?;
if len == 0 || len > MAX_LAYERS {
return Err(invalid("document must have 1..1024 bitmap layers"));
}
let items = items.get(..len).ok_or_else(|| invalid("layer list count exceeds its array"))?;
// Memory block id to the stride of the surfaces that use it.
let mut layouts = HashMap::new();
let mut layers = Vec::with_capacity(len);
let mut name_bytes = 0usize;
for layer in items {
check(keep_going)?;
if graph.class_name(layer)? != "PaintDotNet.BitmapLayer" {
return Err(invalid("only PDN3 bitmap layers are supported"));
}
if count(&graph, layer, "Layer+width")? != width || count(&graph, layer, "Layer+height")? != height {
return Err(invalid("layer dimensions do not match the canvas"));
}
let surface = graph.field(layer, "surface")?;
if graph.class_name(surface)? != "PaintDotNet.Surface"
|| count(&graph, surface, "width")? != width
|| count(&graph, surface, "height")? != height
{
return Err(invalid("invalid bitmap surface"));
}
let stride = count(&graph, surface, "stride")?;
if stride < row_bytes {
return Err(invalid("surface stride is too short for BGRA32 pixels"));
}
let Value::Ref(id) = graph.field(surface, "scan0")? else { return Err(invalid("missing pixel memory reference")) };
if let Some(old) = layouts.insert(*id, stride)
&& old != stride
{
return Err(invalid("shared pixel memory has inconsistent strides"));
}
let props = graph.field(layer, "Layer+properties")?;
let opacity = u8::try_from(count(&graph, props, "opacity")?).map_err(|_| invalid("opacity exceeds 255"))?;
let name = graph.string(graph.field(props, "name")?)?;
if name.len() > MAX_NAME_BYTES {
return Err(invalid("layer name is too long"));
}
name_bytes = name_bytes.saturating_add(name.len());
if name_bytes > MAX_TOTAL_NAME_BYTES {
return Err(invalid("layer names exceed the total metadata budget"));
}
let visible = graph.boolean(graph.field(props, "visible")?)?;
layers.push((*id, Layer { name: name.to_string(), visible, opacity, blend: blend(&graph, layer, props)?, surface: 0 }));
}
// Validate every allocation before any block is decoded.
let mut blocks = Vec::new();
let mut index_of = HashMap::new();
let mut total = 0usize;
for (id, block) in graph.objects_of_class("PaintDotNet.MemoryBlock") {
let length = count(&graph, block, "length64")?;
let stride = *layouts.get(&id).ok_or_else(|| invalid("unreferenced pixel memory block"))?;
if length != stride.checked_mul(height).ok_or_else(|| invalid("pixel length overflow"))? {
return Err(invalid("pixel memory length does not match the surface"));
}
if graph.boolean(graph.field(block, "hasParent")?)? || !graph.boolean(graph.field(block, "deferred")?)? {
return Err(invalid("unsupported non-deferred or parented pixel memory"));
}
total = total.checked_add(length).ok_or_else(|| invalid("pixel length overflow"))?;
if total > MAX_PIXEL_BYTES {
return Err(invalid("decoded pixels exceed the 1 GiB limit"));
}
index_of.insert(id, blocks.len());
blocks.push(Block { length, stride });
}
let layers = layers
.into_iter()
.map(|(id, layer)| Ok(Layer { surface: *index_of.get(&id).ok_or_else(|| invalid("missing pixel memory block"))?, ..layer }))
.collect::<Result<Vec<_>>>()?;
let has_user_metadata = match graph.optional_field(root, "userMetadataItems")? {
Some(meta) if !matches!(graph.resolve(meta)?, Value::Null) => !graph.array(meta)?.is_empty(),
_ => false,
};
Ok(Document { width, height, layers, has_user_metadata, bytes, pixels_at: reader.pos, blocks })
}
/// How many surfaces [`Self::read_surfaces`] hands out.
pub fn surface_count(&self) -> usize {
self.blocks.len()
}
/// Bytes all surfaces take once decoded: at most [`MAX_PIXEL_BYTES`].
pub fn decoded_bytes(&self) -> usize {
self.blocks.iter().map(|b| b.length).sum()
}
/// Decodes the pixel blocks in file order and hands each to `each` with its index (what
/// [`Layer::surface`] names). Only one decoded surface is held at a time unless `each` keeps
/// them. Data after the last block is an error.
pub fn read_surfaces(&self, keep_going: &dyn Fn() -> bool, each: &mut dyn FnMut(usize, Surface)) -> Result<()> {
let mut reader = Reader { bytes: self.bytes, pos: self.pixels_at };
for (i, block) in self.blocks.iter().enumerate() {
let data = pixels(&mut reader, block.length, keep_going)?;
each(i, Surface { width: self.width, stride: block.stride, data });
}
if reader.pos != self.bytes.len() {
return Err(invalid("unexpected trailing data"));
}
Ok(())
}
}
/// Reads a memory block's numbered chunks in their on-disk order; each chunk has its own gzip
/// stream and checksum. Inflates directly into the bounded destination, checking its exact size.
fn pixels(reader: &mut Reader<'_>, length: usize, keep_going: &dyn Fn() -> bool) -> Result<Vec<u8>> {
let version = reader.byte()?;
if version > 1 {
return Err(invalid(format!("unsupported pixel compression {version}")));
}
let chunk_size = reader.be_u32()?;
if chunk_size == 0 {
return Err(invalid("zero pixel chunk size"));
}
let chunks = length.div_ceil(chunk_size);
if chunks > 1_000_000 {
return Err(invalid("too many pixel chunks"));
}
// Uncompressed chunks hold every pixel byte: a file shorter than that is truncated, and is
// rejected before the buffer is allocated.
if version == 1 && reader.bytes.len().saturating_sub(reader.pos) < length {
return Err(invalid("uncompressed pixel data is truncated"));
}
let mut seen = vec![false; chunks];
let mut data = vec![0; length];
for _ in 0..chunks {
check(keep_going)?;
let index = reader.be_u32()?;
let found = seen.get_mut(index).ok_or_else(|| invalid("pixel chunk index out of bounds"))?;
if *found {
return Err(invalid("duplicate pixel chunk"));
}
*found = true;
let size = reader.be_u32()?;
let raw = reader.take(size)?;
let start = index.checked_mul(chunk_size).ok_or_else(|| invalid("pixel offset overflow"))?;
let end = start.checked_add(chunk_size.min(length.saturating_sub(start))).ok_or_else(|| invalid("pixel offset overflow"))?;
let dst = data.get_mut(start..end).ok_or_else(|| invalid("invalid pixel chunk range"))?;
if version == 0 {
let mut decoder = flate2::bufread::GzDecoder::new(raw);
for band in dst.chunks_mut(1 << 20) {
check(keep_going)?;
decoder.read_exact(band).map_err(|e| invalid(format!("invalid gzip pixel chunk: {e}")))?;
}
let mut extra = [0];
if decoder.read(&mut extra).map_err(|e| invalid(format!("invalid gzip pixel checksum: {e}")))? != 0 || !decoder.into_inner().is_empty() {
return Err(invalid("pixel chunk has excess data"));
}
} else {
if raw.len() != dst.len() {
return Err(invalid("incorrect uncompressed pixel chunk size"));
}
dst.copy_from_slice(raw);
}
}
Ok(data)
}
+473
View File
@@ -0,0 +1,473 @@
//! The data-only subset of the .NET Remoting Binary Format (MS-NRBF) that PDN3 uses, from
//! Microsoft's public specification
//! (<https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/>). The stream becomes
//! a [`Graph`] of [`Value`]s; references stay as ids and no .NET type is instantiated or run.
//!
//! Limits: [`MAX_ITEMS`] records and members, [`MAX_STRINGS`] bytes of strings, [`MAX_DEPTH`]
//! levels of nesting and of reference chains.
use std::collections::HashMap;
use std::sync::Arc;
use crate::{Error, Result, invalid};
/// Most records, array items and class members in one stream.
pub const MAX_ITEMS: usize = 1_000_000;
/// Most bytes of strings in one stream.
pub const MAX_STRINGS: usize = 64 << 20;
/// Deepest nesting of records, and longest chain of references [`Graph::resolve`] follows.
pub const MAX_DEPTH: usize = 64;
pub(crate) struct Reader<'a> {
pub bytes: &'a [u8],
pub pos: usize,
}
impl<'a> Reader<'a> {
pub fn take(&mut self, len: usize) -> Result<&'a [u8]> {
let end = self.pos.checked_add(len).ok_or_else(|| invalid("length overflow"))?;
let b = self.bytes.get(self.pos..end).ok_or_else(|| invalid("truncated document"))?;
self.pos = end;
Ok(b)
}
pub fn byte(&mut self) -> Result<u8> {
Ok(self.take(1)?.first().copied().unwrap_or_default())
}
pub fn le_i32(&mut self) -> Result<i32> {
Ok(i32::from_le_bytes(self.take(4)?.try_into().map_err(|_| invalid("invalid integer"))?))
}
pub fn be_u32(&mut self) -> Result<usize> {
usize::try_from(u32::from_be_bytes(self.take(4)?.try_into().map_err(|_| invalid("invalid integer"))?)).map_err(|_| invalid("length overflow"))
}
fn count(&mut self) -> Result<usize> {
let n = usize::try_from(self.le_i32()?).map_err(|_| invalid("negative count"))?;
if n > MAX_ITEMS {
return Err(invalid("serialized array or class is too large"));
}
Ok(n)
}
}
/// A serialized class: its name and its member names, in order.
#[derive(Debug)]
pub struct Class {
name: String,
names: Vec<String>,
primitives: Vec<Option<u8>>,
}
impl Class {
/// The class name as serialized (`PaintDotNet.Document`).
pub fn name(&self) -> &str {
&self.name
}
/// The member names, in the order [`Value::Object`] holds their values.
pub fn member_names(&self) -> &[String] {
&self.names
}
}
/// A value of the graph.
#[derive(Debug)]
pub enum Value {
/// A null reference, and every primitive this reader skips (doubles, decimals, time spans).
Null,
Bool(bool),
/// Every integer type, widened. An unsigned 64-bit value and a date keep their bit pattern.
Int(i64),
/// A string or a single character.
String(String),
/// A reference to the object with this id; [`Graph::resolve`] follows it.
Ref(i32),
/// An array's items. A byte array's content is not kept: its value stays [`Value::Null`].
Array(Vec<Value>),
Object {
class: Arc<Class>,
members: Vec<Value>,
},
}
/// The objects of a stream, by id.
pub struct Graph {
/// A reference to the root object.
pub root: Value,
objects: HashMap<i32, Value>,
order: Vec<i32>,
}
impl Graph {
/// Every object of the class `name` with its id, in stream order.
pub fn objects_of_class<'a>(&'a self, name: &'a str) -> impl Iterator<Item = (i32, &'a Value)> {
self.order
.iter()
.filter_map(|id| self.objects.get(id).map(|v| (*id, v)))
.filter(move |(_, v)| matches!(v, Value::Object { class, .. } if class.name == name))
}
/// Follows references to the value they name.
pub fn resolve<'a>(&'a self, mut value: &'a Value) -> Result<&'a Value> {
for _ in 0..MAX_DEPTH {
match value {
Value::Ref(id) => value = self.objects.get(id).ok_or_else(|| invalid("missing object reference"))?,
_ => return Ok(value),
}
}
Err(invalid("cyclic or excessively deep object reference"))
}
/// The class name of an object.
pub fn class_name<'a>(&'a self, value: &'a Value) -> Result<&'a str> {
match self.resolve(value)? {
Value::Object { class, .. } => Ok(&class.name),
_ => Err(invalid("expected a serialized object")),
}
}
/// A member of an object by name; an error when the class has none of that name.
pub fn field<'a>(&'a self, value: &'a Value, name: &str) -> Result<&'a Value> {
self.optional_field(value, name)?.ok_or_else(|| invalid(format!("missing field {name}")))
}
/// A member of an object by name; `None` when the class has none of that name.
pub fn optional_field<'a>(&'a self, value: &'a Value, name: &str) -> Result<Option<&'a Value>> {
match self.resolve(value)? {
Value::Object { class, members } => Ok(class.names.iter().position(|n| n == name).and_then(|i| members.get(i))),
_ => Err(invalid("expected a serialized object")),
}
}
/// An integer value.
pub fn integer(&self, value: &Value) -> Result<i64> {
match self.resolve(value)? {
Value::Int(n) => Ok(*n),
_ => Err(invalid("expected an integer")),
}
}
/// A boolean value.
pub fn boolean(&self, value: &Value) -> Result<bool> {
match self.resolve(value)? {
Value::Bool(b) => Ok(*b),
_ => Err(invalid("expected a boolean")),
}
}
/// A string value.
pub fn string<'a>(&'a self, value: &'a Value) -> Result<&'a str> {
match self.resolve(value)? {
Value::String(s) => Ok(s),
_ => Err(invalid("expected a string")),
}
}
/// An array's items.
pub fn array<'a>(&'a self, value: &'a Value) -> Result<&'a [Value]> {
match self.resolve(value)? {
Value::Array(a) => Ok(a),
_ => Err(invalid("expected an array")),
}
}
}
struct Parser<'a, 'b> {
reader: &'b mut Reader<'a>,
keep_going: &'b dyn Fn() -> bool,
graph: Graph,
classes: HashMap<i32, Arc<Class>>,
items: usize,
strings: usize,
}
/// Reads a stream from its header record to its end record. Returns the graph and the number of
/// bytes the stream took, so whatever follows it can be read. `keep_going` is asked at every
/// record and array item; `false` ends the read with [`Error::Cancelled`].
pub fn parse(bytes: &[u8], keep_going: &dyn Fn() -> bool) -> Result<(Graph, usize)> {
let mut reader = Reader { bytes, pos: 0 };
let graph = parse_from(&mut reader, keep_going)?;
Ok((graph, reader.pos))
}
pub(crate) fn parse_from(reader: &mut Reader<'_>, keep_going: &dyn Fn() -> bool) -> Result<Graph> {
if reader.byte()? != 0 {
return Err(invalid("missing NRBF stream header"));
}
let root = reader.le_i32()?;
let _header = reader.le_i32()?;
if root == 0 || reader.le_i32()? != 1 || reader.le_i32()? != 0 {
return Err(invalid("unsupported NRBF stream version"));
}
let mut parser = Parser {
reader,
keep_going,
graph: Graph { root: Value::Ref(root), objects: HashMap::new(), order: Vec::new() },
classes: HashMap::new(),
items: 0,
strings: 0,
};
loop {
let tag = parser.reader.byte()?;
if tag == 11 {
break;
}
parser.record(tag, 0)?;
}
parser.graph.resolve(&parser.graph.root)?;
Ok(parser.graph)
}
impl Parser<'_, '_> {
fn check(&self) -> Result<()> {
if (self.keep_going)() { Ok(()) } else { Err(Error::Cancelled) }
}
fn budget(&mut self, n: usize) -> Result<()> {
self.items = self.items.checked_add(n).ok_or_else(|| invalid("object count overflow"))?;
if self.items > MAX_ITEMS {
return Err(invalid("too many serialized objects or members"));
}
Ok(())
}
fn string(&mut self) -> Result<String> {
let mut len = 0usize;
for shift in (0..35).step_by(7) {
let b = self.reader.byte()?;
if shift == 28 && b > 7 {
return Err(invalid("invalid string length"));
}
len |= usize::from(b & 127) << shift;
if b & 128 == 0 {
self.strings = self.strings.checked_add(len).ok_or_else(|| invalid("string length overflow"))?;
if self.strings > MAX_STRINGS {
return Err(invalid("serialized strings are too large"));
}
return String::from_utf8(self.reader.take(len)?.to_vec()).map_err(|_| invalid("invalid UTF-8 string"));
}
}
Err(invalid("invalid string length"))
}
fn primitive(&mut self, tag: u8) -> Result<Value> {
macro_rules! number {
($ty:ty, $variant:ident) => {
Value::$variant(
<$ty>::from_le_bytes(self.reader.take(size_of::<$ty>())?.try_into().map_err(|_| invalid("invalid primitive"))?).into(),
)
};
}
Ok(match tag {
1 => match self.reader.byte()? {
0 => Value::Bool(false),
1 => Value::Bool(true),
_ => return Err(invalid("invalid boolean")),
},
2 => Value::Int(i64::from(self.reader.byte()?)),
3 => {
let first = self.reader.byte()?;
let len = match first {
0..=0x7f => 1,
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
_ => return Err(invalid("invalid character")),
};
let mut b = vec![first];
b.extend_from_slice(self.reader.take(len - 1)?);
Value::String(String::from_utf8(b).map_err(|_| invalid("invalid character"))?)
}
5 | 18 => Value::String(self.string()?),
6 => {
self.reader.take(8)?;
Value::Null
}
7 => number!(i16, Int),
8 => number!(i32, Int),
9 | 12 => number!(i64, Int),
10 => Value::Int(i64::from(self.reader.byte()? as i8)),
11 => {
self.reader.take(4)?;
Value::Null
}
13 | 16 => {
let n = u64::from_le_bytes(self.reader.take(8)?.try_into().map_err(|_| invalid("invalid integer"))?);
// Metadata dates may use the high bits; their bit pattern stays intact.
Value::Int(n as i64)
}
14 => number!(u16, Int),
15 => number!(u32, Int),
17 => Value::Null,
_ => return Err(invalid(format!("unsupported primitive type {tag}"))),
})
}
fn type_info(&mut self, tag: u8) -> Result<Option<u8>> {
match tag {
0 => Ok(Some(self.reader.byte()?)),
1 | 2 | 5 | 6 => Ok(None),
3 => {
self.string()?;
Ok(None)
}
4 => {
self.string()?;
self.reader.le_i32()?;
Ok(None)
}
7 => {
self.reader.byte()?;
Ok(None)
}
_ => Err(invalid(format!("unsupported binary type {tag}"))),
}
}
fn reserve(&mut self, id: i32) -> Result<()> {
if id == 0 || self.graph.objects.insert(id, Value::Null).is_some() {
return Err(invalid("duplicate or zero object id"));
}
self.graph.order.push(id);
Ok(())
}
fn value(&mut self, depth: usize) -> Result<Value> {
loop {
let tag = self.reader.byte()?;
if tag == 12 {
self.record(tag, depth)?;
} else {
return self.record(tag, depth);
}
}
}
fn object(&mut self, id: i32, class: Arc<Class>, depth: usize) -> Result<Value> {
self.reserve(id)?;
self.budget(class.names.len())?;
let mut members = Vec::with_capacity(class.names.len());
for primitive in &class.primitives {
members.push(match primitive {
Some(p) => self.primitive(*p)?,
None => self.value(depth + 1)?,
});
}
self.graph.objects.insert(id, Value::Object { class, members });
Ok(Value::Ref(id))
}
fn array(&mut self, id: i32, len: usize, primitive: Option<u8>, depth: usize) -> Result<Value> {
self.reserve(id)?;
self.budget(len)?;
if primitive == Some(2) {
self.reader.take(len)?;
return Ok(Value::Ref(id));
}
let mut values = Vec::with_capacity(len);
while values.len() < len {
self.check()?;
if let Some(p) = primitive {
values.push(self.primitive(p)?);
continue;
}
let tag = self.reader.byte()?;
if matches!(tag, 13 | 14) {
let count = if tag == 13 { usize::from(self.reader.byte()?) } else { self.reader.count()? };
if count == 0 || count > len - values.len() {
return Err(invalid("invalid array null run"));
}
values.resize_with(values.len() + count, || Value::Null);
} else if tag == 12 {
self.record(tag, depth + 1)?;
} else {
values.push(self.record(tag, depth + 1)?);
}
}
self.graph.objects.insert(id, Value::Array(values));
Ok(Value::Ref(id))
}
fn record(&mut self, tag: u8, depth: usize) -> Result<Value> {
self.check()?;
self.budget(1)?;
if depth > MAX_DEPTH {
return Err(invalid("serialized objects nested too deeply"));
}
match tag {
1 => {
let id = self.reader.le_i32()?;
let metadata = self.reader.le_i32()?;
let class = self.classes.get(&metadata).cloned().ok_or_else(|| invalid("missing class metadata"))?;
self.object(id, class, depth)
}
2..=5 => {
let id = self.reader.le_i32()?;
let name = self.string()?;
let count = self.reader.count()?;
self.budget(count)?;
let names = (0..count).map(|_| self.string()).collect::<Result<Vec<_>>>()?;
let primitives = if tag >= 4 {
let types = self.reader.take(count)?.to_vec();
types.into_iter().map(|t| self.type_info(t)).collect::<Result<Vec<_>>>()?
} else {
vec![None; count]
};
if matches!(tag, 3 | 5) {
self.reader.le_i32()?;
}
let class = Arc::new(Class { name, names, primitives });
if self.classes.insert(id, class.clone()).is_some() {
return Err(invalid("duplicate class metadata"));
}
self.object(id, class, depth)
}
6 => {
let id = self.reader.le_i32()?;
self.reserve(id)?;
let value = Value::String(self.string()?);
self.graph.objects.insert(id, value);
Ok(Value::Ref(id))
}
7 => {
let id = self.reader.le_i32()?;
let kind = self.reader.byte()?;
let rank = self.reader.count()?;
if kind > 5 || rank == 0 || rank > 32 {
return Err(invalid("invalid array dimensions"));
}
let mut len = 1usize;
for _ in 0..rank {
len = len.checked_mul(self.reader.count()?).ok_or_else(|| invalid("array size overflow"))?;
}
if kind >= 3 {
for _ in 0..rank {
if self.reader.le_i32()? != 0 {
return Err(invalid("nonzero array lower bounds are unsupported"));
}
}
}
let tag = self.reader.byte()?;
let primitive = self.type_info(tag)?;
self.array(id, len, primitive, depth)
}
8 => {
let primitive = self.reader.byte()?;
self.primitive(primitive)
}
9 => Ok(Value::Ref(self.reader.le_i32()?)),
10 => Ok(Value::Null),
12 => {
self.reader.le_i32()?;
self.string()?;
Ok(Value::Null)
}
15..=17 => {
let id = self.reader.le_i32()?;
let len = self.reader.count()?;
let primitive = if tag == 15 { Some(self.reader.byte()?) } else { None };
self.array(id, len, primitive, depth)
}
_ => Err(invalid(format!("unsupported NRBF record {tag}"))),
}
}
}
+444
View File
@@ -0,0 +1,444 @@
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));
}