vcard: END:VCARDX is not the envelope's end
END_LINE matched any line opening on `END:VCARD`, so #insert put lines
before an `END:VCARDX` in a card with no real end. The pattern now
needs a line break or the card's end after it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAiyEheRBdzbBjZUDWZUPk
diff --git a/lib/pro_tacts/vcard.rb b/lib/pro_tacts/vcard.rb
index 9a3f63c..b65479a 100644
--- a/lib/pro_tacts/vcard.rb
+++ b/lib/pro_tacts/vcard.rb
@@ -21,8 +21,10 @@ module ProTacts
# The line inserted lines go immediately before, so a property
# lands inside the envelope however bare the card is. Matched at
# the start of a physical line in the whole card, so #insert finds
- # it without splitting a card's every line out to look.
- END_LINE = /^END:VCARD/i #: Regexp
+ # it without splitting a card's every line out to look. Ended by a
+ # line break or the card's end, so `END:VCARDX` is a line of its own
+ # and no envelope.
+ END_LINE = /^END:VCARD(?=[\r\n]|\z)/i #: Regexp
# A ";" after an even run of backslashes (#split_raw_components),
# and a "," after one: the separator of a list of text values,
diff --git a/test/pro_tacts/vcard/test_lines.rb b/test/pro_tacts/vcard/test_lines.rb
index 1ef49b7..f6649d4 100644
--- a/test/pro_tacts/vcard/test_lines.rb
+++ b/test/pro_tacts/vcard/test_lines.rb
@@ -335,6 +335,21 @@ class VCardLinesTest < Minitest::Test
VCARD.new(card).insert(["BDAY:1985-04"]).to_s
end
+ # Inserted lines land just inside the envelope's end: before the last
+ # line that reads as END:VCARD, whatever lines only look like one, and
+ # after everything where no line does.
+ def test_insert_lands_before_the_end_the_parse_reads
+ Hegel.test(test_cases: 1_000) do |tc|
+ lines = tc.draw(arrays(sampled_from(["END:VCARDX", "end:vcard-2", "NOTE:END:VCARD", "END:VCARD", "end:vcard", "FN:Ada"]), max_size: 5))
+ card = VCARD.new(["BEGIN:VCARD", *lines].map { "#{it}\r\n" }.join)
+ ends = card.lines.rindex { it.property&.name&.casecmp?("END") && it.property&.value&.casecmp?("VCARD") }
+
+ walked = card.insert(["BDAY:1985-04"]).lines.map { it.verbatim.chomp }
+
+ assert_equal ends || card.lines.length, walked.index("BDAY:1985-04")
+ end
+ end
+
def test_insert_with_nothing_leaves_the_card_alone
assert_equal CARD, VCARD.new(CARD).insert([]).to_s
end