From 424459f386bca519da390de14503bb36f32f59cc Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 15 Oct 2026 14:27:23 +0800 Subject: [PATCH] fix(audit): find rectangle covers closed with h or painted with b/b* (#1817) (#1865) The redaction audit turned an re rectangle into an unknown path on a redundant h, and its fill branch left out the close-fill-stroke operators b and b*, so those opaque covers over extractable text produced no finding. Keep a closed rectangle closed under h and treat b/b* like the other fills. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 --- crates/audit/src/cover.rs | 5 ++++- crates/audit/src/tests.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/audit/src/cover.rs b/crates/audit/src/cover.rs index be8afe0a8..3df666c19 100644 --- a/crates/audit/src/cover.rs +++ b/crates/audit/src/cover.rs @@ -219,10 +219,13 @@ impl<'a> Walker<'a> { b"h" => { path = match path { Path::Lines(pts, _) => Path::Lines(pts, true), + // `re` already closed its subpath, so a further `h` changes nothing. + Path::Rect(r) => Path::Rect(r), _ => Path::Other, }; } - b"f" | b"F" | b"f*" | b"B" | b"B*" => { + // `b`/`b*` close the path before filling it, which the fill does anyway. + b"f" | b"F" | b"f*" | b"B" | b"B*" | b"b" | b"b*" => { if let Some(r) = std::mem::replace(&mut path, Path::Empty).rect() && gs.alpha >= 1.0 { diff --git a/crates/audit/src/tests.rs b/crates/audit/src/tests.rs index be98d7284..a9397145a 100644 --- a/crates/audit/src/tests.rs +++ b/crates/audit/src/tests.rs @@ -232,8 +232,14 @@ fn self_drawing_form_fails_the_audit() { fn rectangle_drawn_as_a_path_covers_text_like_re() { // The same cover as `re`, built from `m`/`l` (closed with `h`, a repeated start point, or // only by the fill itself), or in a scaled user space: all are faux redactions (#1510). - let covers: [&[u8]; 5] = [ + // So are `re` with a redundant `h`, and either path painted with `b`/`b*` (#1817). + let covers: [&[u8]; 10] = [ b"0 g\n100 688 220 32 re f\n", + b"0 g\n100 688 220 32 re h f\n", + b"0 g\n100 688 220 32 re b\n", + b"0 g\n100 688 220 32 re h b*\n", + b"0 g\n100 688 m 320 688 l 320 720 l 100 720 l b\n", + b"0 g\n100 688 m 320 688 l 320 720 l 100 720 l h b*\n", b"0 g\n100 688 m 320 688 l 320 720 l 100 720 l h f\n", b"0 g\n100 688 m 320 688 l 320 720 l 100 720 l 100 688 l h f\n", b"0 g\n100 688 m 100 720 l 320 720 l 320 688 l f\n", @@ -253,9 +259,12 @@ fn rectangle_drawn_as_a_path_covers_text_like_re() { #[test] fn non_rectangular_path_is_not_a_cover() { // Each path's bounding box covers the text, but the shape itself does not hide it all. - let covers: [&[u8]; 5] = [ + let covers: [&[u8]; 7] = [ // A triangle. b"0 g\n100 688 m 320 688 l 320 720 l h f\n", + // A triangle painted with `b`, and a trapezoid with `b*`. + b"0 g\n100 688 m 320 688 l 320 720 l b\n", + b"0 g\n100 688 m 320 688 l 320 720 l 140 720 l b*\n", // A triangle, then a line from its start point after `h`: not a fourth corner. b"0 g\n100 688 m 320 688 l 320 720 l h 100 720 l f\n", // A trapezoid: four corners, one slanted side.