From 613bba9e0598b349331c4bd9c5d2e05a506e2415 Mon Sep 17 00:00:00 2001 From: Simon Rozman Date: Tue, 19 Sep 2023 21:40:36 +0200 Subject: [PATCH] parser: refine IBAN checking Allow arbitrary spacing, minor optimizations... Signed-off-by: Simon Rozman --- UnitTests/parser.cpp | 41 +++++++++++- include/stdex/parser.hpp | 133 +++++++++++++++++++-------------------- 2 files changed, 102 insertions(+), 72 deletions(-) diff --git a/UnitTests/parser.cpp b/UnitTests/parser.cpp index 1c21fa042..681c964c6 100644 --- a/UnitTests/parser.cpp +++ b/UnitTests/parser.cpp @@ -205,11 +205,47 @@ namespace UnitTests Assert::IsTrue(p.match(L"si56 0231 2001 5226 972", 0, SIZE_MAX, match_case_insensitive)); Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match(L"SI56 0231 2001 5226 9720", 0, SIZE_MAX)); - Assert::IsFalse(p.is_valid); + Assert::AreEqual(stdex::interval(0, 23), p.interval); + Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match(L"...SI56 0231 2001 5226 972...", 3, SIZE_MAX)); Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match(L"SI56 0231 2001 5226 972", 0, SIZE_MAX)); // no-break space Assert::IsTrue(p.is_valid); + + Assert::IsTrue(p.match(L"BE71 0961 2345 6769", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"BR15 0000 0000 0000 1093 2840 814 P2", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"CR99 0000 0000 0000 8888 88", 0, SIZE_MAX)); + Assert::IsFalse(p.is_valid); + Assert::IsTrue(p.match(L"FR76 3000 6000 0112 3456 7890 189", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"IE12 BOFI 9000 0112 3456 78", 0, SIZE_MAX)); + Assert::IsFalse(p.is_valid); + Assert::IsTrue(p.match(L"DE91 1000 0000 0123 4567 89", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"GR96 0810 0010 0000 0123 4567 890", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"MU43 BOMM 0101 1234 5678 9101 000 MUR", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"PK70 BANK 0000 1234 5678 9000", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"PL10 1050 0099 7603 1234 5678 9123", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"RO09 BCYP 0000 0012 3456 7890", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"LC14 BOSL 1234 5678 9012 3456 7890 1234", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"SA44 2000 0001 2345 6789 1234", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"ES79 2100 0813 6101 2345 6789", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"SE87 3000 0000 0101 2345 6789", 0, SIZE_MAX)); + Assert::IsFalse(p.is_valid); + Assert::IsTrue(p.match(L"CH56 0483 5012 3456 7800 9", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); + Assert::IsTrue(p.match(L"GB98 MIDL 0700 9312 3456 78", 0, SIZE_MAX)); + Assert::IsTrue(p.is_valid); } { @@ -322,7 +358,8 @@ namespace UnitTests Assert::IsTrue(p.match("si56 0231 2001 5226 972", 0, SIZE_MAX, match_case_insensitive)); Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match("SI56 0231 2001 5226 9720", 0, SIZE_MAX)); - Assert::IsFalse(p.is_valid); + Assert::AreEqual(stdex::interval(0, 23), p.interval); + Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match("...SI56 0231 2001 5226 972...", 3, SIZE_MAX)); Assert::IsTrue(p.is_valid); Assert::IsTrue(p.match("SI56 0231 2001 5226 972", 0, SIZE_MAX)); diff --git a/include/stdex/parser.hpp b/include/stdex/parser.hpp index edc4dedd3..102e31c21 100644 --- a/include/stdex/parser.hpp +++ b/include/stdex/parser.hpp @@ -4806,7 +4806,8 @@ namespace stdex { { 'X', 'K' }, {}, 20 }, // Kosovo }; const country_t* country_desc = nullptr; - size_t n; + size_t n, available, next, bban_length; + uint32_t nominator; this->interval.end = start; for (size_t i = 0; i < 2; ++i, ++this->interval.end) { @@ -4844,85 +4845,77 @@ namespace stdex (country_desc->check_digits[1] && this->check_digits[1] != country_desc->check_digits[1])) goto error; // unexpected check digits - for (n = 0; ;) { - if (m_space && m_space->match(text, this->interval.end, end, flags)) + bban_length = country_desc->length - 4; + for (n = 0; n < bban_length;) { + if (this->interval.end >= end || !text[this->interval.end]) + goto error; // bban too short + if (m_space && m_space->match(text, this->interval.end, end, flags)) { this->interval.end = m_space->interval.end; - for (size_t j = 0; j < 4; ++j) { - if (this->interval.end >= end || !text[this->interval.end]) - goto out; - T chr = case_insensitive ? ctype.toupper(text[this->interval.end]) : text[this->interval.end]; - if (('0' <= chr && chr <= '9') || ('A' <= chr && chr <= 'Z')) { - if (n >= _countof(this->bban) - 1) - goto error; // bban overflow - this->bban[n++] = chr; - this->interval.end++; - } - else - goto out; + continue; } + T chr = case_insensitive ? ctype.toupper(text[this->interval.end]) : text[this->interval.end]; + if (('0' <= chr && chr <= '9') || ('A' <= chr && chr <= 'Z')) { + this->bban[n++] = chr; + this->interval.end++; + } + else + goto error; // invalid bban } out: - if (n < 11) - goto error; // bban too short (shorter than Norwegian) this->bban[n] = 0; - if (n + 4 == country_desc->length) { - // Normalize IBAN. - T normalized[69]; - size_t available = 0; - for (size_t i = 0; ; ++i) { - if (!this->bban[i]) { - for (i = 0; i < 2; ++i) { - if ('A' <= this->country[i] && this->country[i] <= 'J') { - normalized[available++] = '1'; - normalized[available++] = '0' + this->country[i] - 'A'; - } - else if ('K' <= this->country[i] && this->country[i] <= 'T') { - normalized[available++] = '2'; - normalized[available++] = '0' + this->country[i] - 'K'; - } - else if ('U' <= this->country[i] && this->country[i] <= 'Z') { - normalized[available++] = '3'; - normalized[available++] = '0' + this->country[i] - 'U'; - } + // Normalize IBAN. + T normalized[69]; + available = 0; + for (size_t i = 0; ; ++i) { + if (!this->bban[i]) { + for (i = 0; i < 2; ++i) { + if ('A' <= this->country[i] && this->country[i] <= 'J') { + normalized[available++] = '1'; + normalized[available++] = '0' + this->country[i] - 'A'; + } + else if ('K' <= this->country[i] && this->country[i] <= 'T') { + normalized[available++] = '2'; + normalized[available++] = '0' + this->country[i] - 'K'; + } + else if ('U' <= this->country[i] && this->country[i] <= 'Z') { + normalized[available++] = '3'; + normalized[available++] = '0' + this->country[i] - 'U'; } - normalized[available++] = this->check_digits[0]; - normalized[available++] = this->check_digits[1]; - normalized[available] = 0; - break; - } - if ('0' <= this->bban[i] && this->bban[i] <= '9') - normalized[available++] = this->bban[i]; - else if ('A' <= this->bban[i] && this->bban[i] <= 'J') { - normalized[available++] = '1'; - normalized[available++] = '0' + this->bban[i] - 'A'; - } - else if ('K' <= this->bban[i] && this->bban[i] <= 'T') { - normalized[available++] = '2'; - normalized[available++] = '0' + this->bban[i] - 'K'; - } - else if ('U' <= this->bban[i] && this->bban[i] <= 'Z') { - normalized[available++] = '3'; - normalized[available++] = '0' + this->bban[i] - 'U'; } + normalized[available++] = this->check_digits[0]; + normalized[available++] = this->check_digits[1]; + normalized[available] = 0; + break; } - - // Calculate modulo 97. - size_t next; - uint32_t nominator = stdex::strtou32(normalized, 9, &next, 10); - for (;;) { - nominator %= 97; - if (!normalized[next]) { - this->is_valid = nominator == 1; - break; - } - size_t digit_count = nominator < 10 ? 1 : 2; - for (; digit_count < 9 && normalized[next]; ++next, ++digit_count) - nominator = nominator * 10 + (normalized[next] - '0'); + if ('0' <= this->bban[i] && this->bban[i] <= '9') + normalized[available++] = this->bban[i]; + else if ('A' <= this->bban[i] && this->bban[i] <= 'J') { + normalized[available++] = '1'; + normalized[available++] = '0' + this->bban[i] - 'A'; + } + else if ('K' <= this->bban[i] && this->bban[i] <= 'T') { + normalized[available++] = '2'; + normalized[available++] = '0' + this->bban[i] - 'K'; + } + else if ('U' <= this->bban[i] && this->bban[i] <= 'Z') { + normalized[available++] = '3'; + normalized[available++] = '0' + this->bban[i] - 'U'; } } - else - this->is_valid = false; + + // Calculate modulo 97. + nominator = stdex::strtou32(normalized, 9, &next, 10); + for (;;) { + nominator %= 97; + if (!normalized[next]) { + this->is_valid = nominator == 1; + break; + } + size_t digit_count = nominator == 0 ? 0 : nominator < 10 ? 1 : 2; + for (; digit_count < 9 && normalized[next]; ++next, ++digit_count) + nominator = nominator * 10 + (normalized[next] - '0'); + } this->interval.start = start; return true; @@ -5074,7 +5067,7 @@ namespace stdex this->is_valid = nominator == 1; break; } - size_t digit_count = nominator < 10 ? 1 : 2; + size_t digit_count = nominator == 0 ? 0 : nominator < 10 ? 1 : 2; for (; digit_count < 9 && normalized[next]; ++next, ++digit_count) nominator = nominator * 10 + (normalized[next] - '0'); }