diff --git a/src/main/java/ru/pdguard/detect/RuleRegistry.java b/src/main/java/ru/pdguard/detect/RuleRegistry.java index cde3726..cf310ef 100644 --- a/src/main/java/ru/pdguard/detect/RuleRegistry.java +++ b/src/main/java/ru/pdguard/detect/RuleRegistry.java @@ -210,13 +210,16 @@ public class RuleRegistry { .anchoredBy("срок действия", "действительн", "exp"), // ОГРНИП раньше ОГРН: без отрицательного просмотра «ОГРНИП» частично ловился бы - // ещё и правилом ОГРН. + // ещё и правилом ОГРН. Контрольная сумма отсекает случайные 13/15-значные + // числа рядом со словом — раньше якоря было достаточно самого по себе. Rule.of(OGRNIP, "(?iu:огрнип)\\W{0,5}(\\d{15})\\b", 83) .groups(1) + .validatedBy(Validators::ogrnip) .anchoredBy("огрнип"), Rule.of(OGRN, "(?iu:огрн(?!ип))\\W{0,5}(\\d{13})\\b", 83) .groups(1) + .validatedBy(Validators::ogrn) .anchoredBy("огрн"), Rule.of(KPP, "(?iu:кпп)\\W{0,5}(\\d{9})\\b", 83) diff --git a/src/main/java/ru/pdguard/detect/Validators.java b/src/main/java/ru/pdguard/detect/Validators.java index ed8873f..4a88835 100644 --- a/src/main/java/ru/pdguard/detect/Validators.java +++ b/src/main/java/ru/pdguard/detect/Validators.java @@ -63,6 +63,32 @@ public final class Validators { return control == d[9] * 10 + d[10]; } + /** Контрольная сумма ОГРН: первые 12 цифр по модулю 11, младший разряд — 13-я цифра. */ + public static boolean ogrn(String value) { + int[] d = digits(value); + return d.length == 13 && d[12] == modReduce(d, 12, 11); + } + + /** Контрольная сумма ОГРНИП: первые 14 цифр по модулю 13, младший разряд — 15-я цифра. */ + public static boolean ogrnip(String value) { + int[] d = digits(value); + return d.length == 15 && d[14] == modReduce(d, 14, 13); + } + + /** + * Остаток от деления первых {@code count} цифр как одного числа на {@code divisor}, + * взятый по младшему разряду. Числовое накопление по цифрам, а не парсинг строки + * в {@code long}: у ОГРНИП 14 цифр — на грани переполнения {@code int}, и это тот же + * приём, что уже применяется к самой длинной последовательности в {@link #luhn}. + */ + private static int modReduce(int[] d, int count, int divisor) { + long remainder = 0; + for (int i = 0; i < count; i++) { + remainder = (remainder * 10 + d[i]) % divisor; + } + return (int) (remainder % 10); + } + /** * Дата в числовой записи при любом порядке частей: {@code 12.05.1985}, * {@code 05/12/1985}, {@code 1985-05-12}. Отсекает похожие по форме diff --git a/src/test/java/ru/pdguard/BankTypesTest.java b/src/test/java/ru/pdguard/BankTypesTest.java index 6fa09a4..07b362a 100644 --- a/src/test/java/ru/pdguard/BankTypesTest.java +++ b/src/test/java/ru/pdguard/BankTypesTest.java @@ -56,6 +56,26 @@ class BankTypesTest { assertFalse(masked.matches(".*\\d{15}.*"), "осталась незамаскированная часть номера: " + masked); } + /** + * Контрольная сумма отсекает случайное 13-значное число рядом со словом «ОГРН». + * Число подобрано так, чтобы не проходить заодно и Луна — иначе оно всё равно + * маскировалось бы, но уже как номер карты, и тест ничего бы не показывал. + */ + @Test + void doesNotMaskOgrnWithBrokenChecksum() { + String text = "ОГРН 1027700132190 организации"; + assertEquals(text, pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT), + "число с неверной контрольной суммой не является настоящим ОГРН"); + } + + /** Та же проверка для ОГРНИП — случайное 15-значное число рядом со словом. */ + @Test + void doesNotMaskOgrnipWithBrokenChecksum() { + String text = "ОГРНИП 304500116000150 предпринимателя"; + assertEquals(text, pipeline.process(text, UUID.randomUUID().toString(), SystemPolicy.DEFAULT), + "число с неверной контрольной суммой не является настоящим ОГРНИП"); + } + @Test void masksKpp() { assertHidden("КПП 770101001 указан в реквизитах", "770101001"); diff --git a/src/test/java/ru/pdguard/detect/ValidatorsTest.java b/src/test/java/ru/pdguard/detect/ValidatorsTest.java new file mode 100644 index 0000000..261c1c9 --- /dev/null +++ b/src/test/java/ru/pdguard/detect/ValidatorsTest.java @@ -0,0 +1,69 @@ +package ru.pdguard.detect; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Контрольные суммы ОГРН/ОГРНИП: первые 12/14 цифр по модулю 11/13, младший разряд остатка. */ +class ValidatorsTest { + + @Test + void validOgrnPassesChecksum() { + assertTrue(Validators.ogrn("1027700123450")); + assertTrue(Validators.ogrn("1025000678900")); + assertTrue(Validators.ogrn("1045002233440")); + } + + @Test + void invalidOgrnChecksumFails() { + assertFalse(Validators.ogrn("1027700123451"), "последняя цифра изменена — сумма не сходится"); + } + + @Test + void ogrnWrongLengthFails() { + assertFalse(Validators.ogrn("102770012345"), "12 цифр — не хватает контрольной"); + assertFalse(Validators.ogrn("10277001234500"), "14 цифр — лишняя"); + } + + /** Остаток от деления на 11 может быть 10 — тогда контрольная цифра 0, не 10. */ + @Test + void ogrnRemainderTenMapsToZero() { + assertTrue(Validators.ogrn("1000000000000")); + } + + @Test + void validOgrnipPassesChecksum() { + assertTrue(Validators.ogrnip("304500116000157")); + assertTrue(Validators.ogrnip("312500000000013")); + assertTrue(Validators.ogrnip("305500112233041")); + } + + @Test + void invalidOgrnipChecksumFails() { + assertFalse(Validators.ogrnip("304500116000158"), "последняя цифра изменена — сумма не сходится"); + } + + @Test + void ogrnipWrongLengthFails() { + assertFalse(Validators.ogrnip("30450011600015"), "14 цифр — не хватает контрольной"); + assertFalse(Validators.ogrnip("30450011600015700"), "18 цифр — лишние"); + } + + /** Остаток от деления на 13 может быть 10 — тогда контрольная цифра 0, не 10. */ + @Test + void ogrnipRemainderTenMapsToZero() { + assertTrue(Validators.ogrnip("100000000000000")); + } + + @Test + void nonDigitCharactersAreIgnored() { + assertTrue(Validators.ogrn("10-27700-123450"), "разделители в номере не мешают счёту цифр"); + } + + @Test + void emptyValueIsNotValid() { + assertFalse(Validators.ogrn("")); + assertFalse(Validators.ogrnip("")); + } +}