Files
pdfcraft/crates/prc/src/schema.rs
T
KenM76andClaude Opus 5.5 b4ba4edc7f M12: PRC reader: charge copied blocks, refuse overflowing schema expressions
Review fixes from #6087:

- Picture and uncompressed blocks are charged to the parse ceiling before
  they are copied. File-structure headers may overlap, so each structure's
  copy counts. The ceiling test now includes the 3-byte picture.
- Schema expressions use checked multiply, add and subtract; an overflow is
  Malformed rather than a wrapped loop count.
- Each face's triangles are counted from its entity sizes and charged before
  it is triangulated.
- Matrix::mirrors divides each row by its largest entry before taking the
  determinants, so a mirror whose determinant underflows still reverses the
  winding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 23:58:08 -05:00

566 lines
21 KiB
Rust

//! `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;
/// 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; [`PrcError::TooManyElements`]
/// past the reader's allowance, one element per entry and per token.
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 {
// The entry itself costs one element, so empty programs are not free.
r.take_elements(1)?;
let entity_type = r.unsigned_integer()?;
let len = r.unsigned_integer()? as usize;
if len > r.remaining() {
return Err(PrcError::Truncated("a schema program"));
}
r.take_elements(len)?;
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. Each step is charged to `r`, so every
/// program run on one stream shares [`crate::MAX_SCHEMA_STEPS`].
///
/// # Errors
/// [`PrcError::Unsupported`] for a token that needs exact geometry;
/// [`PrcError::Malformed`] for an ill-formed program, one past the depth ceiling, or
/// one that runs out of the stream's steps; 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 };
exec.program(program, 0)
}
}
struct Exec<'s, 'r, 'd> {
schema: &'s Schema,
r: &'r mut BitReader<'d>,
vars: HashMap<u32, i64>,
loop_counter: i64,
}
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> {
if !self.r.take_step() {
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 => {
// prc-rs: reads SimpleFor's count as an Integer; this reader deliberately reads an
// UnsignedInteger (module doc).
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);
}
let overflow = || malformed("a schema expression overflows");
Ok(match t {
29 => a.checked_mul(b).ok_or_else(overflow)?,
30 => a.checked_div(b).ok_or_else(|| malformed("division by zero"))?,
31 => a.checked_add(b).ok_or_else(overflow)?,
32 => a.checked_sub(b).ok_or_else(overflow)?,
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
}
/// Each schema entry and its program's tokens are charged against the reader's element
/// allowance.
#[test]
fn program_tokens_are_charged_against_the_allowance() {
let bytes = schema(&[(172, &[1, 2, 3])]).bytes();
assert!(matches!(Schema::read(&mut BitReader::with_elements(&bytes, 3)), Err(PrcError::TooManyElements { .. })));
assert!(Schema::read(&mut BitReader::with_elements(&bytes, 4)).is_ok());
let empty = schema(&[(172, &[]), (173, &[]), (174, &[])]).bytes();
assert!(matches!(Schema::read(&mut BitReader::with_elements(&empty, 2)), Err(PrcError::TooManyElements { .. })));
assert!(Schema::read(&mut BitReader::with_elements(&empty, 3)).is_ok());
}
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]
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]
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 an_expression_that_overflows_is_malformed() {
// For ((2^31 * 2^31) * 4) { UInt }: 2^64 would wrap to 0 and read nothing.
let prog: &[u32] = &[15, 29, 29, 26, 1 << 31, 26, 1 << 31, 26, 4, 3];
let (res, _) = run(&[(172, prog)], &W::default());
assert!(matches!(res, Err(PrcError::Malformed(m)) if m.contains("overflows")));
}
#[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(_))));
}
/// Every program run on one reader draws on one step allowance.
#[test]
fn programs_on_one_stream_share_the_step_allowance() {
// For (2^19) { empty block }: about 2^20 steps, so the 2^22 allowance fits three runs.
let bytes = schema(&[(172, &[15, 26, 1 << 19, 19, 21])]).bytes();
let mut r = BitReader::new(&bytes);
let s = Schema::read(&mut r).unwrap();
let runs = (0..5).take_while(|_| s.skip_added_fields(172, &mut r).is_ok()).count();
assert_eq!(runs, 3);
assert!(matches!(s.skip_added_fields(172, &mut r), Err(PrcError::Malformed(m)) if m.contains("step ceiling")));
}
#[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(_))));
}
#[test]
fn an_entry_count_past_the_data_is_refused_before_any_entry_is_read() {
let mut w = W::default();
w.uint(1_000).uint(172).uint(0);
let bytes = w.bytes();
assert_eq!(Schema::read(&mut BitReader::new(&bytes)).unwrap_err(), PrcError::Truncated("the schema"));
}
}