mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 02:21:45 +00:00
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:
1 parent
168a3cb91e
commit
83b230ae73
13 files changed
+1740
No files matched your search
Generated
+9
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(_))));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(_))));
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user