mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M7: redaction audit skips covered-text assembly when copying is withheld
This commit is contained in:
+11
-8
@@ -142,12 +142,17 @@ impl Budget {
|
||||
self.page_cost = lines.iter().fold(0, |n, line| n.saturating_add(line.ink.len().max(1)));
|
||||
}
|
||||
|
||||
/// The text of the `lines` that `hides`, joined with single spaces.
|
||||
fn hidden_text(&mut self, lines: &[TextLine], hides: impl Fn(&TextLine) -> bool) -> Result<String, AuditError> {
|
||||
/// The text of the `lines` that `hides`, joined with single spaces, or `None` when they show
|
||||
/// no text. Without `copying` the text is empty and never assembled: one long line under many
|
||||
/// covers would otherwise be copied once per cover, outside both bounds.
|
||||
fn hidden_text(&mut self, lines: &[TextLine], copying: bool, hides: impl Fn(&TextLine) -> bool) -> Result<Option<String>, AuditError> {
|
||||
self.comparisons = self.comparisons.saturating_add(self.page_cost);
|
||||
if self.comparisons > self.limits.comparisons {
|
||||
return Err(AuditError::TooManyComparisons);
|
||||
}
|
||||
if !copying {
|
||||
return Ok(lines.iter().any(|line| hides(line) && !line.text.trim().is_empty()).then(String::new));
|
||||
}
|
||||
let mut covered = String::new();
|
||||
for line in lines.iter().filter(|l| hides(l)) {
|
||||
if !covered.is_empty() {
|
||||
@@ -155,7 +160,7 @@ impl Budget {
|
||||
}
|
||||
covered.push_str(line.text.trim());
|
||||
}
|
||||
Ok(covered)
|
||||
Ok((!covered.trim().is_empty()).then_some(covered))
|
||||
}
|
||||
|
||||
/// The text a finding reports for `covered`: none when copying is withheld. Charges what it
|
||||
@@ -179,19 +184,17 @@ fn audit(doc: &Document, limits: Limits) -> Result<Vec<AuditFinding>, AuditError
|
||||
budget.start_page(&lines);
|
||||
// 1. Opaque filled rectangles over extractable text.
|
||||
for r in cover::filled_rects(doc, &page.dict)? {
|
||||
let covered = budget.hidden_text(&lines, |line| is_cover(&r, line))?;
|
||||
if !covered.trim().is_empty() {
|
||||
if let Some(covered) = budget.hidden_text(&lines, copying, |line| is_cover(&r, line))? {
|
||||
let covered_text = budget.report_text(covered, copying)?;
|
||||
out.push(AuditFinding { page: n, kind: FindingKind::CoveredText, rect: r.rect, covered_text });
|
||||
}
|
||||
}
|
||||
// 2. Annotation covers over extractable text.
|
||||
for annot in annots(doc, &page.dict)? {
|
||||
let covered = budget.hidden_text(&lines, |line| match &annot {
|
||||
if let Some(covered) = budget.hidden_text(&lines, copying, |line| match &annot {
|
||||
AnnotationCover::Redact(rect) => covers(rect, line),
|
||||
AnnotationCover::Square { rect, rgb } => covers(rect, line) && contrast(line.color, *rgb) < COVER_CONTRAST,
|
||||
})?;
|
||||
if !covered.trim().is_empty() {
|
||||
})? {
|
||||
let (kind, rect) = match annot {
|
||||
AnnotationCover::Redact(rect) => (FindingKind::UnappliedMark, rect),
|
||||
AnnotationCover::Square { rect, .. } => (FindingKind::CoveredText, rect),
|
||||
|
||||
@@ -709,3 +709,18 @@ fn copy_restricted_documents_withhold_covered_text() {
|
||||
let short = crate::Limits { covered_text: exact.covered_text - 1, ..exact };
|
||||
assert!(matches!(crate::audit(&owner, short), Err(crate::AuditError::TooMuchOutput)));
|
||||
}
|
||||
|
||||
/// Withheld text is never assembled: covers over one long line would otherwise copy the line once
|
||||
/// per cover, a volume neither the comparison nor the output bound charges.
|
||||
#[test]
|
||||
fn withheld_covered_text_is_not_assembled() {
|
||||
let doc = open(one_page(b"BT /F1 24 Tf 100 700 Td (SECRET DATA 123) Tj ET\n", ""));
|
||||
let lines = pdfcraft_edit::text_lines(&doc, 0).expect("text");
|
||||
let mut budget = crate::Budget { limits: crate::Limits::DEFAULT, comparisons: 0, page_cost: 0, covered_text: 0 };
|
||||
budget.start_page(&lines);
|
||||
let withheld = budget.hidden_text(&lines, false, |_| true).expect("within the budget");
|
||||
assert_eq!(withheld.map(|t| t.capacity()), Some(0), "a finding, with no text built for it");
|
||||
assert_eq!(budget.hidden_text(&lines, false, |_| false).expect("within the budget"), None);
|
||||
let copied = budget.hidden_text(&lines, true, |_| true).expect("within the budget");
|
||||
assert_eq!(copied.as_deref(), Some("SECRET DATA 123"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user