mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
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:
1 parent
ed16993b3b
commit
fa26a2a4c6
8 files changed
+1360
No files matched your search
Generated
+9
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user