mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 05:42:52 +00:00
Merge pull request #6860 from Piohrun/fix/redact-type3-check-first
M8: the Type 3 cleanup charges the page only when there is a Type 3 font
This commit is contained in:
3 files changed
+133
-29
No files matched your search
@@ -9,8 +9,9 @@ use super::{
|
||||
/// by a form without /Resources of its own, which draws through `xo` too. Such forms are read
|
||||
/// within `budget`, what is left of the page's allowance ([`resource_ops`]). Fails, as the cleanup
|
||||
/// of the fonts and properties these forms draw with does, when that can't be told: such a form
|
||||
/// doesn't decode (an image codec or a /Filter it can't run, say) or runs past `budget`, the walk
|
||||
/// passes 2,000,000 operators, or the parser can't read all of such a form or of `content`.
|
||||
/// doesn't decode (an image codec or a /Filter it can't run, say) or runs past `budget` (for the
|
||||
/// fonts' cleanup, an allowance of its own when no Type 3 font is listed), the walk passes
|
||||
/// 2,000,000 operators, or the parser can't read all of such a form or of `content`.
|
||||
/// Corrupt data in such a form is read, tolerantly, up to the damage, which may be nothing.
|
||||
pub(crate) fn prune(doc: &Document, budget: &mut DecodeBudget, xo: &mut Dict, replaced: &[Vec<u8>], content: &[Vec<u8>]) -> Result<(), CosError> {
|
||||
if replaced.is_empty() {
|
||||
@@ -473,7 +474,8 @@ pub(super) fn clip_out(rects: &[[f64; 4]], ctm: &Matrix, around: [f64; 4]) -> Op
|
||||
/// 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, its decoded bytes charged to `budget`, so the
|
||||
/// cleanups of one page that walk the same forms spend one allowance between them. An error
|
||||
/// cleanups of one page that walk the same forms spend one allowance between them (all but the
|
||||
/// fonts' cleanup when no Type 3 font is listed, which has one of its own). 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 or runs past `budget`. Operators are
|
||||
/// read one at a time, so the walk stops at one past the limit rather than holding them all.
|
||||
|
||||
+101
-13
@@ -3571,14 +3571,43 @@ fn what_content_draws_through_its_resources_is_read_within_its_allowance() {
|
||||
}
|
||||
}
|
||||
|
||||
/// An unused Type 3 font, listed as /T3 beside /F1 in the `/Font` of `holder`'s resources (a page's
|
||||
/// or a form's), so that the font cleanup, which prunes only Type 3 fonts, charges its read of what
|
||||
/// they draw to the page's allowance (without one it has an allowance of its own).
|
||||
fn list_type3(doc: &mut Document, holder: ObjRef) {
|
||||
let font = Object::Dict(Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("Font")),
|
||||
(b"Subtype".to_vec(), Object::name("Type3")),
|
||||
(b"FontBBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 1000.into(), 1000.into()])),
|
||||
(b"FontMatrix".to_vec(), Object::Array(vec![Object::Real(0.001), 0.into(), 0.into(), Object::Real(0.001), 0.into(), 0.into()])),
|
||||
(b"CharProcs".to_vec(), Object::Dict(Dict::new())),
|
||||
]));
|
||||
let font = Object::Ref(doc.add(font));
|
||||
let list = |d: &mut Dict| {
|
||||
let Some(Object::Dict(res)) = d.get_mut(b"Resources") else { panic!("{holder:?} has resources") };
|
||||
let Some(Object::Dict(fonts)) = res.get_mut(b"Font") else { panic!("{holder:?} lists fonts") };
|
||||
fonts.set(b"T3".to_vec(), font);
|
||||
};
|
||||
match &*doc.get(holder) {
|
||||
Object::Stream(s) => {
|
||||
let mut s = s.clone();
|
||||
list(&mut s.dict);
|
||||
doc.set(holder, Object::Stream(s));
|
||||
}
|
||||
_ => doc.update_dict(holder, list).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The cleanups after the content walk spend what the walk left of the page's allowance, one
|
||||
/// allowance between them. A form without resources of its own that crosses the mark is read by
|
||||
/// the walk, then again by the check of what the page still draws and by the font cleanup: at
|
||||
/// 100 MiB, three reads are more than the page's 256 MiB, and the redaction is refused; at
|
||||
/// 50 MiB it goes on.
|
||||
/// the walk, then again by the check of what the page still draws and by the font cleanup, which
|
||||
/// charges the page's allowance when the page lists a Type 3 font: at 100 MiB, three reads are more
|
||||
/// than the page's 256 MiB, and the redaction is refused; at 50 MiB it goes on. With no Type 3 font
|
||||
/// the font cleanup's read has an allowance of its own, so the page's pays for two reads of
|
||||
/// 100 MiB, and the redaction goes on.
|
||||
#[test]
|
||||
fn the_cleanups_after_the_content_walk_spend_what_it_left_of_the_pages_allowance() {
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
for (mib, type3, refused) in [(100, true, true), (50, true, false), (100, false, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"q 50 0 0 50 20 230 cm /Im1 Do Q /FmBare Do",
|
||||
@@ -3588,19 +3617,70 @@ fn the_cleanups_after_the_content_walk_spend_what_it_left_of_the_pages_allowance
|
||||
stream("/Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", b"o"),
|
||||
],
|
||||
);
|
||||
if type3 {
|
||||
list_type3(&mut doc, ObjRef::new(3, 0));
|
||||
}
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB, Type 3: {type3}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Text under the mark in a content stream that holds bytes the parser can't read, inside a `TJ`
|
||||
/// array, is read past by the walk and would stay. The font cleanup's read of what the content
|
||||
/// draws refuses such bytes in the streams and forms the walk leaves as they were, and makes that
|
||||
/// read with any font listed, a Type 3 font or not (with none, on an allowance of its own). Here
|
||||
/// the walk rewrites the text in the page's first stream and leaves the broken `TJ` as it was:
|
||||
/// Apply Redactions refuses the page, whether that `TJ` is in the page's second stream or in a
|
||||
/// form without resources of its own, and so does Remove Hidden Information. The cleanups run only
|
||||
/// when the walk rewrites some of the page, and read what it rewrote as rewritten, so such bytes in
|
||||
/// a stream it rewrites still go by, and so do those on a page it rewrites nothing of, unless the
|
||||
/// document still lists a Type 3 font (`unlink_type3` then reads every page).
|
||||
#[test]
|
||||
fn text_beside_bytes_the_parser_cannot_read_refuses_the_redaction() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
let page_of = |second: &[u8]| {
|
||||
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 5 0 R] /Resources << /Font << /F1 6 0 R >> >> >>".to_vec(),
|
||||
stream("", b"BT /F1 10 Tf 20 230 Td (SECRET) Tj ET"),
|
||||
stream("", second),
|
||||
FONT.replace("95 0 R", "7 0 R").into_bytes(),
|
||||
widths(),
|
||||
])
|
||||
};
|
||||
for tail in [
|
||||
&b"BT /F1 10 Tf 20 260 Td [(TAIL) ) (SECRET)] TJ ET"[..],
|
||||
b"BT /F1 10 Tf 20 260 Td [(TAIL) } (SECRET)] TJ ET",
|
||||
b"BT /F1 10 Tf 20 260 Td [(TAILSECRET) )] TJ ET",
|
||||
b"BT /F1 10 Tf 20 260 Td [) (TAILSECRET)] TJ ET",
|
||||
] {
|
||||
let mut doc = page_of(tail);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert!(apply(&mut doc, None).is_err(), "{}", String::from_utf8_lossy(tail));
|
||||
}
|
||||
let mut doc = one_page(
|
||||
b"BT /F1 10 Tf 20 230 Td (SECRET) Tj ET /FmBare Do",
|
||||
"/XObject << /FmBare 7 0 R >>",
|
||||
vec![stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300]", b"BT /F1 10 Tf 20 260 Td [(TAIL) ) (SECRET)] TJ ET")],
|
||||
);
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert!(apply(&mut doc, None).is_err(), "a form's text");
|
||||
let mut doc = page_of(b"BT /F1 10 Tf 3 Tr 20 260 Td [(TAIL) ) (HIDDEN)] TJ ET");
|
||||
doc.set(ObjRef::new(4, 0), Object::Stream(Stream::from_raw(Dict::new(), b"BT /F1 10 Tf 3 Tr 20 230 Td (HIDDEN) Tj ET".to_vec())));
|
||||
assert!(remove_hidden(&mut doc, &[Hidden::HiddenText]).is_err(), "hidden text");
|
||||
}
|
||||
|
||||
/// The cleanups of a form the redaction rewrites spend the page's allowance too. The rewritten
|
||||
/// form has resources of its own, so the page's cleanups don't follow it, and draws through them a
|
||||
/// form without any that crosses the mark: the walk reads that form, then the check of what the
|
||||
/// rewritten form still draws and its font cleanup read it again. At 100 MiB, three reads are more
|
||||
/// than the page's 256 MiB, and the redaction is refused; at 50 MiB it goes on.
|
||||
/// rewritten form still draws and its font cleanup (when it lists a Type 3 font) read it again. At
|
||||
/// 100 MiB, three reads are more than the page's 256 MiB, and the redaction is refused; at 50 MiB
|
||||
/// it goes on. With no Type 3 font the font cleanup's read has an allowance of its own, so the
|
||||
/// page's pays for two reads of 100 MiB, and the redaction goes on.
|
||||
#[test]
|
||||
fn the_cleanups_of_a_rewritten_form_spend_the_pages_allowance() {
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
for (mib, type3, refused) in [(100, true, true), (50, true, false), (100, false, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"/FmText Do",
|
||||
@@ -3613,20 +3693,25 @@ fn the_cleanups_of_a_rewritten_form_spend_the_pages_allowance() {
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Filter /FlateDecode", &blank),
|
||||
],
|
||||
);
|
||||
if type3 {
|
||||
list_type3(&mut doc, ObjRef::new(7, 0));
|
||||
}
|
||||
mark(&mut doc, 0, &[[0.0, 200.0, 300.0, 300.0]], "");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB");
|
||||
assert_eq!(apply(&mut doc, None).is_err(), refused, "{mib} MiB, Type 3: {type3}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Removing hidden text spends what its content walk left of the page's allowance too. The page
|
||||
/// draws a form holding hidden text, which is rewritten, and a form without resources of its own:
|
||||
/// the walk reads the second, then the check of what the page still draws and the font cleanup
|
||||
/// read it again. At 100 MiB, three reads are more than the page's 256 MiB, and the removal is
|
||||
/// refused; at 50 MiB it goes on.
|
||||
/// (when the page lists a Type 3 font) read it again. At 100 MiB, three reads are more than the
|
||||
/// page's 256 MiB, and the removal is refused; at 50 MiB it goes on. With no Type 3 font the font
|
||||
/// cleanup's read has an allowance of its own, so the page's pays for two reads of 100 MiB, and the
|
||||
/// removal goes on.
|
||||
#[test]
|
||||
fn removing_hidden_text_spends_what_the_content_walk_left_of_the_pages_allowance() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
for (mib, refused) in [(100, true), (50, false)] {
|
||||
for (mib, type3, refused) in [(100, true, true), (50, true, false), (100, false, false)] {
|
||||
let blank = pdfcraft_filters::encode_flate(&vec![b' '; mib << 20]);
|
||||
let mut doc = one_page(
|
||||
b"/FmHidden Do /FmBare Do",
|
||||
@@ -3636,7 +3721,10 @@ fn removing_hidden_text_spends_what_the_content_walk_left_of_the_pages_allowance
|
||||
stream("/Type /XObject /Subtype /Form /BBox [0 0 300 300] /Filter /FlateDecode", &blank),
|
||||
],
|
||||
);
|
||||
assert_eq!(remove_hidden(&mut doc, &[Hidden::HiddenText]).is_err(), refused, "{mib} MiB");
|
||||
if type3 {
|
||||
list_type3(&mut doc, ObjRef::new(3, 0));
|
||||
}
|
||||
assert_eq!(remove_hidden(&mut doc, &[Hidden::HiddenText]).is_err(), refused, "{mib} MiB, Type 3: {type3}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-13
@@ -539,10 +539,19 @@ fn gs_font_object(doc: &Document, state_obj: &Object) -> Option<Object> {
|
||||
font_entry.into_iter().next()
|
||||
}
|
||||
|
||||
/// Whether font `f` (a reference, or a font dictionary) is a Type 3 font, the only kind the font
|
||||
/// cleanup prunes: its `/Subtype` is read directly, as the cleanup's other readers read it, so one
|
||||
/// given by reference isn't recognised by any of them. Which allowance the cleanup's read is charged
|
||||
/// to, and what it prunes, both go by this.
|
||||
fn is_type3(doc: &Document, f: &Object) -> bool {
|
||||
doc.resolve(f).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Type3"))
|
||||
}
|
||||
|
||||
/// Prune Type 3 fonts from a rewritten page or form's `/Resources` dictionary if the rewritten
|
||||
/// content no longer draws any text using that font name. `page` is the page's dictionary when
|
||||
/// `res` are a page's resources. What it reads is charged to `budget`, what is left of the page's
|
||||
/// allowance.
|
||||
/// allowance, when the resources list a Type 3 font, and otherwise to an allowance of a page of its
|
||||
/// own (below).
|
||||
pub(crate) fn prune_type3_from_resources(
|
||||
doc: &Document,
|
||||
budget: &mut pdfcraft_cos::DecodeBudget,
|
||||
@@ -556,12 +565,20 @@ pub(crate) fn prune_type3_from_resources(
|
||||
}
|
||||
|
||||
// The operators of forms that draw through these resources count too: dropping a font a child
|
||||
// form still shows would leave its text without one.
|
||||
let mut ops = crate::interp::resource_ops(doc, budget, res, content)?;
|
||||
// form still shows would leave its text without one. Without a Type 3 font nothing is pruned,
|
||||
// but the read is still made: it refuses bytes the parser can't read in what the content draws,
|
||||
// and on a page with nothing replaced and no properties listed no other check does, unless the
|
||||
// document still lists a Type 3 font (`unlink_type3` then reads every page). The walk reads
|
||||
// past them, so text they hide would stay under the mark. The read is then charged to an
|
||||
// allowance of a page of its own, not to what the walk left of the page's, since it decides
|
||||
// nothing about the fonts.
|
||||
let type3 = fonts.iter().any(|(_, f)| is_type3(doc, f));
|
||||
let mut own = pdfcraft_cos::DecodeBudget::page();
|
||||
let mut ops = crate::interp::resource_ops(doc, if type3 { &mut *budget } else { &mut own }, res, content)?;
|
||||
// So do those of a page's annotation appearances, and of soft mask groups, without resources of
|
||||
// their own. They are read only when there is a Type 3 font to keep, so other files are not
|
||||
// refused over them.
|
||||
if fonts.iter().any(|(_, f)| doc.resolve(f).as_dict().is_some_and(|d| d.name(b"Subtype") == Some(b"Type3"))) {
|
||||
if type3 {
|
||||
if let Some(page) = page {
|
||||
// One total for all of them, as for the content: each appearance under the cap could
|
||||
// still add up to far more than it. Their decoded bytes, and their forms', are charged to
|
||||
@@ -605,15 +622,12 @@ pub(crate) fn prune_type3_from_resources(
|
||||
let mut changed = false;
|
||||
let font_names: Vec<Vec<u8>> = fonts.iter().map(|(n, _)| n.to_vec()).collect();
|
||||
for name in font_names {
|
||||
if let Some(font_obj) = fonts.get(&name) {
|
||||
let resolved = doc.resolve(font_obj);
|
||||
if let Some(d) = resolved.as_dict()
|
||||
&& d.name(b"Subtype") == Some(b"Type3")
|
||||
&& !drawn_font_names.contains(&name)
|
||||
{
|
||||
fonts.remove(&name);
|
||||
changed = true;
|
||||
}
|
||||
if let Some(font_obj) = fonts.get(&name)
|
||||
&& is_type3(doc, font_obj)
|
||||
&& !drawn_font_names.contains(&name)
|
||||
{
|
||||
fonts.remove(&name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user