mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 06:51:50 -06:00
fix(redact): structure titles go with the descriptions of removed content (#5866)
Redaction and hidden-content removal cleared /Alt, /ActualText and /E from the structure element owning removed marked content, but kept its /T title, which producers often fill with the element's text. Ancestor cleanup and in-place description detection shared a constant that also omitted /T, while annotation removal had its own list that included it. One DESCRIPTIONS policy (Alt, ActualText, T, E) now serves the owning element, its ancestors, in-place detection and annotation removal.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Marked content (MCIDs) that applying redactions changed is found by comparing each MCID's
|
||||
//! operators before and after. Elements owning such content lose their alternate text, actual
|
||||
//! text and expansion (which may spell out the removed words);
|
||||
//! text, title and expansion (which may spell out the removed words), as do their ancestors;
|
||||
//! references to marked content that is now empty are dropped from the element.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -281,7 +281,7 @@ pub(crate) fn clean_within(
|
||||
}
|
||||
doc.update_dict(r, |e| {
|
||||
if sensitive {
|
||||
for key in [&b"Alt"[..], b"ActualText", b"E"] {
|
||||
for key in DESCRIPTIONS {
|
||||
e.remove(key);
|
||||
}
|
||||
}
|
||||
@@ -321,8 +321,10 @@ impl Above {
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys of an element that describe its content.
|
||||
const DESCRIPTIONS: [&[u8]; 3] = [b"Alt", b"ActualText", b"E"];
|
||||
/// The keys of an element that describe its content, and so may repeat what was removed: one
|
||||
/// policy for the element that owned it, the elements above it and elements written in place.
|
||||
/// The title (`/T`) is included, as producers often fill it with the element's text.
|
||||
const DESCRIPTIONS: [&[u8]; 4] = [b"Alt", b"ActualText", b"T", b"E"];
|
||||
|
||||
/// Each element's parents as the walk found them: through `/K`, which `/P` may not agree with.
|
||||
struct Parents(HashMap<ObjRef, Vec<Above>>);
|
||||
@@ -539,7 +541,7 @@ pub(crate) fn drop_object_refs_within(doc: &mut Document, gone: &HashSet<ObjRef>
|
||||
// An element that lost an annotation keeps no text describing it, even with other
|
||||
// kids left: a link's alternate text often repeats the URL, an annotation's its
|
||||
// comment. Elements above it may describe it too.
|
||||
for key in [&b"Alt"[..], b"ActualText", b"T", b"E"] {
|
||||
for key in DESCRIPTIONS {
|
||||
e.remove(key);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -62,3 +62,61 @@ fn a_link_element_that_keeps_its_text_loses_the_description_of_its_removed_annot
|
||||
let link = doc.get(ObjRef::new(11, 0)).as_dict().cloned().unwrap();
|
||||
assert_eq!(link.get(b"K"), Some(&Object::Array(vec![Object::Int(0)])), "the element keeps its text");
|
||||
}
|
||||
|
||||
/// [`sectioned`], with titles that repeat the secret on the paragraph that owns it, on its section
|
||||
/// and on the document element above both.
|
||||
fn titled() -> Document {
|
||||
let mut doc = sectioned("11 0 R");
|
||||
let title = |t: &str| Object::String(pdfcraft_cos::PdfString::literal(t.as_bytes().to_vec()));
|
||||
for (n, t) in [(8, "LEAK_DOC_TITLE"), (9, "LEAK_OWN_TITLE"), (11, "LEAK_SECT_TITLE"), (10, "Public title")] {
|
||||
doc.update_dict(ObjRef::new(n, 0), |d| d.set(b"T".to_vec(), title(t))).unwrap();
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn titles_repeating_redacted_content_go_from_the_owner_and_its_ancestors() {
|
||||
let mut doc = titled();
|
||||
mark(&mut doc, 0, &[[5.0, 195.0, 60.0, 212.0]], "");
|
||||
apply(&mut doc, None).unwrap();
|
||||
assert!(!full_save_holds(&doc, b"LEAK_"), "a structure title repeating the redacted word survived");
|
||||
let public = doc.get(ObjRef::new(10, 0)).as_dict().cloned().unwrap();
|
||||
assert!(public.contains(b"T"), "an element beside it keeps its own title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn titles_repeating_hidden_text_go_from_the_owner_and_its_ancestors() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
let mut doc = titled();
|
||||
let hidden =
|
||||
b"/P <</MCID 0>> BDC BT /F1 10 Tf 3 Tr 10 200 Td (SECRET) Tj ET EMC /P <</MCID 1>> BDC BT /F1 10 Tf 0 Tr 10 100 Td (Public) Tj ET EMC";
|
||||
doc.set(ObjRef::new(4, 0), Object::Stream(Stream::from_raw(Dict::new(), hidden.to_vec())));
|
||||
remove_hidden(&mut doc, &[Hidden::HiddenText]).unwrap();
|
||||
assert!(!full_save_holds(&doc, b"SECRET"), "the hidden text survived");
|
||||
assert!(!full_save_holds(&doc, b"LEAK_"), "a structure title repeating the hidden text survived");
|
||||
let public = doc.get(ObjRef::new(10, 0)).as_dict().cloned().unwrap();
|
||||
assert!(public.contains(b"T"), "an element beside it keeps its own title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ancestor_title_describing_a_removed_annotation_goes() {
|
||||
use crate::sanitize::{Hidden, remove_hidden};
|
||||
let mut doc = pdf(vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 8 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 << /Font << /F1 5 0 R >> >> /Annots [6 0 R] /StructParents 0 >>"
|
||||
.to_vec(),
|
||||
stream("", b"/Link << /MCID 0 >> BDC BT /F1 10 Tf 10 230 Td (See the site) Tj ET EMC"),
|
||||
FONT.replace("95 0 R", "12 0 R").into_bytes(),
|
||||
b"<< /Type /Annot /Subtype /Link /Rect [10 228 80 240] /StructParent 1 /A << /S /URI /URI (https://example.org/) >> >>".to_vec(),
|
||||
b"null".to_vec(),
|
||||
b"<< /Type /StructTreeRoot /K 9 0 R /ParentTree << /Nums [0 [11 0 R] 1 11 0 R] >> /ParentTreeNextKey 2 >>".to_vec(),
|
||||
b"<< /Type /StructElem /S /Document /P 8 0 R /K 10 0 R >>".to_vec(),
|
||||
// Only a title above the link element repeats the address.
|
||||
b"<< /Type /StructElem /S /P /P 9 0 R /T (LEAK_PARENT_TITLE) /K 11 0 R >>".to_vec(),
|
||||
b"<< /Type /StructElem /S /Link /P 10 0 R /Pg 3 0 R /K [0 << /Type /OBJR /Obj 6 0 R /Pg 3 0 R >>] >>".to_vec(),
|
||||
widths(),
|
||||
]);
|
||||
remove_hidden(&mut doc, &[Hidden::LinksActionsScripts]).unwrap();
|
||||
assert!(!full_save_holds(&doc, b"LEAK_"), "an ancestor's title describing the removed link survived");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user