mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M8: redaction reads the forms and glyphs it follows with /DecodeParms by reference
To tell which resources content still draws, redaction reads the forms it follows, and the Type 3 glyph programs, with `Stream::decoded`, which reads `/DecodeParms` only when direct. Behind a predictor whose parameters are an indirect object, a form read as the predictor's tag bytes and row differences instead of its operators. So: - the check of what a page still draws (`resource_ops`), the unlinking check (`content_draws`) and the unlinking from surviving forms' resources (`unlink_from_forms`) didn't see such a form draw an image again, and the image went from resources it is still drawn through; - the Type 3 cleanup didn't see such a form, drawn or not, show a glyph, and dropped the glyph's program; - it refused the redaction over a glyph program behind such a predictor, which it couldn't parse. All six reads now go through `Document::with_direct_parms`. Each has a test, the form or glyph behind a predictor whose parameters are given by reference, that fails with that read reverted: - an_image_a_form_with_parameters_by_reference_draws_again_is_kept (`resource_ops` and `content_draws`); - an_image_a_surviving_form_with_parameters_by_reference_draws_is_kept; - a_type3_glyph_a_form_with_parameters_by_reference_shows_is_kept; - a_type3_glyph_an_undrawn_form_with_parameters_by_reference_shows_is_kept; - an_image_a_type3_glyph_with_parameters_by_reference_draws_is_kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
ea51451efc
commit
f227d63ab1
4 files changed
+166
-6
No files matched your search
@@ -49,7 +49,8 @@ fn content_draws(doc: &Document, content: &[Vec<u8>], xo: &Dict, name: &[u8], de
|
||||
if depth >= MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let Ok(data) = s.decoded() else { return None };
|
||||
// Its `/DecodeParms` given by reference are read too.
|
||||
let Ok(data) = doc.with_direct_parms(s).decoded() else { return None };
|
||||
match content_draws(doc, std::slice::from_ref(&data), xo, name, depth + 1, seen) {
|
||||
Some(false) => {}
|
||||
other => return other,
|
||||
@@ -222,7 +223,7 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet<Ob
|
||||
pending.push(res);
|
||||
continue;
|
||||
}
|
||||
let data = form.decoded()?;
|
||||
let data = doc.with_direct_parms(form).decoded()?;
|
||||
let mut pruned = prune_doomed_xobjects(doc, std::slice::from_ref(&data), &mut xo, &doomed_names)?;
|
||||
if pruned {
|
||||
res.set(b"XObject".to_vec(), Object::Dict(xo));
|
||||
@@ -272,6 +273,11 @@ pub(super) fn clip_out(rects: &[[f64; 4]], ctm: &Matrix, around: [f64; 4]) -> Op
|
||||
Some(ops)
|
||||
}
|
||||
|
||||
/// The operators of `content`, and of every form without /Resources of its own that it draws
|
||||
/// through `res`, nested ones too: what draws with these resources. Each such form is read once,
|
||||
/// with its `/DecodeParms` given by reference. An error when that can't all be read: the parser
|
||||
/// can't read all of `content` or of such a form, the walk passes 2,000,000 operators, or such a
|
||||
/// form doesn't decode.
|
||||
pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
|
||||
let parsed = Pieces::join(content).parse();
|
||||
if parsed.skipped > 0 || parsed.ops.len() > 2_000_000 {
|
||||
@@ -290,7 +296,7 @@ pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> R
|
||||
&& s.dict.name(b"Subtype") == Some(b"Form")
|
||||
&& s.dict.get(b"Resources").is_none_or(|r| doc.resolve(r).as_dict().is_none())
|
||||
{
|
||||
let parsed = pdfcraft_content::parse(&s.decoded()?);
|
||||
let parsed = pdfcraft_content::parse(&doc.with_direct_parms(s).decoded()?);
|
||||
if seen.len() > 2_000_000 || ops.len().saturating_add(parsed.ops.len()) > 2_000_000 || parsed.skipped > 0 {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect inherited form resources".into() });
|
||||
}
|
||||
|
||||
@@ -1771,6 +1771,76 @@ fn what_content_draws_through_its_resources_is_read_up_to_two_million_operators(
|
||||
}
|
||||
}
|
||||
|
||||
/// `content` behind a PNG predictor (Up, one column) whose `/DecodeParms` are given by reference,
|
||||
/// in a stream with `entries` too: read without the parameters, its bytes are the predictor's tags
|
||||
/// and differences, not the operators.
|
||||
fn behind_parms_by_reference(doc: &mut Document, entries: &[(&str, Object)], content: &[u8]) -> Stream {
|
||||
let mut rows = Vec::new();
|
||||
let mut above = 0u8;
|
||||
for &b in content {
|
||||
rows.extend([2, b.wrapping_sub(above)]);
|
||||
above = b;
|
||||
}
|
||||
let parms = doc.add(Object::Dict(Dict::from_iter([(b"Predictor".to_vec(), Object::Int(12))])));
|
||||
let mut s = Stream::flate(Dict::new(), &rows);
|
||||
for (key, value) in entries {
|
||||
s.dict.set(key.as_bytes().to_vec(), value.clone());
|
||||
}
|
||||
s.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms));
|
||||
s
|
||||
}
|
||||
|
||||
/// A form's entries: `/Subtype /Form` and `bbox`.
|
||||
fn form_entries(bbox: [i64; 4]) -> Vec<(&'static str, Object)> {
|
||||
vec![("Subtype", Object::name("Form")), ("BBox", Object::Array(bbox.into_iter().map(Object::Int).collect()))]
|
||||
}
|
||||
|
||||
/// A form outside the mark draws the image again through the page's resources, its content behind
|
||||
/// a predictor whose `/DecodeParms` are given by reference. Both checks of whether the page still
|
||||
/// draws it read the form through them, the page's (`resource_ops`) and the unlinking one at the
|
||||
/// end (`content_draws`): the page keeps the image for the form.
|
||||
#[test]
|
||||
fn an_image_a_form_with_parameters_by_reference_draws_again_is_kept() {
|
||||
let mut doc = one_page(
|
||||
b"q 50 0 0 50 20 230 cm /Im1 Do Q /FmBare Do",
|
||||
"/XObject << /FmBare 7 0 R /Im1 8 0 R >>",
|
||||
vec![
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 100 100]", b""),
|
||||
stream("/Type /XObject /Subtype /Image /Width 16 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"PUBIMGSHOWNAGAIN"),
|
||||
],
|
||||
);
|
||||
let form = behind_parms_by_reference(&mut doc, &form_entries([0, 0, 100, 100]), b"q 50 0 0 50 20 20 cm /Im1 Do Q");
|
||||
doc.set(ObjRef::new(7, 0), form);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
let r = apply(&mut doc, None).unwrap();
|
||||
assert_eq!(r.images_removed, 1, "{r:?}");
|
||||
let saved = Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap();
|
||||
assert_eq!(named_image(&saved, 0, b"Im1").as_deref(), Some(&b"PUBIMGSHOWNAGAIN"[..]), "the form still draws it");
|
||||
}
|
||||
|
||||
/// A surviving form outside the mark shares the page's resources and draws the image itself, its
|
||||
/// content behind a predictor whose `/DecodeParms` are given by reference: unlinking the redacted
|
||||
/// image from the forms' resources reads it through them, and keeps the image the form draws.
|
||||
#[test]
|
||||
fn an_image_a_surviving_form_with_parameters_by_reference_draws_is_kept() {
|
||||
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"q 100 0 0 100 0 0 cm /Im Do Q /Fm Do"),
|
||||
b"<< /XObject << /Im 6 0 R /Fm 7 0 R >> >>".to_vec(),
|
||||
stream("/Type /XObject /Subtype /Image /Width 11 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"IMAGESECRET"),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [150 150 300 300]", b""),
|
||||
]);
|
||||
let mut entries = form_entries([150, 150, 300, 300]);
|
||||
entries.push(("Resources", Object::Ref(ObjRef::new(5, 0))));
|
||||
let form = behind_parms_by_reference(&mut doc, &entries, b"q 50 0 0 50 200 200 cm /Im Do Q");
|
||||
doc.set(ObjRef::new(7, 0), form);
|
||||
mark(&mut doc, 0, &[[0.0, 0.0, 120.0, 120.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().images_removed, 1);
|
||||
assert!(surviving_references::full_save_holds(&doc, b"IMAGESECRET"), "the image the form still draws outside the mark");
|
||||
}
|
||||
|
||||
/// A form with resources of its own draws through those, not its caller's: one on another page
|
||||
/// that names a different image /Im1 doesn't keep the redacted /Im1 in the dictionary both pages
|
||||
/// inherit.
|
||||
|
||||
@@ -298,3 +298,87 @@ fn type3_font_used_in_a_form_xobject_has_its_redacted_glyph_removed() {
|
||||
assert_eq!(report.glyphs, 1);
|
||||
assert!(!full_save_holds(&doc, b"SECRET_FORM_GLYPH_PAYLOAD"), "redacted glyph program survived");
|
||||
}
|
||||
|
||||
/// A Type 3 font with glyphs A (its program object 7) and B (object 8), at object 6.
|
||||
const TYPE3: &str = "<< /Type /Font /Subtype /Type3 /FontBBox [0 -200 1000 800] /FontMatrix [0.001 0 0 0.001 0 0] /FirstChar 65 /LastChar 66 /Widths [500 500] /Encoding << /Differences [65 /A /B] >> /CharProcs << /A 7 0 R /B 8 0 R >> >>";
|
||||
|
||||
/// A form outside the mark shows glyph B of a Type 3 font, its content behind a predictor whose
|
||||
/// `/DecodeParms` are given by reference: the font's cleanup reads it through them, and keeps B.
|
||||
#[test]
|
||||
fn a_type3_glyph_a_form_with_parameters_by_reference_shows_is_kept() {
|
||||
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]", b""),
|
||||
]);
|
||||
let mut entries = super::form_entries([0, 0, 300, 50]);
|
||||
entries.push(("Resources", Object::Ref(ObjRef::new(5, 0))));
|
||||
let form = super::behind_parms_by_reference(&mut doc, &entries, b"BT /T3 10 Tf 10 20 Td (B) Tj ET");
|
||||
doc.set(ObjRef::new(9, 0), form);
|
||||
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1);
|
||||
assert!(!full_save_holds(&doc, b"SECRET_TYPE3_GLYPH_A"), "the redacted glyph program survived");
|
||||
assert!(full_save_holds(&doc, b"PUBLIC_TYPE3_GLYPH_B"), "the form still shows B");
|
||||
}
|
||||
|
||||
/// A form the page lists but doesn't draw shows glyph B, its content behind a predictor whose
|
||||
/// `/DecodeParms` are given by reference: the font's cleanup reads every surviving form, this one
|
||||
/// through them, and keeps B.
|
||||
#[test]
|
||||
fn a_type3_glyph_an_undrawn_form_with_parameters_by_reference_shows_is_kept() {
|
||||
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"),
|
||||
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]", b""),
|
||||
]);
|
||||
let mut entries = super::form_entries([0, 0, 300, 50]);
|
||||
entries.push((
|
||||
"Resources",
|
||||
Object::Dict(Dict::from_iter([(b"Font".to_vec(), Object::Dict(Dict::from_iter([(b"T3".to_vec(), Object::Ref(ObjRef::new(6, 0)))])))])),
|
||||
));
|
||||
let form = super::behind_parms_by_reference(&mut doc, &entries, b"BT /T3 10 Tf 10 20 Td (B) Tj ET");
|
||||
doc.set(ObjRef::new(9, 0), form);
|
||||
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1);
|
||||
assert!(!full_save_holds(&doc, b"SECRET_TYPE3_GLYPH_A"), "the redacted glyph program survived");
|
||||
assert!(full_save_holds(&doc, b"PUBLIC_TYPE3_GLYPH_B"), "the undrawn form still shows B");
|
||||
}
|
||||
|
||||
/// Glyph B's program draws an image from the font's resources, behind a predictor whose
|
||||
/// `/DecodeParms` are given by reference: the font's cleanup reads it through them, and keeps the
|
||||
/// image B still draws while the one only the redacted A drew goes.
|
||||
#[test]
|
||||
fn an_image_a_type3_glyph_with_parameters_by_reference_draws_is_kept() {
|
||||
let image = "/Type /XObject /Subtype /Image /Width 12 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8";
|
||||
let font = TYPE3.replace("/CharProcs", "/Resources << /XObject << /ImA 9 0 R /ImB 10 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"/ImA Do"),
|
||||
stream("", b""),
|
||||
stream(image, b"SECRET_IMG_A"),
|
||||
stream(image, b"PUBLIC_IMG_B"),
|
||||
]);
|
||||
let glyph = super::behind_parms_by_reference(&mut doc, &[], b"/ImB Do");
|
||||
doc.set(ObjRef::new(8, 0), glyph);
|
||||
mark(&mut doc, 0, &[[9.0, 95.0, 16.0, 110.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1);
|
||||
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");
|
||||
}
|
||||
@@ -327,7 +327,7 @@ fn scan_xobject(
|
||||
if !ctx.seen_forms.insert((r, current_font, through)) {
|
||||
return Ok(());
|
||||
}
|
||||
let data = s.decoded()?;
|
||||
let data = ctx.doc.with_direct_parms(s).decoded()?;
|
||||
match form_own_res {
|
||||
Some(form_res) => scan_content_stream(ctx, std::slice::from_ref(&data), r, &form_res, current_font, depth + 1),
|
||||
None => scan_content_stream(ctx, std::slice::from_ref(&data), holder_ref, res, current_font, depth + 1),
|
||||
@@ -482,7 +482,7 @@ pub(crate) fn unlink_type3(doc: &mut Document) -> Result<(), RedactError> {
|
||||
for (form_ref, form_res) in &form_holders {
|
||||
if !ctx.seen_forms.iter().any(|(form, _, _)| form == form_ref) {
|
||||
let Object::Stream(s) = &*ctx.doc.get(*form_ref) else { continue };
|
||||
let data = s.decoded()?;
|
||||
let data = ctx.doc.with_direct_parms(s).decoded()?;
|
||||
scan_content_stream(&mut ctx, std::slice::from_ref(&data), *form_ref, form_res, None, 0)?;
|
||||
}
|
||||
}
|
||||
@@ -616,7 +616,7 @@ fn clean_type3_font(
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let data = s.decoded()?;
|
||||
let data = doc.with_direct_parms(s).decoded()?;
|
||||
collect_used_resources_in_stream(&data, &mut used_in_glyphs)?;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user