mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:35:21 +00:00
fix(redact): mark changed marked-content spans in linear work
clean_replacements extended the affected set with the whole open-span stack for every changed operator, so nesting depth times changed operators was the work, and both come from the file. A change now marks only the innermost open span, and a span passes the mark to its parent when it closes (or at the end of the stream), so each operator and each span costs constant work. The spans found affected are the same.
This commit is contained in:
@@ -604,18 +604,35 @@ fn clean_replacements(doc: &Document, resources: &Dict, original: &[Op], process
|
||||
let originals: HashMap<usize, &Op> = original.iter().map(|op| (op.span.start, op)).collect();
|
||||
let unchanged: HashSet<usize> =
|
||||
processed.iter().flat_map(|(ops, _)| ops).filter(|op| originals.get(&op.span.start) == Some(op)).map(|op| op.span.start).collect();
|
||||
let mut stack = Vec::new();
|
||||
// Open spans, each with whether something inside it changed. A change marks only the
|
||||
// innermost span; a span passes that on to its parent when it closes, so every operator costs
|
||||
// the same however deeply spans nest.
|
||||
let mut stack: Vec<(usize, bool)> = Vec::new();
|
||||
let mut affected = HashSet::new();
|
||||
let close = |stack: &mut Vec<(usize, bool)>, affected: &mut HashSet<usize>| {
|
||||
if let Some((start, true)) = stack.pop() {
|
||||
affected.insert(start);
|
||||
if let Some(parent) = stack.last_mut() {
|
||||
parent.1 = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
for op in original {
|
||||
match op.op.as_slice() {
|
||||
b"BDC" | b"BMC" => stack.push(op.span.start),
|
||||
b"EMC" => {
|
||||
stack.pop();
|
||||
b"BDC" | b"BMC" => stack.push((op.span.start, false)),
|
||||
b"EMC" => close(&mut stack, &mut affected),
|
||||
_ if !unchanged.contains(&op.span.start) => {
|
||||
if let Some(top) = stack.last_mut() {
|
||||
top.1 = true;
|
||||
}
|
||||
}
|
||||
_ if !unchanged.contains(&op.span.start) => affected.extend(stack.iter().copied()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Spans the stream leaves open run to its end.
|
||||
while !stack.is_empty() {
|
||||
close(&mut stack, &mut affected);
|
||||
}
|
||||
let properties = res_dict(doc, resources, b"Properties");
|
||||
for (ops, changed) in processed {
|
||||
for op in ops {
|
||||
|
||||
@@ -53,3 +53,25 @@ fn a_change_inside_nested_marked_content_touches_every_enclosing_mcid() {
|
||||
assert_eq!(changed, HashSet::from([0, 1]));
|
||||
assert_eq!(empty, HashSet::from([1]));
|
||||
}
|
||||
|
||||
/// Every changed operator marked every open marked-content span again, so nesting times changed
|
||||
/// operators was the work. A change now marks the innermost span, which passes it up as it
|
||||
/// closes: 5 000 nested spans (below the overlay's nesting cap) around 50 000 redacted strings, a
|
||||
/// quarter of a billion steps before, still lose every description.
|
||||
#[test]
|
||||
fn deeply_nested_spans_around_many_changes_are_cleaned_in_linear_work() {
|
||||
const DEPTH: usize = 5_000;
|
||||
const SHOWN: usize = 50_000;
|
||||
let mut src = b"BT /F1 10 Tf 10 100 Td ".to_vec();
|
||||
src.extend(b"/Span <</ActualText (LEAK)>> BDC ".repeat(DEPTH));
|
||||
src.extend(b"(S) Tj 0 0 Td ".repeat(SHOWN));
|
||||
src.extend(b"EMC ".repeat(DEPTH));
|
||||
src.extend(b"ET");
|
||||
let mut doc = one_page(&src, "", vec![]);
|
||||
mark(&mut doc, 0, &[[5.0, 95.0, 20.0, 110.0]], "");
|
||||
let started = std::time::Instant::now();
|
||||
let r = apply(&mut doc, None).unwrap();
|
||||
assert_eq!((r.glyphs, r.replacement_text), (SHOWN, DEPTH), "{:?}", (r.glyphs, r.replacement_text));
|
||||
assert!(!content(&doc, 0).contains("LEAK"));
|
||||
assert!(started.elapsed() < std::time::Duration::from_secs(60), "{:?}", started.elapsed());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user