M8: tests pin the Type 3 cleanup's refusal of bytes it can't read

The Type 3 cleanup refuses a redaction when its scan of a page or form,
or its read of a kept glyph's program, meets bytes the parser can't
read. Nothing tested either refusal: cargo-mutants found both checks
could be dropped with every test passing, once 8bf63c879 moved them
after the loop.

unreadable_bytes_in_a_form_the_type3_cleanup_scans_refuse_the_redaction
leaves a string open in a form only that scan reads (resources of its
own, outside the mark), hiding the glyph the form shows after it, and
unreadable_bytes_in_a_kept_type3_glyph_program_refuse_the_redaction
leaves one open in the program of the glyph the page keeps, hiding the
image it draws. Each fails with its check disabled; with the checks off,
the glyph loses its program and the font its image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz Martens
2026-11-06 18:11:07 +01:00
co-authored by Claude Opus 5.5
parent 8bf63c8792
commit 20e138fbf5
2 changed files with 54 additions and 4 deletions
@@ -382,3 +382,52 @@ fn an_image_a_type3_glyph_with_parameters_by_reference_draws_is_kept() {
assert!(!full_save_holds(&doc, b"SECRET_IMG_A"), "the image only the redacted glyph drew survived");
assert!(full_save_holds(&doc, b"PUBLIC_IMG_B"), "glyph B still draws its image");
}
/// Whether `out` is the redaction refused with a syntax error whose detail holds `detail`.
fn refused_for(out: &Result<Report, RedactError>, detail: &str) -> bool {
matches!(out, Err(RedactError::Cos(pdfcraft_cos::CosError::Syntax { detail: d, .. })) if d.contains(detail))
}
/// The font's cleanup scans every page and form for the glyphs they show. A string left open in a
/// form nothing else reads (resources of its own, outside the mark) runs to the end of the
/// stream, hiding the glyph the form shows after it, so the redaction is refused: with the check
/// off, that glyph loses its program.
#[test]
fn unreadable_bytes_in_a_form_the_type3_cleanup_scans_refuse_the_redaction() {
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"BT /T3 10 Tf 10 100 Td (A) Tj ET /Fm Do"),
b"<< /Font << /T3 6 0 R >> /XObject << /Fm 9 0 R >> >>".to_vec(),
TYPE3.as_bytes().to_vec(),
stream("", b"% SECRET_TYPE3_GLYPH_A"),
stream("", b"% PUBLIC_TYPE3_GLYPH_B"),
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 50] /Resources << /Font << /T3 6 0 R >> >>", b"BT /T3 10 Tf 10 20 Td ( (B) Tj ET"),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let out = apply(&mut doc, None);
assert!(refused_for(&out, "content stream syntax"), "{out:?}");
}
/// The font's cleanup reads the program of each glyph it keeps, for the resources it uses. A
/// string left open there runs to the end of the program, hiding the image it draws after it, so
/// the redaction is refused: with the check off, the font's resources lose that image.
#[test]
fn unreadable_bytes_in_a_kept_type3_glyph_program_refuse_the_redaction() {
let font = TYPE3.replace("/CharProcs", "/Resources << /XObject << /ImB 9 0 R >> >> /CharProcs");
let mut doc = pdf(vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources 5 0 R >>".to_vec(),
stream("", b"BT /T3 10 Tf 10 100 Td (A) Tj 0 100 Td (B) Tj ET"),
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
font.into_bytes(),
stream("", b"% SECRET_TYPE3_GLYPH_A"),
stream("", b"500 0 d0 ( /ImB Do"),
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"o"),
]);
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
let out = apply(&mut doc, None);
assert!(refused_for(&out, "glyph stream syntax"), "{out:?}");
}
+5 -4
View File
@@ -1,6 +1,7 @@
//! Memory regressions for reading the forms a redaction follows through resources: their
//! operators are read one at a time, so a form of millions of them is never held whole. A test
//! binary of its own, so that the one test is all the process runs while its peak is measured.
//! Memory regressions for the streams a redaction reads besides its walk: the forms it follows
//! through resources, and what the Type 3 cleanup scans and reads. Their operators are read one
//! at a time, so a stream of millions of them is never held whole. A test binary of its own, so
//! that the one test is all the process runs while its peak is measured.
use std::sync::Arc;
@@ -80,7 +81,7 @@ fn type3_page(form: &[u8], glyph: &[u8]) -> Document {
/// glyphs it shows;
/// - the Type 3 cleanup reads the program of a glyph it keeps, for the resources it uses.
///
/// Each parsed every operator first: the peak grew by 4,415 and 2,527 MiB in the first two. Now
/// Each parsed every operator first: the peak grew by 4,415, 2,526, 2,526 and 2,494 MiB. Now
/// the first holds at most one operator past the walk's limit, and the others none.
#[test]
fn the_forms_followed_through_resources_are_read_one_operator_at_a_time() {