M12: PRC reader crate: container, bit primitives, schema (#1297)

New standalone L0 crate pdfcraft-prc: the PRC container walk (file
header, file-structure descriptions, per-section zlib inflation under a
512 MiB ceiling), the bit-level primitives of the compressed sections,
and the schema interpreter a newer producer uses to describe fields it
added (step and nesting ceilings).

The Double prefix-code table is not in this commit: Double reads
return UnknownDoubleCode, and the tests that need the table are
ignored with that reason.

Adapted from pdfcer (MIT, same author), crates/pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76andClaude Opus 5.5 committed 2026-11-02 12:26:43 -05:00
1 parent 168a3cb91e
commit 83b230ae73
13 files changed
+1740

No files matched your search

Generated
+9
View File
@@ -4551,6 +4551,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "pdfcraft-prc"
version = "0.15.0"
dependencies = [
"flate2",
"proptest",
"thiserror 2.0.21",
]
[[package]]
name = "pdfcraft-preflight"
version = "0.17.0"
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "pdfcraft-prc"
description = "PRC 3D model reader (ISO 14739-1): container, bit-level primitives, schema"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
flate2.workspace = true
thiserror.workspace = true
[dev-dependencies]
proptest.workspace = true
+23
View File
@@ -0,0 +1,23 @@
# pdfcraft-prc
A reader for PRC (ISO 14739-1), the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries (ISO 32000-2 §13.6.3).
- **Layer:** L0, standalone. No dependency on any other PdfCraft crate.
- **Licence:** MIT OR Apache-2.0. Inflation through `flate2` with the pure-Rust `miniz_oxide` backend.
- `#![forbid(unsafe_code)]`. Never panics on input: every read is bounded and returns `PrcError`.
## API
```rust
PrcFile::parse(data) -> Result<PrcFile, PrcError> // header, file structures, model file
PrcFile::parse_with_limit(data, max_inflated) -> Result<PrcFile, PrcError>
FileStructure::section(SectionKind) -> &[u8] // inflated globals / tree / tessellation / geometry / extra geometry
FileStructure::schema() -> Result<Schema, PrcError> // the globals section's leading schema
Schema::read(&mut BitReader) / Schema::skip_added_fields(entity_type, &mut BitReader)
BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_vbn, nbits_then_unsigned, string, float_as_bytes, double
```
- `MAX_INFLATED_BYTES` (512 MiB) bounds every section together; `TooLarge` reports the limit hit.
- Schema programs (how a newer producer describes fields it added) run under a step ceiling (2^22) and a nesting ceiling (64).
- `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`).
- Not yet in the crate: the `Double` code table, so `BitReader::double` returns `UnknownDoubleCode`; the product tree and tessellation readers.
+98
View File
@@ -0,0 +1,98 @@
//! The prefix-code table behind the PRC `Double` [WD 11.17.2].
//!
//! The table has 2077 rows: 29 frequent doubles and one row per IEEE binary64 exponent
//! (0..=2047). Each row is a code of 1 to 22 bits and the value it decodes to.
//!
//! The rows are not in this crate yet: until they are, [`lookup`] finds no code, so every
//! `Double` read fails with `UnknownDoubleCode` and nothing else changes.
use std::collections::HashMap;
use std::sync::OnceLock;
/// Rows a complete table has.
const ROWS: usize = 2077;
/// The longest code, in bits.
pub(crate) const MAX_CODE_BITS: u32 = 22;
/// What a code decodes to.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(not(test), expect(dead_code, reason = "built only by the table's rows, which are not in the crate yet"))]
pub(crate) enum Entry {
/// A frequent value, returned with the sign bit applied.
Double(f64),
/// The upper 32 bits of a binary64 (exponent set, mantissa zero); the mantissa follows
/// in the stream.
Exponent(u32),
}
/// One row: code length, code, and what it decodes to.
type Row = (u32, u32, Entry);
/// The table's rows.
const TABLE: &[Row] = &[];
fn key(nbits: u32, code: u32) -> u32 {
(nbits << 24) | code
}
/// The rows keyed by `(nbits, code)`, or `None` when the table is incomplete or not prefix
/// free (then no `Double` decodes, rather than some decoding wrongly).
fn build(rows: &[Row]) -> Option<HashMap<u32, Entry>> {
let mut map = HashMap::with_capacity(rows.len());
for &(nbits, code, entry) in rows {
if nbits == 0 || nbits > MAX_CODE_BITS || code >> nbits != 0 || map.insert(key(nbits, code), entry).is_some() {
return None;
}
}
let prefix_free = map.keys().all(|&k| {
let (n, c) = (k >> 24, k & 0xff_ffff);
(1..n).all(|m| !map.contains_key(&key(m, c >> (n - m))))
});
(map.len() == ROWS && prefix_free).then_some(map)
}
fn table() -> Option<&'static HashMap<u32, Entry>> {
static MAP: OnceLock<Option<HashMap<u32, Entry>>> = OnceLock::new();
MAP.get_or_init(|| build(TABLE)).as_ref()
}
/// The entry for an `nbits`-long code, if one exists.
pub(crate) fn lookup(nbits: u32, code: u32) -> Option<Entry> {
table()?.get(&key(nbits, code)).copied()
}
/// The `(nbits, code)` of IEEE exponent `e`'s row, the test writer's half of [`lookup`].
#[cfg(test)]
pub(crate) fn exponent_code(e: u32) -> Option<(u32, u32)> {
table()?.iter().find(|(_, v)| **v == Entry::Exponent(e << 20)).map(|(&k, _)| (k >> 24, k & 0xff_ffff))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn an_incomplete_or_ambiguous_table_decodes_nothing() {
assert!(build(&[(2, 0b01, Entry::Double(0.0))]).is_none(), "one row is not the table");
let rows: Vec<Row> = (0..ROWS as u32).map(|i| (22, i, Entry::Exponent(i))).collect();
assert!(build(&rows).is_some(), "2077 distinct 22-bit codes are prefix free");
let mut ambiguous = rows.clone();
ambiguous[0] = (1, 0, Entry::Double(1.0)); // a prefix of every code starting 0
assert!(build(&ambiguous).is_none());
let mut too_long = rows;
too_long[0] = (2, 0b111, Entry::Double(1.0)); // a code wider than its length
assert!(build(&too_long).is_none());
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn the_table_is_complete() {
let t = table().unwrap();
assert_eq!(t.len(), ROWS);
assert_eq!(t.values().filter(|e| matches!(e, Entry::Double(_))).count(), 29);
assert!((0..2048).all(|e| exponent_code(e).is_some()));
assert_eq!(lookup(2, 0b01), Some(Entry::Double(0.0)));
}
}
+421
View File
@@ -0,0 +1,421 @@
//! The bit-level reader for PRC compressed sections [WD 10.3, 11.1-11.17].
//!
//! Values are consumed MSB-first from each inflated byte and straddle byte boundaries freely
//! [WD 10.3.1, 11.3]. Multi-byte integers are groups of eight bits, low group first. Two sign
//! conventions coexist: `Integer` is two's complement, the `*WithVariableBitNumber` types are
//! sign-magnitude.
use crate::PrcError;
use crate::acof::{self, Entry};
/// A cursor over one inflated section.
#[derive(Debug, Clone)]
pub struct BitReader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> BitReader<'a> {
/// A reader positioned at the first bit of `data`.
pub fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
/// Bits consumed so far.
pub fn position(&self) -> usize {
self.pos
}
/// Bits left before the end of the data.
pub fn remaining(&self) -> usize {
self.data.len().saturating_mul(8).saturating_sub(self.pos)
}
/// Step over `n` bits of opaque data (`UserData` [WD 8.6]).
///
/// # Errors
/// [`PrcError::Truncated`] when fewer than `n` bits remain; the cursor does not move.
pub fn skip_bits(&mut self, n: usize) -> Result<(), PrcError> {
if n > self.remaining() {
return Err(PrcError::Truncated("user data"));
}
self.pos = self.pos.saturating_add(n);
Ok(())
}
/// One bit; a PRC `Boolean` [WD 10.3.3].
///
/// # Errors
/// [`PrcError::Truncated`] at the end of the data.
pub fn bit(&mut self) -> Result<bool, PrcError> {
let byte = self.data.get(self.pos / 8).ok_or(PrcError::Truncated("a bit"))?;
let b = (byte >> (7 - (self.pos % 8))) & 1 == 1;
// `pos / 8` indexed the data, so `pos + 1` is at most `8 * len`.
self.pos = self.pos.saturating_add(1);
Ok(b)
}
/// `n` bits (at most 32), MSB-first; `UnsignedIntegerWithVariableBitNumber` [WD 11.13].
///
/// # Errors
/// [`PrcError::Truncated`] at the end of the data; [`PrcError::Malformed`] for `n > 32`.
pub fn bits(&mut self, n: u32) -> Result<u32, PrcError> {
if n > 32 {
return Err(PrcError::Malformed(format!("{n}-bit field")));
}
if self.remaining() < n as usize {
return Err(PrcError::Truncated("a bit field"));
}
let mut v: u64 = 0;
for _ in 0..n {
v = (v << 1) | u64::from(self.bit()?);
}
// n <= 32, so v fits.
Ok(u32::try_from(v).unwrap_or(u32::MAX))
}
fn byte(&mut self) -> Result<u8, PrcError> {
Ok(u8::try_from(self.bits(8)?).unwrap_or(u8::MAX))
}
/// A `Character`: one byte [WD 10.3.4].
///
/// # Errors
/// [`PrcError::Truncated`].
pub fn character(&mut self) -> Result<u8, PrcError> {
self.byte()
}
/// An `UnsignedInteger` [WD 11.10]: while a 1 bit precedes it, one more eight-bit group,
/// low group first.
///
/// # Errors
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups.
pub fn unsigned_integer(&mut self) -> Result<u32, PrcError> {
let mut v: u32 = 0;
let mut shift = 0;
while self.bit()? {
if shift >= 32 {
return Err(PrcError::Malformed("UnsignedInteger over 32 bits".into()));
}
v |= u32::from(self.byte()?) << shift;
shift += 8;
}
Ok(v)
}
/// An `Integer` [WD 11.11]: a 0 bit is zero; otherwise eight-bit groups, low group first,
/// each followed by a continue bit, sign-extended from the last group's top bit.
///
/// # Errors
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups.
pub fn integer(&mut self) -> Result<i32, PrcError> {
if !self.bit()? {
return Ok(0);
}
let mut v: u32 = 0;
let mut shift = 0;
loop {
if shift >= 32 {
return Err(PrcError::Malformed("Integer over 32 bits".into()));
}
let group = self.byte()?;
v |= u32::from(group) << shift;
shift += 8;
if !self.bit()? {
if group & 0x80 != 0 && shift < 32 {
v |= u32::MAX << shift;
}
return Ok(v.cast_signed());
}
}
}
/// An `IntegerWithVariableBitNumber(n)` [WD 11.12]: a negative flag (1 = negative; the WD
/// prose says the opposite, its sample code and real files agree on this), then the
/// magnitude in `n - 1` bits.
///
/// # Errors
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] for `n > 33`.
pub fn integer_vbn(&mut self, n: u32) -> Result<i64, PrcError> {
let Some(magnitude_bits) = n.checked_sub(1) else {
return Ok(0);
};
let negative = self.bit()?;
let m = i64::from(self.bits(magnitude_bits)?);
Ok(if negative { -m } else { m })
}
/// A `DoubleWithVariableBitNumber(tolerance, n)` [WD 11.14]: a sign-magnitude quantum
/// count times `tolerance`.
///
/// # Errors
/// As [`Self::integer_vbn`].
pub fn double_vbn(&mut self, tolerance: f64, n: u32) -> Result<f64, PrcError> {
Ok(self.integer_vbn(n)? as f64 * tolerance)
}
/// `NumberOfBitsThenUnsignedInteger` [WD 11.15]: a 5-bit width, then the value in that
/// many bits.
///
/// # Errors
/// [`PrcError::Truncated`].
pub fn nbits_then_unsigned(&mut self) -> Result<u32, PrcError> {
let n = self.bits(5)?;
self.bits(n)
}
/// A `String` [WD 10.3.7, 11.4]: `None` for the null string, else UTF-8 (invalid
/// sequences replaced).
///
/// # Errors
/// [`PrcError::Truncated`] when the byte count runs past the data.
pub fn string(&mut self) -> Result<Option<String>, PrcError> {
if !self.bit()? {
return Ok(None);
}
let len = self.unsigned_integer()? as usize;
if len.saturating_mul(8) > self.remaining() {
return Err(PrcError::Truncated("a String"));
}
let bytes = (0..len).map(|_| self.byte()).collect::<Result<Vec<u8>, _>>()?;
Ok(Some(String::from_utf8_lossy(&bytes).into_owned()))
}
/// `FloatAsBytes` [WD 11.5]: an IEEE binary32, least-significant byte first.
///
/// # Errors
/// [`PrcError::Truncated`].
pub fn float_as_bytes(&mut self) -> Result<f32, PrcError> {
let b = [self.byte()?, self.byte()?, self.byte()?, self.byte()?];
Ok(f32::from_le_bytes(b))
}
/// A `Double` [WD 11.17]: a prefix code from the Double table, then a sign bit and, for
/// an exponent code, a byte-wise mantissa with back-references.
///
/// The two-bit zero code has no sign bit. Back-reference offsets 1 to 5 are all accepted,
/// distance 1 included (PDF Association pdf-issues #769).
///
/// # Errors
/// [`PrcError::UnknownDoubleCode`], [`PrcError::Truncated`], or [`PrcError::Malformed`]
/// for a back-reference outside the value.
pub fn double(&mut self) -> Result<f64, PrcError> {
let mut code = 0u32;
let mut entry = None;
for n in 1..=acof::MAX_CODE_BITS {
code = (code << 1) | u32::from(self.bit()?);
if let Some(e) = acof::lookup(n, code) {
if n == 2 && code == 0b01 {
return Ok(0.0);
}
entry = Some(e);
break;
}
}
let entry = entry.ok_or(PrcError::UnknownDoubleCode)?;
let negative = self.bit()?;
let upper = match entry {
Entry::Double(v) => return Ok(if negative { -v } else { v }),
Entry::Exponent(upper) => upper,
};
let mut b = (u64::from(upper) << 32).to_le_bytes();
if self.bit()? {
self.mantissa(&mut b)?;
}
let sign = if negative { 1u64 << 63 } else { 0 };
Ok(f64::from_bits(u64::from_le_bytes(b) | sign))
}
/// Mantissa bits 51..0 into `b` (little-endian byte order): a nibble, then bytes 5 down
/// to 0, each a literal or a back-reference to a byte already set.
fn mantissa(&mut self, b: &mut [u8; 8]) -> Result<(), PrcError> {
let nibble = u8::try_from(self.bits(4)?).unwrap_or(0);
let [.., b6, _] = b;
*b6 |= nibble;
let at = |b: &[u8; 8], i: usize| b.get(i).copied().ok_or_else(|| PrcError::Malformed("Double back-reference past the value".into()));
for cbi in (0..=5usize).rev() {
let prev = at(b, cbi + 1)?;
let value = if self.bit()? {
self.byte()?
} else {
match self.bits(3)? {
// Fill this byte and every lower one with the previous byte.
0 => {
b.iter_mut().take(cbi + 1).for_each(|x| *x = prev);
return Ok(());
}
// As 0, but the lowest byte is a literal.
6 => {
b.iter_mut().take(cbi + 1).skip(1).for_each(|x| *x = prev);
let low = self.byte()?;
let [b0, ..] = b;
*b0 = low;
return Ok(());
}
off @ 1..=5 => at(b, cbi + off as usize)?,
_ => return Err(PrcError::Malformed("Double mantissa offset 7".into())),
}
};
if let Some(x) = b.get_mut(cbi) {
*x = value;
}
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::testw::W;
/// The code for IEEE exponent `e`.
fn exponent_code(e: u32) -> (u32, u32) {
acof::exponent_code(e).expect("the table has a row per exponent")
}
fn read_double(w: &W) -> f64 {
BitReader::new(&w.bytes()).double().unwrap()
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn zero_is_the_two_bit_code_with_no_sign() {
let mut w = W::default();
w.put(0b01, 2).put(0b1111_1111, 8);
let bytes = w.bytes();
let mut r = BitReader::new(&bytes);
assert_eq!(r.double().unwrap(), 0.0);
assert_eq!(r.position(), 2);
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn exponent_with_literal_mantissa() {
let v = -1234.5678f64;
let le = v.to_bits().to_le_bytes();
let e = u32::try_from((v.to_bits() >> 52) & 0x7ff).unwrap();
let (n, c) = exponent_code(e);
let mut w = W::default();
w.put(u64::from(c), n).put(1, 1).put(1, 1).put(u64::from(le[6] & 0x0f), 4);
for byte in le[..6].iter().rev() {
w.put(1, 1).put(u64::from(*byte), 8);
}
assert_eq!(read_double(&w), v);
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn exponent_without_mantissa_is_a_power_of_two() {
let (n, c) = exponent_code(1023 + 3);
let mut w = W::default();
w.put(u64::from(c), n).put(0, 1).put(0, 1);
assert_eq!(read_double(&w), 8.0);
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn fill_back_reference_and_literal_low_byte() {
// Mantissa bytes 6..0 = 0x05, 0xAA, 0xAA (offset 1), 0xAA (offset 2), fill 0xAA to byte 1, byte 0 = 0x11.
let (n, c) = exponent_code(1023);
let mut w = W::default();
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4);
w.put(1, 1).put(0xAA, 8);
w.put(0, 1).put(1, 3);
w.put(0, 1).put(2, 3);
w.put(0, 1).put(6, 3).put(0x11, 8);
assert_eq!(read_double(&w), f64::from_le_bytes([0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xF5, 0x3F]));
// Byte 5 literal, then fill 4..0.
let mut w = W::default();
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4);
w.put(1, 1).put(0x77, 8).put(0, 1).put(0, 3);
assert_eq!(read_double(&w), f64::from_le_bytes([0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0xF5, 0x3F]));
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn a_back_reference_past_the_value_is_refused() {
let (n, c) = exponent_code(1023);
let mut w = W::default();
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0, 4);
w.put(0, 1).put(5, 3).put(0, 16); // byte 5 + offset 5 = byte 10
assert!(matches!(BitReader::new(&w.bytes()).double(), Err(PrcError::Malformed(_))));
}
#[test]
fn without_the_table_a_double_is_an_error_not_a_panic() {
let mut r = BitReader::new(&[0x40, 0, 0]);
assert_eq!(r.double(), Err(PrcError::UnknownDoubleCode));
assert!(matches!(BitReader::new(&[0x40]).double(), Err(PrcError::Truncated(_))));
}
#[test]
fn integers_and_strings() {
let mut w = W::default();
// UnsignedInteger 300 = groups 0x2C, 0x01.
w.put(1, 1).put(0x2C, 8).put(1, 1).put(0x01, 8).put(0, 1);
// Integer -2 = group 0xFE, stop.
w.put(1, 1).put(0xFE, 8).put(0, 1);
// Integer 200 = 0xC8 then 0x00 (top bit clear), stop.
w.put(1, 1).put(0xC8, 8).put(1, 1).put(0x00, 8).put(0, 1);
// IntegerWithVariableBitNumber(4) = -5.
w.put(1, 1).put(5, 3);
// String "hé".
w.put(1, 1).put(1, 1).put(3, 8).put(0, 1).put(0x68, 8).put(0xC3, 8).put(0xA9, 8);
// Null string.
w.put(0, 1);
let bytes = w.bytes();
let mut r = BitReader::new(&bytes);
assert_eq!(r.unsigned_integer().unwrap(), 300);
assert_eq!(r.integer().unwrap(), -2);
assert_eq!(r.integer().unwrap(), 200);
assert_eq!(r.integer_vbn(4).unwrap(), -5);
assert_eq!(r.string().unwrap().as_deref(), Some("hé"));
assert_eq!(r.string().unwrap(), None);
}
#[test]
fn the_writer_round_trips_through_the_reader() {
let mut w = W::default();
w.uint(0).uint(u32::MAX).int(-129).int(i32::MIN).int(i32::MAX).string(Some("glass")).string(None);
let bytes = w.bytes();
let mut r = BitReader::new(&bytes);
assert_eq!(r.unsigned_integer().unwrap(), 0);
assert_eq!(r.unsigned_integer().unwrap(), u32::MAX);
assert_eq!(r.integer().unwrap(), -129);
assert_eq!(r.integer().unwrap(), i32::MIN);
assert_eq!(r.integer().unwrap(), i32::MAX);
assert_eq!(r.string().unwrap().as_deref(), Some("glass"));
assert_eq!(r.string().unwrap(), None);
}
#[test]
fn oversized_groups_and_widths_are_malformed() {
// Five UnsignedInteger groups.
let mut w = W::default();
(0..5).for_each(|_| {
w.put(1, 1).put(0xFF, 8);
});
w.put(0, 1);
assert!(matches!(BitReader::new(&w.bytes()).unsigned_integer(), Err(PrcError::Malformed(_))));
assert!(matches!(BitReader::new(&[0; 8]).bits(33), Err(PrcError::Malformed(_))));
assert!(matches!(BitReader::new(&[0; 8]).integer_vbn(34), Err(PrcError::Malformed(_))));
assert_eq!(BitReader::new(&[]).integer_vbn(0), Ok(0));
}
#[test]
fn truncation_is_an_error_not_a_panic() {
let mut r = BitReader::new(&[0xFF]);
assert!(r.unsigned_integer().is_err());
let mut r = BitReader::new(&[0xC0, 0x40]);
assert!(matches!(r.string(), Err(PrcError::Truncated(_))));
let mut r = BitReader::new(&[0xAB]);
assert!(matches!(r.skip_bits(9), Err(PrcError::Truncated(_))));
assert_eq!(r.position(), 0, "a refused skip does not move the cursor");
r.skip_bits(8).unwrap();
assert_eq!(r.remaining(), 0);
}
}
+325
View File
@@ -0,0 +1,325 @@
//! The PRC container: file header, file-structure descriptions, section extents and
//! inflation [WD 6.1-6.2, 10.2].
//!
//! Section extents are not stored: each section runs to the next one's start offset, and the
//! last one to the model file's start. Inflation stops at the zlib stream's end, so padding
//! between sections is tolerated.
use std::io::Read;
use crate::PrcError;
use crate::bits::BitReader;
use crate::schema::Schema;
/// The PRC version this reader implements.
pub const PRC_READER_VERSION: u32 = 8137;
/// The most file structures [`PrcFile::parse`] accepts.
pub const MAX_FILE_STRUCTURES: usize = 65_536;
/// Ceiling on the total inflated size of every section in one PRC stream.
pub const MAX_INFLATED_BYTES: usize = 512 * 1024 * 1024;
/// Sections per file structure [WD 6.1.2]: its header, then the five in [`SectionKind`]
/// order.
const SECTIONS: usize = 6;
/// A 128-bit PRC identifier [WD 10.2.4]: four little-endian `u32`s.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct UniqueId(pub [u32; 4]);
/// The compressed sections of one file structure, in file order [WD 6.1.2, 6.2.1].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SectionKind {
/// Starts with the file structure's schema, then its globals entity.
Globals,
/// The product-structure tree.
Tree,
/// Tessellated (mesh) data.
Tessellation,
/// Exact (B-rep) geometry.
Geometry,
/// Supplementary geometry.
ExtraGeometry,
}
impl SectionKind {
/// Every kind, in file order.
pub const ALL: [SectionKind; 5] =
[SectionKind::Globals, SectionKind::Tree, SectionKind::Tessellation, SectionKind::Geometry, SectionKind::ExtraGeometry];
/// The section's name, as used in errors.
pub fn name(self) -> &'static str {
match self {
SectionKind::Globals => "globals",
SectionKind::Tree => "tree",
SectionKind::Tessellation => "tessellation",
SectionKind::Geometry => "geometry",
SectionKind::ExtraGeometry => "extra geometry",
}
}
fn index(self) -> usize {
match self {
SectionKind::Globals => 0,
SectionKind::Tree => 1,
SectionKind::Tessellation => 2,
SectionKind::Geometry => 3,
SectionKind::ExtraGeometry => 4,
}
}
}
/// The uncompressed file header [WD 6.1.1].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PrcHeader {
/// The oldest reader version the producer says can read the file.
pub min_version_for_read: u32,
/// The producer's PRC version.
pub authoring_version: u32,
/// The file's identifier.
pub file_id: UniqueId,
/// The producing application's identifier (all zero = unregistered).
pub application_id: UniqueId,
}
/// One file structure: its uncompressed header fields and its five inflated sections
/// [WD 6.2].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FileStructure {
/// The identifier; equals the file header's description of it.
pub id: UniqueId,
/// Per-structure reader version floor; may differ from the file's.
pub min_version_for_read: u32,
/// Per-structure producer version.
pub authoring_version: u32,
/// Picture payloads (JPEG, PNG, ...) carried uncompressed in the header, referenced by
/// index from picture entities [WD 6.2.2].
pub pictures: Vec<Vec<u8>>,
sections: [Vec<u8>; 5],
}
impl FileStructure {
/// The inflated bytes of one section.
pub fn section(&self, kind: SectionKind) -> &[u8] {
self.sections.get(kind.index()).map_or(&[], Vec::as_slice)
}
/// The globals section's leading schema: the fields producers newer than this reader
/// added to each entity type [WD 6.3].
///
/// # Errors
/// As [`Schema::read`].
pub fn schema(&self) -> Result<Schema, PrcError> {
Schema::read(&mut BitReader::new(self.section(SectionKind::Globals)))
}
}
/// A parsed PRC stream: header, file structures and model-file section.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PrcFile {
/// The file header.
pub header: PrcHeader,
/// File structures in physical order (referenced before referencing).
pub file_structures: Vec<FileStructure>,
/// The inflated model-file section: its schema, then the model file entity.
pub model_file: Vec<u8>,
/// The `UncompressedFiles` blocks after the file header [WD 6.1.1], which picture
/// entities may also reference.
pub uncompressed_files: Vec<Vec<u8>>,
}
/// A little-endian byte cursor over the uncompressed parts of the stream.
struct Bytes<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Bytes<'a> {
fn take(&mut self, n: usize, what: &'static str) -> Result<&'a [u8], PrcError> {
let end = self.pos.checked_add(n).ok_or(PrcError::Truncated(what))?;
let s = self.data.get(self.pos..end).ok_or(PrcError::Truncated(what))?;
self.pos = end;
Ok(s)
}
fn u32(&mut self, what: &'static str) -> Result<u32, PrcError> {
let s = self.take(4, what)?;
let b: [u8; 4] = s.try_into().map_err(|_| PrcError::Truncated(what))?;
Ok(u32::from_le_bytes(b))
}
fn id(&mut self, what: &'static str) -> Result<UniqueId, PrcError> {
Ok(UniqueId([self.u32(what)?, self.u32(what)?, self.u32(what)?, self.u32(what)?]))
}
fn magic(&mut self) -> Result<(), PrcError> {
match self.take(3, "magic") {
Ok(b"PRC") => Ok(()),
_ => Err(PrcError::NotPrc),
}
}
/// `UncompressedBlock`s: a count, then size-prefixed blocks [WD 10.2.2-3].
fn blocks(&mut self, what: &'static str) -> Result<Vec<Vec<u8>>, PrcError> {
let n = offset(self.u32(what)?);
// Each block needs at least its 4-byte size, so `n` is bounded by the data left.
if n > self.data.len().saturating_sub(self.pos) / 4 {
return Err(PrcError::Truncated(what));
}
(0..n)
.map(|_| {
let len = offset(self.u32(what)?);
Ok(self.take(len, what)?.to_vec())
})
.collect()
}
}
/// A stored `u32` offset or count as a `usize` (saturating on a 16-bit target, where no data
/// that large could exist).
fn offset(v: u32) -> usize {
usize::try_from(v).unwrap_or(usize::MAX)
}
/// One file structure description of the file header [WD 6.2]: its id and the offsets of
/// its sections.
fn file_structure_description(r: &mut Bytes) -> Result<(UniqueId, Vec<usize>), PrcError> {
let id = r.id("file structure description")?;
if r.u32("file structure description")? != 0 {
return Err(PrcError::Malformed("file structure reserved field is not 0".into()));
}
let count = offset(r.u32("file structure description")?);
if count < SECTIONS {
return Err(PrcError::Malformed(format!("{count} sections in a file structure")));
}
let mut offs = Vec::with_capacity(SECTIONS);
// Offsets past the six this reader uses are read and dropped; each read is bounded by
// the data, so a huge `count` ends in `Truncated`.
for i in 0..count {
let o = r.u32("section offset")?;
if i < SECTIONS {
offs.push(offset(o));
}
}
Ok((id, offs))
}
/// Where one file structure's parts lie: its section offsets and the end of its last section.
struct Extent<'a> {
offs: &'a [usize],
last: usize,
}
impl Extent<'_> {
fn bound(&self, i: usize) -> usize {
self.offs.get(i).copied().unwrap_or(self.last)
}
}
/// The file structure `id` lying at `extent`: its header and its inflated sections, charged
/// to `budget`.
fn file_structure(data: &[u8], id: UniqueId, extent: &Extent, budget: &mut Budget) -> Result<FileStructure, PrcError> {
if extent.offs.windows(2).any(|w| w.first() > w.get(1)) || extent.bound(SECTIONS - 1) > extent.last {
return Err(PrcError::Malformed("file structure section offsets are not ascending".into()));
}
let header = data.get(..extent.bound(1)).ok_or(PrcError::Truncated("file structure header"))?;
let mut h = Bytes { data: header, pos: extent.bound(0) };
h.magic()?;
let min_version_for_read = h.u32("file structure header")?;
let authoring_version = h.u32("file structure header")?;
if h.id("file structure header")? != id {
return Err(PrcError::Malformed("file structure id differs from its description".into()));
}
let _application = h.id("file structure header")?;
let pictures = h.blocks("file structure pictures")?;
let mut sections: [Vec<u8>; 5] = Default::default();
for (i, (slot, kind)) in sections.iter_mut().zip(SectionKind::ALL).enumerate() {
let end = if i + 2 < SECTIONS { extent.bound(i + 2) } else { extent.last };
*slot = budget.inflate(data, (extent.bound(i + 1), end), kind.name())?;
}
Ok(FileStructure { id, min_version_for_read, authoring_version, pictures, sections })
}
/// What is left of the inflation ceiling, and the ceiling itself for the error.
struct Budget {
left: usize,
limit: usize,
}
impl Budget {
/// The zlib stream in `data[range]`, its size charged to the budget.
fn inflate(&mut self, data: &[u8], range: (usize, usize), section: &'static str) -> Result<Vec<u8>, PrcError> {
let (start, end) = range;
if start == end {
return Ok(Vec::new());
}
let src = data.get(start..end).ok_or_else(|| PrcError::Malformed(format!("{section} section {start}..{end} is outside the data")))?;
let mut out = Vec::new();
// One byte past the budget is enough to know it was exceeded.
let cap = u64::try_from(self.left).unwrap_or(u64::MAX).saturating_add(1);
flate2::read::ZlibDecoder::new(src).take(cap).read_to_end(&mut out).map_err(|e| PrcError::Inflate { section, reason: e.to_string() })?;
self.left = self.left.checked_sub(out.len()).ok_or(PrcError::TooLarge { limit: self.limit })?;
Ok(out)
}
}
impl PrcFile {
/// Parse a PRC stream (the decoded data of a `/3D` stream with `/Subtype /PRC`).
///
/// Newer `min_version_for_read` values are not refused: the fields read here are
/// unchanged across published versions, and the caller sees the versions in
/// [`PrcHeader`].
///
/// # Errors
/// [`PrcError::NotPrc`] without the magic; [`PrcError::Truncated`] or
/// [`PrcError::Malformed`] for a damaged header or offset table; [`PrcError::Inflate`]
/// for a section that is not zlib; [`PrcError::TooLarge`] past [`MAX_INFLATED_BYTES`]
/// in total.
///
/// # Examples
/// ```
/// use pdfcraft_prc::{PrcError, PrcFile};
/// assert_eq!(PrcFile::parse(b"%PDF"), Err(PrcError::NotPrc));
/// ```
pub fn parse(data: &[u8]) -> Result<Self, PrcError> {
Self::parse_with_limit(data, MAX_INFLATED_BYTES)
}
/// [`Self::parse`] with a caller-chosen ceiling on the total inflated size, for callers
/// with less memory to spend than [`MAX_INFLATED_BYTES`].
///
/// # Errors
/// As [`Self::parse`]; [`PrcError::TooLarge`] carries `limit`.
pub fn parse_with_limit(data: &[u8], limit: usize) -> Result<Self, PrcError> {
let mut r = Bytes { data, pos: 0 };
r.magic()?;
let header = PrcHeader {
min_version_for_read: r.u32("file header")?,
authoring_version: r.u32("file header")?,
file_id: r.id("file header")?,
application_id: r.id("file header")?,
};
let n_fs = offset(r.u32("file structure count")?);
if n_fs == 0 || n_fs > MAX_FILE_STRUCTURES {
return Err(PrcError::Malformed(format!("{n_fs} file structures")));
}
let descriptions = (0..n_fs).map(|_| file_structure_description(&mut r)).collect::<Result<Vec<_>, _>>()?;
let mf_start = offset(r.u32("model file offsets")?);
let mf_end = offset(r.u32("model file offsets")?);
let uncompressed_files = r.blocks("uncompressed files")?;
if mf_end < mf_start {
return Err(PrcError::Malformed("model file ends before it starts".into()));
}
let mut budget = Budget { left: limit, limit };
let last = mf_start.min(data.len());
let file_structures =
descriptions.iter().map(|(id, offs)| file_structure(data, *id, &Extent { offs, last }, &mut budget)).collect::<Result<Vec<_>, _>>()?;
let model_file = budget.inflate(data, (mf_start, mf_end.min(data.len())), "model file")?;
Ok(PrcFile { header, file_structures, model_file, uncompressed_files })
}
}
+38
View File
@@ -0,0 +1,38 @@
//! The crate's error type.
/// Why a PRC stream could not be read.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PrcError {
/// The data does not start with the `PRC` magic [WD 6.1.1].
#[error("not a PRC stream: missing the \"PRC\" magic")]
NotPrc,
/// A value ran past the end of its byte or bit stream.
#[error("PRC data truncated while reading {0}")]
Truncated(&'static str),
/// A structural field holds a value the format forbids.
#[error("malformed PRC: {0}")]
Malformed(String),
/// No `Double` code of up to 22 bits matched [WD 11.17].
#[error("PRC Double: no code matches the next 22 bits")]
UnknownDoubleCode,
/// A section is not a valid zlib stream.
#[error("PRC {section} section does not inflate: {reason}")]
Inflate {
/// The section's name.
section: &'static str,
/// The decoder's message.
reason: String,
},
/// The inflated sections together exceed the ceiling ([`crate::MAX_INFLATED_BYTES`] or
/// the caller's limit).
#[error("PRC sections inflate past the {limit}-byte ceiling")]
TooLarge {
/// The ceiling that was hit.
limit: usize,
},
/// A construct this reader does not decode; the part of the file holding it cannot be
/// read past.
#[error("PRC {0} is not supported")]
Unsupported(&'static str),
}
+26
View File
@@ -0,0 +1,26 @@
//! A reader for PRC, the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries
//! (ISO 32000-2 §13.6.3, ISO 14739-1).
//!
//! Layer L0, standalone: this crate depends on no other PdfCraft crate. It reads the
//! uncompressed container, inflates each section, and decodes the bit-level primitives and
//! the schema a newer producer uses to describe fields this reader does not know.
//!
//! Clause numbers in `[WD x.y]` refer to the PRC working draft, ISO/TC 171/SC 2 N570.
//!
//! Every read is bounded: inflation by [`MAX_INFLATED_BYTES`] (or a caller's limit), schema
//! programs by a step and depth ceiling. Damaged data is an error, never a panic.
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod acof;
mod bits;
mod container;
mod error;
mod schema;
#[cfg(test)]
mod testw;
pub use bits::BitReader;
pub use container::{FileStructure, MAX_FILE_STRUCTURES, MAX_INFLATED_BYTES, PRC_READER_VERSION, PrcFile, PrcHeader, SectionKind, UniqueId};
pub use error::PrcError;
pub use schema::Schema;
+525
View File
@@ -0,0 +1,525 @@
//! `FileStructureSchema` and its token interpreter [WD 6.3, 9].
//!
//! A producer newer than the reader describes, per entity type, the fields it appended; the
//! reader runs that program after the fields it knows, to step over the rest [WD 9.1]. Token
//! values follow the WD 9.2 table as corrected by PDF Association pdf-issues #407
//! (`Block_Start` = 19; 39 and 40 swallow one token).
//!
//! Choices the sources leave open, made here:
//! - `SimpleFor` reads its count as an `UnsignedInteger` [WD 9.3.17 pseudocode; pdf-issues
//! #575]: that read also decodes an `Integer`-written non-negative count, while a signed
//! read does not decode an unsigned one whose top group is 0x80 or higher.
//! - `Block_Version V` runs its block only when `V` is newer than
//! [`crate::PRC_READER_VERSION`]: an older block holds fields this reader already reads
//! natively [WD 9.3.20].
//! - `Extent_1D` and `Extent_2D` read 2 and 4 `Double`s: `Interval` and `Domain`
//! [WD 8.2.2, 8.2.4].
//! - Expression values are `i64`; a `Double` read as an operand truncates (NaN reads as 0).
//! - Variables are not block-scoped: a program that reuses an index after its block ends
//! reads the stale value instead of an error.
//! - Pointer tokens (12-14) and `Value_CurveIs3D` (28) need the exact-geometry reader and are
//! refused with [`PrcError::Unsupported`].
use std::collections::HashMap;
use crate::PrcError;
use crate::bits::BitReader;
/// Steps (tokens executed, loop iterations included) one
/// [`Schema::skip_added_fields`] call may take.
const MAX_STEPS: u64 = 1 << 22;
/// Nesting depth of blocks, loops, expressions and `Father_Type` calls.
const MAX_DEPTH: u32 = 64;
/// The per-entity-type programs at the start of a globals or model-file
/// bitstream [WD 6.3.1].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Schema {
programs: HashMap<u32, Vec<u32>>,
}
impl Schema {
/// Read a schema: `UnsignedInteger` count, then per entry the entity
/// type, a token count and the tokens [WD 6.3.2].
///
/// # Errors
/// [`PrcError::Truncated`] when a count runs past the data;
/// [`PrcError::Malformed`] for an entity type listed twice.
pub fn read(r: &mut BitReader<'_>) -> Result<Self, PrcError> {
let n = r.unsigned_integer()? as usize;
// Every entry takes at least two bits.
if n > r.remaining() / 2 {
return Err(PrcError::Truncated("the schema"));
}
let mut programs = HashMap::new();
for _ in 0..n {
let entity_type = r.unsigned_integer()?;
let len = r.unsigned_integer()? as usize;
if len > r.remaining() {
return Err(PrcError::Truncated("a schema program"));
}
let tokens = (0..len).map(|_| r.unsigned_integer()).collect::<Result<Vec<_>, _>>()?;
if programs.insert(entity_type, tokens).is_some() {
return Err(PrcError::Malformed(format!("schema lists entity type {entity_type} twice")));
}
}
Ok(Self { programs })
}
/// True when no entity type has added fields (the common case: the file
/// was written at the version this reader implements).
pub fn is_empty(&self) -> bool {
self.programs.is_empty()
}
/// The entity types the schema describes, unordered.
pub fn entity_types(&self) -> impl Iterator<Item = u32> + '_ {
self.programs.keys().copied()
}
/// Consume the fields a newer producer appended to `entity_type`, if the
/// schema describes it; a no-op otherwise.
///
/// # Errors
/// [`PrcError::Unsupported`] for a token that needs exact geometry;
/// [`PrcError::Malformed`] for an ill-formed program or one past the step
/// or depth ceiling; any read error from `r`.
pub fn skip_added_fields(&self, entity_type: u32, r: &mut BitReader<'_>) -> Result<(), PrcError> {
let Some(program) = self.programs.get(&entity_type) else {
return Ok(());
};
let mut exec = Exec { schema: self, r, vars: HashMap::new(), loop_counter: 0, steps: 0 };
exec.program(program, 0)
}
}
struct Exec<'s, 'r, 'd> {
schema: &'s Schema,
r: &'r mut BitReader<'d>,
vars: HashMap<u32, i64>,
loop_counter: i64,
steps: u64,
}
fn malformed(what: &str) -> PrcError {
PrcError::Malformed(format!("schema program: {what}"))
}
/// The token at `*pos`, advancing past it.
fn next(prog: &[u32], pos: &mut usize) -> Result<u32, PrcError> {
let t = prog.get(*pos).copied().ok_or_else(|| malformed("ends mid-statement"))?;
*pos += 1;
Ok(t)
}
impl Exec<'_, '_, '_> {
fn tick(&mut self, depth: u32) -> Result<(), PrcError> {
self.steps += 1;
if self.steps > MAX_STEPS {
return Err(malformed("step ceiling reached"));
}
if depth > MAX_DEPTH {
return Err(malformed("nesting ceiling reached"));
}
Ok(())
}
fn program(&mut self, prog: &[u32], depth: u32) -> Result<(), PrcError> {
let mut pos = 0;
while pos < prog.len() {
self.stmt(prog, &mut pos, true, depth)?;
}
Ok(())
}
/// One data read for tokens 0-5 and 7-11, returning its value as an
/// operand.
fn read(&mut self, token: u32) -> Result<i64, PrcError> {
let doubles = match token {
0 => return Ok(i64::from(self.r.bit()?)),
1 => return Ok(self.r.double()? as i64),
2 => return Ok(i64::from(self.r.character()?)),
3 => return Ok(i64::from(self.r.unsigned_integer()?)),
4 => return Ok(i64::from(self.r.integer()?)),
5 => {
self.r.string()?;
return Ok(0);
}
7 => 2,
8 => 3,
9 => 2,
10 => 4,
11 => 6,
_ => return Err(malformed("not a data token")),
};
for _ in 0..doubles {
self.r.double()?;
}
Ok(0)
}
/// Statements until `Block_End`, which is consumed.
fn block(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
loop {
if prog.get(*pos) == Some(&21) {
*pos += 1;
return Ok(());
}
self.stmt(prog, pos, run, depth + 1)?;
}
}
/// Run `body` (one statement at `*pos`) `count` times; `*pos` ends past it.
fn repeat(&mut self, prog: &[u32], pos: &mut usize, count: i64, run: bool, depth: u32) -> Result<(), PrcError> {
let start = *pos;
self.stmt(prog, pos, false, depth + 1)?;
if run {
let saved = self.loop_counter;
for i in 0..count.max(0) {
self.tick(depth)?;
self.loop_counter = i;
let mut p = start;
self.stmt(prog, &mut p, true, depth + 1)?;
}
self.loop_counter = saved;
}
Ok(())
}
/// One statement; with `run` false, only step over its tokens.
fn stmt(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
self.tick(depth)?;
match next(prog, pos)? {
t @ (0..=5 | 7..=11) => {
if run {
self.read(t)?;
}
}
6 => self.parent_program(prog, pos, run, depth)?,
12 => {
next(prog, pos)?;
if run {
return Err(PrcError::Unsupported("a schema pointer field"));
}
}
13 | 14 => {
if run {
return Err(PrcError::Unsupported("a schema pointer field"));
}
}
15 => {
let count = self.expr(prog, pos, run, depth + 1)?;
self.repeat(prog, pos, count, run, depth)?;
}
16 => {
let count = if run { i64::from(self.r.unsigned_integer()?) } else { 0 };
self.repeat(prog, pos, count, run, depth)?;
}
17 => self.conditional(prog, pos, run, depth)?,
19 => self.block(prog, pos, run, depth)?,
20 => {
let version = next(prog, pos)?;
let newer = version > crate::PRC_READER_VERSION;
self.block(prog, pos, run && newer, depth)?;
}
22 => {
let v = next(prog, pos)?;
if run {
self.vars.insert(v, 0);
}
}
23 | 24 => {
let v = next(prog, pos)?;
let value = self.expr(prog, pos, run, depth + 1)?;
if run {
self.vars.insert(v, value);
}
}
39 | 40 => {
next(prog, pos)?;
}
t => return Err(malformed(&format!("token {t} where a statement belongs"))),
}
Ok(())
}
/// Token 6: run the named type's own program, the fields it inherits.
fn parent_program(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
let parent = next(prog, pos)?;
if run && let Some(p) = self.schema.programs.get(&parent) {
self.tick(depth + 1)?;
let mut q = 0;
while q < p.len() {
self.stmt(p, &mut q, true, depth + 1)?;
}
}
Ok(())
}
/// Token 17: `if` with an optional token-18 `else`.
fn conditional(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
let cond = self.expr(prog, pos, run, depth + 1)? != 0;
self.stmt(prog, pos, run && cond, depth + 1)?;
if prog.get(*pos) == Some(&18) {
*pos += 1;
self.stmt(prog, pos, run && !cond, depth + 1)?;
}
Ok(())
}
/// One expression; with `run` false its value is 0 and nothing is read.
fn expr(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<i64, PrcError> {
self.tick(depth)?;
let t = next(prog, pos)?;
match t {
0..=4 => {
if run {
self.read(t)
} else {
Ok(0)
}
}
25 => {
let v = next(prog, pos)?;
if !run {
return Ok(0);
}
self.vars.get(&v).copied().ok_or_else(|| malformed(&format!("variable {v} read before it is set")))
}
26 => Ok(i64::from(next(prog, pos)?)),
27 => Ok(self.loop_counter),
28 => {
if run {
Err(PrcError::Unsupported("Value_CurveIs3D"))
} else {
Ok(0)
}
}
29..=38 => {
let a = self.expr(prog, pos, run, depth + 1)?;
let b = self.expr(prog, pos, run, depth + 1)?;
if !run {
return Ok(0);
}
Ok(match t {
29 => a.wrapping_mul(b),
30 => a.checked_div(b).ok_or_else(|| malformed("division by zero"))?,
31 => a.wrapping_add(b),
32 => a.wrapping_sub(b),
33 => i64::from(a < b),
34 => i64::from(a <= b),
35 => i64::from(a > b),
36 => i64::from(a >= b),
37 => i64::from(a == b),
_ => i64::from(a != b),
})
}
t => Err(malformed(&format!("token {t} where a value belongs"))),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
use crate::testw::W;
fn schema(entries: &[(u32, &[u32])]) -> W {
let mut w = W::default();
w.uint(entries.len() as u32);
for (t, toks) in entries {
w.uint(*t).uint(toks.len() as u32);
for k in *toks {
w.uint(*k);
}
}
w
}
fn run(entries: &[(u32, &[u32])], data: &W) -> (Result<(), PrcError>, usize) {
let mut w = schema(entries);
w.append(data);
let bytes = w.bytes();
let mut r = BitReader::new(&bytes);
let s = Schema::read(&mut r).unwrap();
let start = r.position();
let res = s.skip_added_fields(172, &mut r);
(res, r.position() - start)
}
#[test]
fn integer_string_and_boolean_tokens_consume_their_fields() {
let mut d = W::default();
d.bit(true).uint(300).int(-7).string(Some("ab")).put(0x41, 8);
let want = d.len();
d.uint(99); // not part of the added fields
let (res, used) = run(&[(172, &[0, 3, 4, 5, 2])], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn data_tokens_consume_their_fields() {
let mut d = W::default();
d.bit(true).double(1.5).uint(300).int(-7).string(Some("ab")).double(2.0).double(3.0);
let want = d.len();
d.uint(99); // not part of the added fields
let (res, used) = run(&[(172, &[0, 1, 3, 4, 5, 7])], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
#[ignore = "the Double table is not in the crate yet"]
fn vector_extent_and_box_tokens_read_their_doubles() {
// Vec3, Extent1D, Extent2D, BBox: 3 + 2 + 4 + 6 Doubles.
let mut d = W::default();
for i in 0..15 {
d.double(f64::from(i) + 0.5);
}
let want = d.len();
d.uint(99);
let (res, used) = run(&[(172, &[8, 9, 10, 11])], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn arithmetic_and_comparison_operators() {
// For (5 - 3) { UInt }, then If (2 < 2) { Bool } Else { Char }.
let mut d = W::default();
d.uint(7).uint(8).put(0x41, 8);
let want = d.len();
d.uint(99);
let prog = [15, 32, 26, 5, 26, 3, 3, 17, 33, 26, 2, 26, 2, 0, 18, 2];
let (res, used) = run(&[(172, &prog)], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn an_absent_type_is_a_no_op() {
let mut w = schema(&[(300, &[3])]);
w.uint(5);
let bytes = w.bytes();
let mut r = BitReader::new(&bytes);
let s = Schema::read(&mut r).unwrap();
let at = r.position();
s.skip_added_fields(172, &mut r).unwrap();
assert_eq!(r.position(), at);
assert!(!s.is_empty());
}
#[test]
fn simple_for_reads_its_count_then_repeats_the_body() {
let mut d = W::default();
d.int(3).uint(1).uint(2).uint(3);
let want = d.len();
d.uint(77);
let (res, used) = run(&[(172, &[16, 3])], &d);
res.unwrap();
assert_eq!(used, want);
// A block body.
let mut d = W::default();
d.int(2).bit(true).uint(9).bit(false).uint(8);
let want = d.len();
let (res, used) = run(&[(172, &[16, 19, 0, 3, 21])], &d);
res.unwrap();
assert_eq!(used, want);
// A count whose top byte is 0x80 or higher, written either way.
for signed in [false, true] {
let mut d = W::default();
if signed {
d.int(200);
} else {
d.uint(200);
}
(0..200).for_each(|_| {
d.bit(true);
});
let want = d.len();
d.uint(77);
let (res, used) = run(&[(172, &[16, 0])], &d);
res.unwrap();
assert_eq!(used, want, "signed={signed}");
}
}
#[test]
fn if_else_and_variables_choose_the_branch_by_file_data() {
// DeclareAndSet v0 = <UInt from file>; if v0 > 1 { UInt } else { Character }.
let prog: &[u32] = &[24, 0, 3, 17, 35, 25, 0, 26, 1, 3, 18, 2];
let mut d = W::default();
d.uint(5).uint(4250);
let want = d.len();
let (res, used) = run(&[(172, prog)], &d);
res.unwrap();
assert_eq!(used, want);
let mut d = W::default();
d.uint(0).put(0xAB, 8);
let want = d.len();
let (res, used) = run(&[(172, prog)], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn for_counts_by_expression_and_exposes_the_counter() {
// For (2 * 2) { if counter == 2 { UInt } } => one UInt read.
let prog: &[u32] = &[15, 29, 26, 2, 26, 2, 17, 37, 27, 26, 2, 3];
let mut d = W::default();
d.uint(1234);
let want = d.len();
let (res, used) = run(&[(172, prog)], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn version_blocks_run_only_when_newer_than_the_reader() {
let mut d = W::default();
d.uint(42);
let want = d.len();
let (res, used) = run(&[(172, &[20, 8137, 3, 21, 20, 9000, 3, 21])], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn father_type_runs_the_parent_program_and_obsolete_tokens_swallow_one() {
let mut d = W::default();
d.uint(1).uint(2);
let want = d.len();
let (res, used) = run(&[(172, &[39, 3, 6, 50, 40, 8]), (50, &[3, 3])], &d);
res.unwrap();
assert_eq!(used, want);
}
#[test]
fn hostile_programs_are_refused_not_run_forever() {
let d = W::default();
// For (1000000 * 1000000) { empty block }.
let prog: &[u32] = &[15, 29, 26, 1_000_000, 26, 1_000_000, 19, 21];
assert!(matches!(run(&[(172, prog)], &d).0, Err(PrcError::Malformed(_))));
// Father_Type recursing into itself.
assert!(matches!(run(&[(172, &[6, 172])], &d).0, Err(PrcError::Malformed(_))));
// A pointer field.
assert!(matches!(run(&[(172, &[13])], &d).0, Err(PrcError::Unsupported(_))));
// A stray Block_End, an unterminated block, a division by zero.
assert!(matches!(run(&[(172, &[21])], &d).0, Err(PrcError::Malformed(_))));
assert!(matches!(run(&[(172, &[19, 0])], &d).0, Err(PrcError::Malformed(_))));
assert!(matches!(run(&[(172, &[23, 0, 30, 26, 1, 26, 0])], &d).0, Err(PrcError::Malformed(_))));
}
#[test]
fn a_duplicated_entity_type_is_malformed() {
let bytes = schema(&[(172, &[3]), (172, &[3])]).bytes();
assert!(matches!(Schema::read(&mut BitReader::new(&bytes)), Err(PrcError::Malformed(_))));
}
}
+101
View File
@@ -0,0 +1,101 @@
//! Test-only PRC bit writer: the inverse of [`crate::BitReader`].
#![allow(clippy::expect_used)]
use crate::acof;
/// An MSB-first bit sink for building test streams.
#[derive(Debug, Default, Clone)]
pub(crate) struct W {
bits: Vec<bool>,
}
impl W {
/// The low `n` bits of `v` (`n` ≤ 64), most significant first.
pub(crate) fn put(&mut self, v: u64, n: u32) -> &mut Self {
for i in (0..n).rev() {
self.bits.push((v >> i) & 1 == 1);
}
self
}
/// One bit; a PRC `Boolean`.
pub(crate) fn bit(&mut self, b: bool) -> &mut Self {
self.bits.push(b);
self
}
/// Bits written so far.
pub(crate) fn len(&self) -> usize {
self.bits.len()
}
/// Every bit of `other`, unaligned.
pub(crate) fn append(&mut self, other: &Self) -> &mut Self {
self.bits.extend_from_slice(&other.bits);
self
}
/// An `UnsignedInteger` [WD 11.10].
pub(crate) fn uint(&mut self, mut v: u32) -> &mut Self {
while v != 0 {
self.bit(true).put(u64::from(v & 0xff), 8);
v >>= 8;
}
self.bit(false)
}
/// An `Integer` [WD 11.11]: two's-complement groups, low first, each followed by a
/// continue bit.
pub(crate) fn int(&mut self, v: i32) -> &mut Self {
if v == 0 {
return self.bit(false);
}
self.bit(true);
let mut v = i64::from(v);
loop {
let group = v & 0xff;
self.put(group.cast_unsigned(), 8);
v >>= 8;
let done = (v == 0 && group & 0x80 == 0) || (v == -1 && group & 0x80 != 0);
self.bit(!done);
if done {
return self;
}
}
}
/// A `String` [WD 11.4]: presence bit, byte count, raw UTF-8 bytes.
pub(crate) fn string(&mut self, s: Option<&str>) -> &mut Self {
let Some(s) = s else {
return self.bit(false);
};
self.bit(true).uint(u32::try_from(s.len()).expect("a short test string"));
for b in s.bytes() {
self.put(u64::from(b), 8);
}
self
}
/// A finite `Double` [WD 11.17]: zero as the two-bit zero code; anything else as its
/// exponent row, the sign, then the mantissa as literal bytes.
pub(crate) fn double(&mut self, v: f64) -> &mut Self {
if v == 0.0 {
return self.put(0b01, 2);
}
let bits = v.to_bits();
let exponent = u32::try_from((bits >> 52) & 0x7ff).expect("11 bits");
let (n, c) = acof::exponent_code(exponent).expect("the Double table has a row per exponent");
let le = bits.to_le_bytes();
self.put(u64::from(c), n).bit(v < 0.0).bit(true).put(u64::from(le[6] & 0x0f), 4);
for b in le[..6].iter().rev() {
self.bit(true).put(u64::from(*b), 8);
}
self
}
/// The bits packed MSB-first, the last byte zero-padded.
pub(crate) fn bytes(&self) -> Vec<u8> {
self.bits.chunks(8).map(|c| c.iter().enumerate().fold(0u8, |a, (i, &b)| a | (u8::from(b) << (7 - i)))).collect()
}
}
+114
View File
@@ -0,0 +1,114 @@
//! The PRC container walk [WD 6.1-6.2] against synthetic streams.
#![allow(clippy::unwrap_used)]
use std::io::Write;
use pdfcraft_prc::{PrcError, PrcFile, SectionKind, UniqueId};
fn zlib(data: &[u8]) -> Vec<u8> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
}
fn u32s(out: &mut Vec<u8>, vs: &[u32]) {
for v in vs {
out.extend_from_slice(&v.to_le_bytes());
}
}
/// One file structure with the given section payloads and model file.
fn build(sections: [&[u8]; 5], model: &[u8], padding: usize) -> Vec<u8> {
const HEADER_LEN: usize = 107;
let mut fs_header = b"PRC".to_vec();
u32s(&mut fs_header, &[8137, 8137, 5, 6, 7, 8, 0, 0, 0, 0, 1, 3]);
fs_header.extend_from_slice(b"\xff\xd8\xff");
let mut body = fs_header.clone();
let mut offs = vec![HEADER_LEN];
for s in sections {
offs.push(HEADER_LEN + body.len());
body.extend(zlib(s));
body.extend(std::iter::repeat_n(0u8, padding));
}
let mf_start = HEADER_LEN + body.len();
body.extend(zlib(model));
let mf_end = HEADER_LEN + body.len();
let mut out = b"PRC".to_vec();
u32s(&mut out, &[8137, 8137, 1, 2, 3, 4, 0, 0, 0, 0, 1]);
u32s(&mut out, &[5, 6, 7, 8, 0, 6]);
u32s(&mut out, &offs.iter().map(|&o| o as u32).collect::<Vec<_>>());
u32s(&mut out, &[mf_start as u32, mf_end as u32, 0]);
assert_eq!(out.len(), HEADER_LEN);
out.extend(body);
out
}
const SECTIONS: [&[u8]; 5] = [b"globals", b"tree", b"tess", b"", b"extra"];
#[test]
fn the_container_walk_inflates_every_section() {
for padding in [0, 3] {
let f = PrcFile::parse(&build(SECTIONS, b"model file", padding)).unwrap();
assert_eq!(f.header.min_version_for_read, 8137);
assert_eq!(f.header.file_id, UniqueId([1, 2, 3, 4]));
assert_eq!(f.file_structures.len(), 1);
let fs = &f.file_structures[0];
assert_eq!(fs.id, UniqueId([5, 6, 7, 8]));
assert_eq!(fs.pictures, vec![b"\xff\xd8\xff".to_vec()]);
for (kind, want) in SectionKind::ALL.into_iter().zip(SECTIONS) {
assert_eq!(fs.section(kind), want, "{}", kind.name());
}
assert_eq!(f.model_file, b"model file");
}
}
#[test]
fn damaged_containers_are_refused_by_name() {
let good = build(SECTIONS, b"m", 0);
assert_eq!(PrcFile::parse(b"U3D\0").unwrap_err(), PrcError::NotPrc);
assert!(matches!(PrcFile::parse(&good[..40]), Err(PrcError::Truncated(_))));
// Reserved field (offset 63) not zero.
let mut bad = good.clone();
bad[63] = 1;
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
// File-structure id disagrees with its description (FS header id at 107+11).
let mut bad = good.clone();
bad[118] ^= 0xff;
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
// A section that is not zlib: corrupt the globals stream's header byte.
let globals = u32::from_le_bytes(good[75..79].try_into().unwrap()) as usize;
let mut bad = good.clone();
bad[globals] = 0;
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Inflate { section: "globals", .. })));
// Offsets out of order.
let mut bad = good;
bad[75..79].copy_from_slice(&1u32.to_le_bytes());
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
}
#[test]
fn the_inflation_ceiling_is_reported_not_exceeded() {
let f = build([&[0u8; 600], &[0u8; 600], b"", b"", b""], b"", 0);
assert!(PrcFile::parse_with_limit(&f, 1200).is_ok());
assert_eq!(PrcFile::parse_with_limit(&f, 1199).unwrap_err(), PrcError::TooLarge { limit: 1199 });
}
#[test]
fn an_offset_or_count_past_the_data_is_an_error_not_a_panic() {
let mut bad = build(SECTIONS, b"m", 0);
// The model file's start (header offset 95) far past the end, its end further still.
bad[95..99].copy_from_slice(&u32::MAX.to_le_bytes());
bad[99..103].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(PrcFile::parse(&bad).is_err());
// A section count that claims more offsets than the data holds.
let mut bad = build(SECTIONS, b"m", 0);
bad[67..71].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Truncated(_))));
}
+41
View File
@@ -0,0 +1,41 @@
//! Arbitrary bytes through every reader: an error or a value, never a panic.
use pdfcraft_prc::{BitReader, PrcFile, Schema};
use proptest::prelude::*;
proptest! {
#[test]
fn any_bytes_parse_or_fail_cleanly(data in proptest::collection::vec(any::<u8>(), 0..512), magic in any::<bool>()) {
let mut data = data;
if magic && data.len() >= 3 {
data[..3].copy_from_slice(b"PRC");
}
let _ = PrcFile::parse_with_limit(&data, 1 << 20);
}
#[test]
fn any_bits_read_or_fail_cleanly(data in proptest::collection::vec(any::<u8>(), 0..64), n in 0u32..40) {
let mut r = BitReader::new(&data);
let _ = r.unsigned_integer();
let _ = r.integer();
let _ = r.integer_vbn(n);
let _ = r.double_vbn(0.5, n);
let _ = r.nbits_then_unsigned();
let _ = r.string();
let _ = r.float_as_bytes();
let _ = r.double();
let _ = r.skip_bits(usize::try_from(n).unwrap_or(0));
prop_assert!(r.position() <= data.len() * 8);
}
#[test]
fn any_schema_runs_or_fails_cleanly(data in proptest::collection::vec(any::<u8>(), 0..256), entity in 0u32..4) {
let mut r = BitReader::new(&data);
if let Ok(schema) = Schema::read(&mut r) {
let types: Vec<u32> = schema.entity_types().chain([entity]).collect();
for t in types {
let _ = schema.skip_added_fields(t, &mut r.clone());
}
}
}
}
+1
View File
@@ -34,6 +34,7 @@ pub const TABLE: &[(&str, Class)] = &[
("filters", Class::Standalone(0)),
("crypt", Class::Standalone(0)),
("arlington", Class::Standalone(0)),
("prc", Class::Standalone(0)),
// L1 object layer
("cos", Class::Standalone(1)),
// L2 core model