mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
fix(redact): prune the resources of Forms a surviving Type 3 glyph draws (#5924)
This commit is contained in:
1 parent
eaeffa748b
commit
a55eaa90e3
2 files changed
+100
-1
No files matched your search
@@ -161,6 +161,30 @@ fn redacted_type3_glyph_and_exclusive_resources_do_not_survive_full_save() {
|
||||
assert!(t3_xo.contains(b"ImB"), "Type 3 font resources lost surviving image ImB");
|
||||
}
|
||||
|
||||
/// The surviving glyph B draws a Form whose own resources also name A's image, which the Form
|
||||
/// never draws. Pruning only the font's resources left that entry, so the image of the removed
|
||||
/// glyph stayed in the file through the Form.
|
||||
#[test]
|
||||
fn a_redacted_type3_glyphs_image_does_not_survive_in_a_surviving_glyphs_form() {
|
||||
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 (AB) Tj ET"),
|
||||
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
|
||||
b"<< /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 >> /Resources << /XObject << /ImA 9 0 R /Fm 10 0 R >> >> >>".to_vec(),
|
||||
stream("", b"/ImA Do"),
|
||||
stream("", b"/Fm Do"),
|
||||
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"CONFIDENTIAL_IMAGE_A"),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 1000 1000] /Resources << /XObject << /ImA 9 0 R /ImB 11 0 R >> >>", b"/ImB Do"),
|
||||
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"PUBLIC_IMAGE_B"),
|
||||
]);
|
||||
mark(&mut doc, 0, &[[9.0, 95.0, 14.9, 110.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).unwrap().glyphs, 1);
|
||||
assert!(!full_save_holds(&doc, b"CONFIDENTIAL_IMAGE_A"), "the removed glyph's image survives through the surviving glyph's form");
|
||||
assert!(full_save_holds(&doc, b"PUBLIC_IMAGE_B"), "the image the surviving glyph's form draws was lost");
|
||||
}
|
||||
|
||||
/// When all glyphs of a Type 3 font are redacted, the entire font is removed from Resources (F-011).
|
||||
#[test]
|
||||
fn all_redacted_type3_glyphs_cause_font_removal_from_resources() {
|
||||
@@ -298,3 +322,23 @@ 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 Form the surviving glyph draws holds `/Resources` as an array that references the removed
|
||||
/// glyph's image: it can't be pruned, so the redaction refuses rather than keep the image.
|
||||
#[test]
|
||||
fn a_type3_glyphs_form_with_non_dictionary_resources_fails_closed() {
|
||||
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 (AB) Tj ET"),
|
||||
b"<< /Font << /T3 6 0 R >> >>".to_vec(),
|
||||
b"<< /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 >> /Resources << /XObject << /ImA 9 0 R /Fm 10 0 R >> >> >>".to_vec(),
|
||||
stream("", b"/ImA Do"),
|
||||
stream("", b"/Fm Do"),
|
||||
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"CONFIDENTIAL_IMAGE_A"),
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 1000 1000] /Resources [9 0 R]", b""),
|
||||
]);
|
||||
mark(&mut doc, 0, &[[9.0, 95.0, 14.9, 110.0]], "");
|
||||
assert!(apply(&mut doc, None).is_err());
|
||||
}
|
||||
@@ -135,7 +135,13 @@ fn collect_used_resources_in_stream(data: &[u8], used: &mut UsedInGlyphs) -> Res
|
||||
if parsed.skipped > 0 {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect glyph stream syntax".into() });
|
||||
}
|
||||
for op in parsed.ops {
|
||||
parsed.ops.iter().for_each(|op| record_used(op, used));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record the resource name `op` uses, if any.
|
||||
fn record_used(op: &pdfcraft_content::Op, used: &mut UsedInGlyphs) {
|
||||
{
|
||||
match op.op.as_slice() {
|
||||
b"Do" => {
|
||||
if let Some(n) = op.last_name() {
|
||||
@@ -175,6 +181,54 @@ fn collect_used_resources_in_stream(data: &[u8], used: &mut UsedInGlyphs) -> Res
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prune the own `/Resources` of every Form a Type 3 font's (already pruned) resources still
|
||||
/// list, and of the Forms those list in turn, to the names each Form's content uses. A Form a
|
||||
/// surviving glyph draws may name, unused, an image only a removed glyph drew; that entry would
|
||||
/// keep the image in the file. Pruning only unused entries is safe for a shared Form: no drawer
|
||||
/// of it can use a name its content doesn't. Fails closed when a Form can't be read.
|
||||
fn prune_font_forms(doc: &mut Document, font_res: &Dict) -> Result<(), CosError> {
|
||||
let mut pending: Vec<(ObjRef, usize)> = res_dict(doc, font_res, b"XObject").iter().filter_map(|(_, v)| v.as_ref()).map(|r| (r, 0)).collect();
|
||||
let mut seen = HashSet::new();
|
||||
while let Some((form_ref, depth)) = pending.pop() {
|
||||
if !seen.insert(form_ref) {
|
||||
continue;
|
||||
}
|
||||
if depth > MAX_DEPTH || seen.len() > MAX_HOLDERS {
|
||||
return Err(CosError::Syntax { offset: 0, detail: "redaction could not establish safe Type 3 font cleanup: forms nest too deep".into() });
|
||||
}
|
||||
let Object::Stream(form) = &*doc.get(form_ref) else { continue };
|
||||
if form.dict.name(b"Subtype") != Some(b"Form") {
|
||||
continue;
|
||||
}
|
||||
// A Form without resources of its own (no key, or `null`) draws through its caller's,
|
||||
// which were pruned already. Any other value that isn't a dictionary may still reference
|
||||
// what a removed glyph drew, and can't be pruned: fail closed.
|
||||
let mut res = match form.dict.get(b"Resources").map(|r| doc.resolve(r)) {
|
||||
None => continue,
|
||||
Some(r) if matches!(*r, Object::Null) => continue,
|
||||
Some(r) => match r.as_dict() {
|
||||
Some(d) => d.clone(),
|
||||
None => {
|
||||
return Err(CosError::Syntax {
|
||||
offset: 0,
|
||||
detail: "redaction could not establish safe Type 3 font cleanup: a form's /Resources is not a dictionary".into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
let data = form.decoded()?;
|
||||
let mut used = UsedInGlyphs::default();
|
||||
crate::interp::resource_ops(doc, &res, std::slice::from_ref(&data))?.iter().for_each(|op| record_used(op, &mut used));
|
||||
// `rg`, `g` and `k` draw through these entries when the Form keeps them as its defaults,
|
||||
// and nothing cites them with `cs`/`CS`: pruning them would change the Form's colours.
|
||||
used.colorspaces.extend([b"DefaultRGB".to_vec(), b"DefaultGray".to_vec(), b"DefaultCMYK".to_vec()]);
|
||||
if prune_type3_font_resources(doc, &mut res, &used) {
|
||||
doc.update_dict(form_ref, |d| d.set(b"Resources".to_vec(), Object::Dict(res.clone())))?;
|
||||
}
|
||||
pending.extend(res_dict(doc, &res, b"XObject").iter().filter_map(|(_, v)| v.as_ref()).map(|r| (r, depth + 1)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -631,6 +685,7 @@ fn clean_type3_font(
|
||||
}
|
||||
})?;
|
||||
}
|
||||
prune_font_forms(doc, &f_res)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user