diff --git a/crates/prc/src/arrays.rs b/crates/prc/src/arrays.rs index dff2f430d6..868e561fa2 100644 --- a/crates/prc/src/arrays.rs +++ b/crates/prc/src/arrays.rs @@ -352,4 +352,53 @@ mod tests { assert_eq!(r.elements_left(), 1); assert!(matches!(bool_array(&mut BitReader::with_elements(&[0], 1), 2), Err(PrcError::TooManyElements { .. }))); } + + /// Uncompressed character and short arrays are charged against the allowance too, and a + /// character count past the data is refused by name before anything is read. + #[test] + fn uncompressed_arrays_are_charged_and_their_counts_checked() { + let mut w = W::default(); + w.uint(3).put(0, 8).put(1, 8).put(2, 8); + let chars = w.bytes(); + let read = |n| character_array(&mut BitReader::with_elements(&chars, n), 8, Some(false), false); + assert!(matches!(read(2), Err(PrcError::TooManyElements { .. }))); + assert_eq!(read(3).unwrap(), [0, 1, 2]); + let mut w = W::default(); + w.bit(false).uint(2).put(1, 8).put(0, 8).put(2, 8).put(0, 8); + let shorts = w.bytes(); + assert!(matches!(short_array(&mut BitReader::with_elements(&shorts, 1), 8), Err(PrcError::TooManyElements { .. }))); + assert_eq!(short_array(&mut BitReader::with_elements(&shorts, 2), 8).unwrap(), [1, 2]); + let mut w = W::default(); + w.uint(1000).put(7, 8); + let short_of_data = w.bytes(); + let res = character_array(&mut BitReader::new(&short_of_data), 8, Some(false), false); + assert!(matches!(res, Err(PrcError::Truncated("a character array"))), "{res:?}"); + } + + fn malformed_by(res: Result, PrcError>, what: &str) -> bool { + matches!(res, Err(PrcError::Malformed(m)) if m == what) + } + + #[test] + fn a_zero_code_length_width_or_length_is_refused_by_name() { + let mut w = W::default(); + w.huffman_raw(2, 0, &[(1, 0, 0)], &[]); + let bytes = w.bytes(); + assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code-length width")); + let mut w = W::default(); + w.huffman_raw(2, 2, &[(1, 0, 0)], &[]); + let bytes = w.bytes(); + assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code length")); + } + + #[test] + fn a_boolean_array_past_the_data_is_refused_before_it_is_read() { + let mut w = W::default(); + w.put(0b1011, 4); + let bytes = w.bytes(); + let mut r = BitReader::new(&bytes); + let n = r.remaining() + 1; + assert_eq!(bool_array(&mut r, n), Err(PrcError::Truncated("a boolean array"))); + assert_eq!(r.position(), 0, "nothing read"); + } } diff --git a/crates/prc/src/schema.rs b/crates/prc/src/schema.rs index 02a5cccee4..e418a63742 100644 --- a/crates/prc/src/schema.rs +++ b/crates/prc/src/schema.rs @@ -341,6 +341,14 @@ mod tests { w } + /// A schema 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, 2)), Err(PrcError::TooManyElements { .. }))); + assert!(Schema::read(&mut BitReader::with_elements(&bytes, 3)).is_ok()); + } + fn run(entries: &[(u32, &[u32])], data: &W) -> (Result<(), PrcError>, usize) { let mut w = schema(entries); w.append(data); @@ -523,4 +531,12 @@ mod tests { 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")); + } } diff --git a/crates/prc/src/tess/tests.rs b/crates/prc/src/tess/tests.rs index ad7f497e82..76b1265d53 100644 --- a/crates/prc/src/tess/tests.rs +++ b/crates/prc/src/tess/tests.rs @@ -216,7 +216,9 @@ fn too_many_texture_sets_are_refused_before_anything_is_allocated() { // An empty fan on a face claiming 2^32 - 1 texture sets: no slot bounds the count. let mut b = W::default(); tess_3d(&mut b, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: u32::MAX, ..Mesh::default() }); - assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(_)))); + assert!( + matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == format!("{} texture-coordinate sets on a face", u32::MAX)) + ); let sets = u32::try_from(MAX_TEXTURE_SETS).unwrap(); let mut ok = W::default(); tess_3d(&mut ok, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: sets, ..Mesh::default() }); @@ -425,6 +427,11 @@ fn attributes_names_and_user_data_are_read_past() { /// A one-triangle TESS_3D_Compressed. `full` takes every optional branch: stored normals, /// colours, line attributes, texture, behaviours. Otherwise `three_t` picks the 3T edge form. fn compressed(w: &mut W, v: u32, full: bool, three_t: bool) { + compressed_with_angles(w, v, full, three_t, 10); +} + +/// [`compressed`] whose `full` form stores `angle_bits`-bit normal angles. +fn compressed_with_angles(w: &mut W, v: u32, full: bool, three_t: bool, angle_bits: u8) { w.uint(173).bit(false).bit(true).double(0.001); if v >= 7031 { w.put(0, 32).put(0, 32).put(0, 32); @@ -456,7 +463,7 @@ fn compressed(w: &mut W, v: u32, full: bool, three_t: bool) { w.uint(3).put(0, 3).uint(0); } if full { - w.bit(false).put(10, 8).uint(3).put(0b101, 3); + w.bit(false).put(u64::from(angle_bits), 8).uint(3).put(0b101, 3); w.bit(true).huffman(10, 2, &[(5, 0b10, 2)], &[5, 5]); w.bit(true); // is_face_planar w.bit(true).bit(true).bit(false).uint(5); @@ -586,3 +593,22 @@ fn a_prc_file_yields_its_tessellations() { let t = f.file_structures[0].tessellations().unwrap(); assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2], [0, 2, 3]]); } + +#[test] +fn normal_angles_wider_than_16_bits_are_refused_by_name() { + let mut b = W::default(); + compressed_with_angles(&mut b, 8137, true, true, 17); + assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == "17-bit normal angles")); +} + +/// An entity's counted array is charged against the reader's element allowance. +#[test] +fn counted_entity_arrays_are_charged_against_the_allowance() { + let mut w = W::default(); + w.uint(3).uint(1).uint(2).uint(3); + let bytes = w.bytes(); + let schema = Schema::default(); + let read = |n| Ctx::new(BitReader::with_elements(&bytes, n), &schema, 8137).uints("indices"); + assert!(matches!(read(2), Err(PrcError::TooManyElements { .. }))); + assert_eq!(read(3).unwrap(), [1, 2, 3]); +} diff --git a/crates/prc/src/tree/mod.rs b/crates/prc/src/tree/mod.rs index 28f2c719de..5277dfb874 100644 --- a/crates/prc/src/tree/mod.rs +++ b/crates/prc/src/tree/mod.rs @@ -269,11 +269,21 @@ impl PrcFile { } fn walk(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result<(Vec, Vec), PrcError> { + self.walk_within(rule, scope, MAX_DECODED_ELEMENTS) + } + + /// [`Self::walk`] with `elements` decoded array elements allowed across all its readers. + pub(crate) fn walk_within( + &self, + rule: StyleAlpha, + scope: EntityOverrides, + elements: usize, + ) -> Result<(Vec, Vec), PrcError> { if !self.stores_tree() { return Ok((Vec::new(), Vec::new())); } // One element allowance for the whole walk, so many sections cannot each decode a full one. - let mut left = MAX_DECODED_ELEMENTS; + let mut left = elements; let mut trees = Vec::with_capacity(self.file_structures.len()); for fs in &self.file_structures { let schema = fs.schema()?; diff --git a/crates/prc/src/tree/tests.rs b/crates/prc/src/tree/tests.rs index 7f395c0f3b..2c1b1777cd 100644 --- a/crates/prc/src/tree/tests.rs +++ b/crates/prc/src/tree/tests.rs @@ -398,6 +398,25 @@ fn a_prototype_cycle_is_an_error() { assert!(malformed(single(&[], &occs).model_tree()).contains("own prototype")); } +/// One element allowance covers the whole walk: an allowance that fits one file structure does +/// not fit two, and running short of it is reported as such. +#[test] +fn one_element_allowance_covers_the_whole_walk() { + // A part of 64 items, so each file structure's tree section, not its globals or the model + // file, decodes most of the elements. + let wide = || part(None, PLAIN, 64, |w| (0..64).for_each(|_| poly_item(w, 0, 0, 1, PLAIN))); + let fs = || Fs { globals: palette(), tree: tree(&[wide()], &[Occ { part: 1, ..Occ::default() }]), tess: two_face_square() }; + let one = PrcFile::parse(&prc(&[fs()], &model(&[(0, 1)]))).unwrap(); + let two = PrcFile::parse(&prc(&[fs(), fs()], &model(&[(0, 1), (1, 1)]))).unwrap(); + let walk = |f: &PrcFile, n| f.walk_within(StyleAlpha::default(), EntityOverrides::default(), n); + let short = |f: &PrcFile, n| matches!(walk(f, n), Err(PrcError::TooManyElements { .. })); + // One structure decodes about 70 elements, two about 140. + assert!(short(&one, 32)); + assert_eq!(walk(&one, 100).map(|w| w.0.len()), Ok(64)); + assert!(short(&two, 100), "each structure fits in 100, both do not"); + assert_eq!(walk(&two, 200).map(|w| w.0.len()), Ok(128)); +} + #[test] fn an_exponential_walk_is_cut_off() { // Occurrence k has occurrence k + 1 twice as sons: 2^21 visits. @@ -590,3 +609,32 @@ fn every_truncation_of_a_tree_or_globals_is_an_error_not_a_panic() { } } } + +#[test] +fn a_prototype_chain_past_the_ceiling_is_an_error() { + // Occurrence k (1-based) has occurrence k + 1 as its prototype; none has a part. + let n = u32::try_from(super::MAX_DEPTH).unwrap() + 3; + let occs: Vec<_> = (1..=n).map(|k| Occ { prototype: if k < n { k + 1 } else { 0 }, ..Occ::default() }).collect(); + assert!(malformed(single(&[], &occs).model_tree()).contains("prototype chain too long")); +} + +/// `depth` representation sets, each holding the next, around one poly-B-rep item. +fn nested_sets(w: &mut W, depth: usize) { + if depth == 0 { + return poly_item(w, 0, 0, 1, PLAIN); + } + w.uint(super::RI_SET); + graphics(w, None, 0, PLAIN); + w.uint(0).uint(0).uint(1); + nested_sets(w, depth - 1); + w.uint(0); // user data +} + +#[test] +fn representation_sets_nested_past_the_ceiling_are_an_error() { + let deep = part(None, PLAIN, 1, |w| nested_sets(w, super::MAX_DEPTH + 1)); + let occs = [Occ { part: 1, ..Occ::default() }]; + assert!(malformed(single(&[deep], &occs).placements()).contains("representation sets nest too deep")); + let ok = part(None, PLAIN, 1, |w| nested_sets(w, super::MAX_DEPTH)); + assert_eq!(single(&[ok], &occs).placements().unwrap().len(), 1); +} diff --git a/crates/prc/tests/container.rs b/crates/prc/tests/container.rs index dc9970704b..964474de25 100644 --- a/crates/prc/tests/container.rs +++ b/crates/prc/tests/container.rs @@ -4,7 +4,7 @@ use std::io::Write; -use pdfcraft_prc::{PrcError, PrcFile, SectionKind, UniqueId}; +use pdfcraft_prc::{MAX_FILE_STRUCTURES, PrcError, PrcFile, SectionKind, UniqueId}; fn zlib(data: &[u8]) -> Vec { let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default()); @@ -112,3 +112,15 @@ fn an_offset_or_count_past_the_data_is_an_error_not_a_panic() { bad[67..71].copy_from_slice(&u32::MAX.to_le_bytes()); assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Truncated(_)))); } + +#[test] +fn no_file_structures_or_more_than_the_cap_is_refused_by_name() { + let good = build(SECTIONS, b"m", 0); + // The file-structure count follows the magic, two versions and two ids. + assert_eq!(good[43..47], 1u32.to_le_bytes()); + for n in [0, u32::try_from(MAX_FILE_STRUCTURES).unwrap() + 1] { + let mut bad = good.clone(); + bad[43..47].copy_from_slice(&n.to_le_bytes()); + assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(m)) if m == format!("{n} file structures")), "{n}"); + } +}