diff --git a/hsregex/build.gradle b/hsregex/build.gradle new file mode 100644 index 00000000..d8953b40 --- /dev/null +++ b/hsregex/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + implementation(project(":pgpainless-core")) + + // Henry Spencers Regular Expression (RegEx packets) + implementation 'com.basistech.tclre:tcl-regex:0.14.5' +} + +test { + useJUnitPlatform() +} diff --git a/hsregex/src/main/java/org/pgpainless/algorithm/HSRegexInterpreterFactory.java b/hsregex/src/main/java/org/pgpainless/algorithm/HSRegexInterpreterFactory.java new file mode 100644 index 00000000..8d8a3068 --- /dev/null +++ b/hsregex/src/main/java/org/pgpainless/algorithm/HSRegexInterpreterFactory.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import com.basistech.tclre.HsrePattern; +import com.basistech.tclre.PatternFlags; +import com.basistech.tclre.RePattern; +import com.basistech.tclre.RegexException; + +public class HSRegexInterpreterFactory extends RegexInterpreterFactory { + + public Regex instantiate(String regex) { + return new Regex() { + + private final RePattern pattern; + + { + try { + pattern = HsrePattern.compile(regex, PatternFlags.ADVANCED); + } catch (RegexException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public boolean matches(String string) { + return pattern.matcher(string).find(); + } + }; + } +} diff --git a/hsregex/src/main/java/org/pgpainless/algorithm/package-info.java b/hsregex/src/main/java/org/pgpainless/algorithm/package-info.java new file mode 100644 index 00000000..5dc78706 --- /dev/null +++ b/hsregex/src/main/java/org/pgpainless/algorithm/package-info.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Regex interpreter implementation based on Henry Spencers Regular Expression library. + * + * @see RFC4880 - ยง8. Regular Expressions + */ +package org.pgpainless.algorithm; diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 84b4273f..c6886f36 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -30,4 +30,7 @@ dependencies { // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" + + // HSRE regex for tests + testImplementation project(":hsregex") } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Regex.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Regex.java new file mode 100644 index 00000000..5e23874b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Regex.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.pgpainless.key.util.UserId; + +public interface Regex { + + /** + * Return true, if the regex matches the given user-id. + * + * @param userId userId + * @return true if matches, false otherwise + */ + default boolean matches(UserId userId) { + return matches(userId.toString()); + } + + /** + * Return true, if the regex matches the given string. + * + * @param string string + * @return true if matches, false otherwise + */ + boolean matches(String string); +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RegexInterpreterFactory.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RegexInterpreterFactory.java new file mode 100644 index 00000000..266a245c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RegexInterpreterFactory.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import javax.annotation.Nonnull; +import java.util.regex.Pattern; + +public abstract class RegexInterpreterFactory { + + private static RegexInterpreterFactory INSTANCE; + + public static RegexInterpreterFactory getInstance() { + if (INSTANCE == null) { + INSTANCE = new JavaRegexInterpreterFactory(); + } + return INSTANCE; + } + + public static void setInstance(@Nonnull RegexInterpreterFactory instance) { + INSTANCE = instance; + } + + public static Regex create(String regex) { + return getInstance().instantiate(regex); + } + + public abstract Regex instantiate(String regex) throws IllegalArgumentException; + + public static class JavaRegexInterpreterFactory extends RegexInterpreterFactory { + + @Override + public Regex instantiate(String regex) { + return new Regex() { + + private final Pattern pattern = Pattern.compile(regex); + + @Override + public boolean matches(String string) { + return pattern.matcher(string).find(); + } + }; + } + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RegexTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RegexTest.java new file mode 100644 index 00000000..0eafec9d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RegexTest.java @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.key.util.UserId; + +public class RegexTest { + private static Stream provideRegexInterpreterFactories() { + return Stream.of( + Arguments.of(Named.of("Default JavaRegexInterpreterFactory", + new RegexInterpreterFactory.JavaRegexInterpreterFactory())), + Arguments.of(Named.of("HSRegexInterpreterFactory", + new HSRegexInterpreterFactory())) + ); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void simpleTest(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("Alice|Bob"); + assertTrue(regex.matches("Alice")); + assertTrue(regex.matches("Bob")); + assertFalse(regex.matches("Charlie")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testEmailRegexMatchesDomain(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$"); + assertTrue(regex.matches("Alice ")); + assertTrue(regex.matches("Bob ")); + assertFalse(regex.matches("Alice "), "wrong domain"); + assertFalse(regex.matches("Bob "), "wrong domain"); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testEmailRegexMatchesOnlyWrappedAddresses(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$"); + assertTrue(regex.matches("")); + assertFalse(regex.matches("alice@pgpainless.org"), "only match mails in <>"); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testCaseSensitivity(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$"); + assertFalse(regex.matches("Alice ")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testWildCard(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate(".*"); + assertTrue(regex.matches("")); + assertTrue(regex.matches("Alice")); + assertTrue(regex.matches("")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testExclusion(RegexInterpreterFactory factory) { + // Test [^>] matches all but '>' + Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$"); + assertFalse(regex.matches("appendix@pgpainless.org>")); + assertFalse(regex.matches("<>alice@pgpainless.org>")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testOnlyMatchAtTheEnd(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$"); + assertFalse(regex.matches("Alice ")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testRanges(RegexInterpreterFactory factory) { + Regex regex = factory.instantiate("<[^>]+[0-9][@.]pgpainless\\.org>$"); + + for (int i = 0; i < 10; i++) { + String mail = ""; + assertTrue(regex.matches(mail)); + } + + assertFalse(regex.matches("")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testExactMailMatch(RegexInterpreterFactory factory) { + Regex exactMail = factory.instantiate("$"); + assertTrue(exactMail.matches("")); + assertTrue(exactMail.matches("Exact Match ")); + assertFalse(exactMail.matches("")); + } + + @ParameterizedTest + @MethodSource("provideRegexInterpreterFactories") + public void testSetInstance(RegexInterpreterFactory factory) { + RegexInterpreterFactory before = RegexInterpreterFactory.getInstance(); + RegexInterpreterFactory.setInstance(factory); + + Regex regex = RegexInterpreterFactory.create("<[^>]+[@.]pgpainless\\.org>$"); + assertTrue(regex.matches(UserId.nameAndEmail("Alice", "alice@pgpainless.org"))); + + RegexInterpreterFactory.setInstance(before); + } +} diff --git a/settings.gradle b/settings.gradle index aea19392..9fe2061a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,6 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', 'pgpainless-sop', - 'pgpainless-cli' + 'pgpainless-cli', + 'hsregex'