mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Find credit card numbers followed by an expiry, year or CVV (#2969)
* Find credit card numbers followed by an expiry, year or CVV Found while testing crates/redact/src/patterns.rs:180-190 (credit_card, via digit_run at :53) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Redact: a card followed by a number drops at most four trailing digits A longer grouped number (an IBAN's digits) no longer matches on a Luhn-valid prefix; the run is read far enough to see the tail and refused when it goes on past that. --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
Brandon Thomas
parent
25495b959b
commit
2eb1639097
@@ -183,12 +183,33 @@ fn credit_card(s: &[char], i: usize) -> Option<usize> {
|
||||
if !s[i].is_ascii_digit() || digit(i.checked_sub(1).and_then(|p| s.get(p))) {
|
||||
return None;
|
||||
}
|
||||
let (end, n) = digit_run(s, i, &[' ', '-'], 23);
|
||||
if !(13..=19).contains(&n) || digit(s.get(end)) {
|
||||
// Room for the longest card (19 digits in groups, 23 characters) and a tail of up to four digits.
|
||||
let (end, n) = digit_run(s, i, &[' ', '-'], 29);
|
||||
// A run longer than that is some other, longer number.
|
||||
if end == i || run_continues(s, end - 1, 1, &[' ', '-']) {
|
||||
return None;
|
||||
}
|
||||
let digits: Vec<u32> = s[i..end].iter().filter_map(|c| c.to_digit(10)).collect();
|
||||
luhn(&digits).then_some(end - i)
|
||||
// The longest run may carry a trailing number (an expiry, a year, a CVV), so try it and then
|
||||
// each shorter run ending at a group boundary, longest first. The dropped tail is at most four
|
||||
// digits, so a longer number (an IBAN's digits) doesn't match on a prefix.
|
||||
let mut count = n;
|
||||
for e in (i + 1..=end).rev() {
|
||||
if n - count > 4 {
|
||||
break;
|
||||
}
|
||||
if !s.get(e - 1).is_some_and(char::is_ascii_digit) {
|
||||
continue;
|
||||
}
|
||||
let boundary = if e == end { !digit(s.get(end)) } else { matches!(s.get(e), Some(' ' | '-')) };
|
||||
if boundary && (13..=19).contains(&count) {
|
||||
let digits: Vec<u32> = s[i..e].iter().filter_map(|c| c.to_digit(10)).collect();
|
||||
if luhn(&digits) {
|
||||
return Some(e - i);
|
||||
}
|
||||
}
|
||||
count = count.saturating_sub(1);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn ssn(s: &[char], i: usize) -> Option<usize> {
|
||||
@@ -435,6 +456,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credit_card_followed_by_another_number() {
|
||||
for tail in ["12/26", "2026", "123", "12 26"] {
|
||||
assert_eq!(found(Pattern::CreditCard, &format!("Card 4111 1111 1111 1111 {tail}")), ["4111 1111 1111 1111"], "tail {tail}");
|
||||
}
|
||||
assert_eq!(found(Pattern::CreditCard, "Card 3782-822463-10005 123"), ["3782-822463-10005"]);
|
||||
// A run that is not a card at any group boundary still matches nothing.
|
||||
assert_eq!(found(Pattern::CreditCard, "Card 4111 1111 1111 1112 12/26"), Vec::<String>::new());
|
||||
// A longer grouped number doesn't match on a card-shaped prefix.
|
||||
assert_eq!(found(Pattern::CreditCard, "IBAN 4111 1111 1111 1111 0000 0000"), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn social_security_numbers() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user