mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 13:24:35 +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 {
|
if depth >= MAX_DEPTH {
|
||||||
return None;
|
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) {
|
match content_draws(doc, std::slice::from_ref(&data), xo, name, depth + 1, seen) {
|
||||||
Some(false) => {}
|
Some(false) => {}
|
||||||
other => return other,
|
other => return other,
|
||||||
@@ -222,7 +223,7 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet<Ob
|
|||||||
pending.push(res);
|
pending.push(res);
|
||||||
continue;
|
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)?;
|
let mut pruned = prune_doomed_xobjects(doc, std::slice::from_ref(&data), &mut xo, &doomed_names)?;
|
||||||
if pruned {
|
if pruned {
|
||||||
res.set(b"XObject".to_vec(), Object::Dict(xo));
|
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)
|
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> {
|
pub(crate) fn resource_ops(doc: &Document, res: &Dict, content: &[Vec<u8>]) -> Result<Vec<Op>, CosError> {
|
||||||
let parsed = Pieces::join(content).parse();
|
let parsed = Pieces::join(content).parse();
|
||||||
if parsed.skipped > 0 || parsed.ops.len() > 2_000_000 {
|
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.name(b"Subtype") == Some(b"Form")
|
||||||
&& s.dict.get(b"Resources").is_none_or(|r| doc.resolve(r).as_dict().is_none())
|
&& 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 {
|
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() });
|
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
|
/// 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
|
/// that names a different image /Im1 doesn't keep the redacted /Im1 in the dictionary both pages
|
||||||
/// inherit.
|
/// inherit.
|
||||||
|
|||||||
@@ -298,3 +298,87 @@ fn type3_font_used_in_a_form_xobject_has_its_redacted_glyph_removed() {
|
|||||||
assert_eq!(report.glyphs, 1);
|
assert_eq!(report.glyphs, 1);
|
||||||
assert!(!full_save_holds(&doc, b"SECRET_FORM_GLYPH_PAYLOAD"), "redacted glyph program survived");
|
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)) {
|
if !ctx.seen_forms.insert((r, current_font, through)) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let data = s.decoded()?;
|
let data = ctx.doc.with_direct_parms(s).decoded()?;
|
||||||
match form_own_res {
|
match form_own_res {
|
||||||
Some(form_res) => scan_content_stream(ctx, std::slice::from_ref(&data), r, &form_res, current_font, depth + 1),
|
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),
|
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 {
|
for (form_ref, form_res) in &form_holders {
|
||||||
if !ctx.seen_forms.iter().any(|(form, _, _)| form == form_ref) {
|
if !ctx.seen_forms.iter().any(|(form, _, _)| form == form_ref) {
|
||||||
let Object::Stream(s) = &*ctx.doc.get(*form_ref) else { continue };
|
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)?;
|
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());
|
.into());
|
||||||
};
|
};
|
||||||
let data = s.decoded()?;
|
let data = doc.with_direct_parms(s).decoded()?;
|
||||||
collect_used_resources_in_stream(&data, &mut used_in_glyphs)?;
|
collect_used_resources_in_stream(&data, &mut used_in_glyphs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user