Pin the vCard redaction to the end of the marker
(?~) is greedy to the longest run holding no complete END:VCARD, so it runs
into the marker and stops at END:VCAR; the trailing alternation is what
backtracks it onto the boundary. Dropping it left a stray "D" behind, and
every existing test passed anyway, because they checked that card content
was gone rather than what replaced it.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/lib/pro_tacts/sentry_scrubber.rb b/lib/pro_tacts/sentry_scrubber.rb
index 66ea3db..053c8ad 100644
--- a/lib/pro_tacts/sentry_scrubber.rb
+++ b/lib/pro_tacts/sentry_scrubber.rb
@@ -7,13 +7,11 @@ module ProTacts
#
# Sentry truncates bodies at 16KB (RequestInterface::MAX_BODY_LIMIT), so a
# card can arrive with its BEGIN and no END. The trailing alternation
- # redacts to the end of the body in that case rather than missing the
- # match; a card with an END redacts only to its own END, leaving the rest
- # of the body intact.
+ # handles that, and is load-bearing for the ordinary case too: (?~) is
+ # greedy up to the longest run with no complete END:VCARD in it, which
+ # runs into the marker and stops at END:VCAR. Requiring END:VCARD or the
+ # end of the body is what backtracks it onto the real boundary.
module SentryScrubber
- # (?~exp) is Ruby's absence operator: the run of characters not
- # containing exp. Says "up to the first END:VCARD" more directly than a
- # lazy quantifier, whose stopping point depends on alternation order.
VCARD = /BEGIN:VCARD(?~END:VCARD)(?:END:VCARD|\z)/mi
VCARD_CONTENT_TYPE = %r{\Atext/vcard}i
REDACTED = "[vcard redacted]".freeze #: String
diff --git a/test/pro_tacts/test_sentry_scrubber.rb b/test/pro_tacts/test_sentry_scrubber.rb
index a99d42a..5c53688 100644
--- a/test/pro_tacts/test_sentry_scrubber.rb
+++ b/test/pro_tacts/test_sentry_scrubber.rb
@@ -43,6 +43,14 @@ class SentryScrubberTest < Minitest::Test
assert_includes scrubbed, "<href>/keep/me</href>"
end
+ # (?~) is greedy to the longest run holding no complete END:VCARD, which
+ # runs into the marker and stops at END:VCAR. Without something forcing it
+ # back onto the boundary a stray "D" survives, so assert the exact result
+ # rather than just the absence of card content.
+ def test_no_fragment_of_the_end_marker_survives
+ assert_equal "<d>#{ProTacts::SentryScrubber::REDACTED}</d>TAIL", scrub("<d>#{CARD}</d>TAIL")
+ end
+
def test_content_between_two_cards_survives
scrubbed = scrub("#{CARD}\nMIDDLE\n#{CARD.sub('Real Person', 'Other Person')}\nTRAILING")