From 8edfb4d3a6d0b21eb673195e17d1f0eb6531e107 Mon Sep 17 00:00:00 2001 From: daniele-NA Date: Mon, 1 Jun 2026 21:26:55 +0200 Subject: [PATCH] mastercard fix --- .../com/mirkoddd/sift/core/SiftCatalog.java | 18 ++++++---- .../mirkoddd/sift/core/SiftCatalogTest.java | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/sift-core/src/main/java/com/mirkoddd/sift/core/SiftCatalog.java b/sift-core/src/main/java/com/mirkoddd/sift/core/SiftCatalog.java index 0033c06..553c887 100644 --- a/sift-core/src/main/java/com/mirkoddd/sift/core/SiftCatalog.java +++ b/sift-core/src/main/java/com/mirkoddd/sift/core/SiftCatalog.java @@ -278,11 +278,10 @@ public static SiftPattern jwt() { * It does not perform Luhn algorithm checksum validation. * * @@ -300,8 +299,13 @@ public static SiftPattern creditCard() { SiftPattern mastercard = anyOf( // Legacy 51-55 Sift.fromAnywhere().character('5').then().range('1', '5').then().exactly(14).digits(), - // Modern 2221-2720 (structural approximation: 2[2-6]XXXXXXXXXXXXXX) - Sift.fromAnywhere().character('2').then().range('2', '6').then().exactly(14).digits() + // Modern 2-series, exact 2221-2720 range (ISO/Mastercard 2017 expansion): + // 2221-2229 | 2230-2299 | 2300-2699 | 2700-2719 | 2720 + Sift.fromAnywhere().character('2').followedBy('2').followedBy('2').then().range('1', '9').then().exactly(12).digits(), + Sift.fromAnywhere().character('2').followedBy('2').then().range('3', '9').then().exactly(13).digits(), + Sift.fromAnywhere().character('2').then().range('3', '6').then().exactly(14).digits(), + Sift.fromAnywhere().character('2').followedBy('7').then().range('0', '1').then().exactly(13).digits(), + Sift.fromAnywhere().character('2').followedBy('7').followedBy('2').followedBy('0').then().exactly(12).digits() ); return anyOf(visa, amex, mastercard) diff --git a/sift-core/src/test/java/com/mirkoddd/sift/core/SiftCatalogTest.java b/sift-core/src/test/java/com/mirkoddd/sift/core/SiftCatalogTest.java index e08a21c..f73c3b0 100644 --- a/sift-core/src/test/java/com/mirkoddd/sift/core/SiftCatalogTest.java +++ b/sift-core/src/test/java/com/mirkoddd/sift/core/SiftCatalogTest.java @@ -446,6 +446,21 @@ Arbitrary amexNumbers() { return Combinators.combine(prefix, body).as((p, b) -> p + b); } + @Property + void creditCardShouldAcceptMastercard2Series(@ForAll("mastercard2SeriesNumbers") String mc) { + try (SiftCompiledPattern pattern = SiftCatalog.creditCard().sieve()) { + assertTrue(pattern.matchesEntire(mc), "Should accept valid 2-series Mastercard: " + mc); + } + } + + @Property + void creditCardShouldRejectMastercard2SeriesOutOfRange(@ForAll("mastercard2SeriesOutOfRangePrefixes") int prefix) { + String number = prefix + "000000000000"; // 4-digit prefix + 12 digits = 16 + try (SiftCompiledPattern pattern = SiftCatalog.creditCard().sieve()) { + assertFalse(pattern.matchesEntire(number), "Should reject out-of-range 2-series prefix: " + prefix); + } + } + @Provide Arbitrary mastercardLegacyNumbers() { // Legacy 51-55 prefix @@ -454,6 +469,21 @@ Arbitrary mastercardLegacyNumbers() { return Combinators.combine(firstDigit, body).as((d, b) -> "5" + d + b); } + @Provide + Arbitrary mastercard2SeriesNumbers() { + Arbitrary prefix = Arbitraries.integers().between(2221, 2720); + Arbitrary body = Arbitraries.strings().withCharRange('0', '9').ofLength(12); + return Combinators.combine(prefix, body).as((p, b) -> p + b); + } + + @Provide + Arbitrary mastercard2SeriesOutOfRangePrefixes() { + return Arbitraries.oneOf( + Arbitraries.integers().between(2200, 2220), + Arbitraries.integers().between(2721, 2799) + ); + } + @Test void shouldValidateCreditCard() { try (SiftCompiledPattern ccPattern = SiftCatalog.creditCard().sieve()) { @@ -465,6 +495,12 @@ void shouldValidateCreditCard() { // Mastercard legacy assertTrue(ccPattern.matchesEntire("5500005555555559")); assertTrue(ccPattern.matchesEntire("5105105105105100")); + // Mastercard 2-series boundaries (exact 2221-2720 range) + assertTrue(ccPattern.matchesEntire("2221000000000000"), "Should accept lower boundary 2221"); + assertTrue(ccPattern.matchesEntire("2720990000000000"), "Should accept upper boundary 2720"); + assertTrue(ccPattern.matchesEntire("2700000000000000"), "Should accept 2700 (regression: previously rejected)"); + assertFalse(ccPattern.matchesEntire("2220000000000000"), "Should reject 2220 (just below range)"); + assertFalse(ccPattern.matchesEntire("2721000000000000"), "Should reject 2721 (just above range)"); assertFalse(ccPattern.matchesEntire("1234567890123456"), "Should reject unknown prefix"); assertFalse(ccPattern.matchesEntire("411111111111111"), "Should reject Visa too short");