This is an automated email from the ASF dual-hosted git repository. slachiewicz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-remote-resources-plugin.git
commit 43783cc673c9f9b28396177c74c2ece645478551 Author: Sylwester Lachiewicz <[email protected]> AuthorDate: Wed Oct 28 09:30:55 2026 +0000 Reject malformed SPDX expressions and accept LicenseRef IDs --- .../plugin/resources/remote/SpdxLicenses.java | 97 ++++++++++++++++++---- .../plugin/resources/remote/SpdxLicensesTest.java | 8 ++ 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java b/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java index c3d9a96..80b6234 100644 --- a/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java +++ b/src/main/java/org/apache/maven/plugin/resources/remote/SpdxLicenses.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -55,7 +56,9 @@ public class SpdxLicenses { private static final String RESOURCE = "license-aliases.txt"; - private static final Pattern EXPRESSION_TOKEN = Pattern.compile("[^\\s()]+"); + private static final Pattern EXPRESSION_TOKEN = Pattern.compile("\\(|\\)|[^\\s()]+"); + + private static final Pattern LICENSE_REF = Pattern.compile("LicenseRef-[A-Za-z0-9.-]+"); private static final Pattern ID_URL = Pattern.compile("(?:opensource\\.org/licenses?|spdx\\.org/licenses)/([^/?]+)"); @@ -169,7 +172,7 @@ public class SpdxLicenses { } private String idOfName(String name) { - String id = ids.get(name.toLowerCase(Locale.ROOT)); + String id = knownId(name); if (id == null) { id = parseExpression(name); } @@ -184,36 +187,98 @@ public class SpdxLicenses { if (id == null) { Matcher m = ID_URL.matcher(url); if (m.matches()) { - id = ids.get(m.group(1)); + id = knownId(m.group(1)); } } return id; } + /** Returns the ID in its SPDX spelling, a <code>LicenseRef-</code> ID as declared, or {@code null}. */ + private String knownId(String token) { + String id = ids.get(token.toLowerCase(Locale.ROOT)); + if (id == null && LICENSE_REF.matcher(token).matches()) { + id = token; + } + return id; + } + /** - * Returns the expression with each ID in its SPDX spelling, or {@code null} unless it combines at least - * two known IDs with <code>AND</code>, <code>OR</code> or <code>WITH</code>. + * Returns the expression with each ID in its SPDX spelling, or {@code null} unless it is a well-formed + * SPDX license expression that combines known IDs with <code>AND</code>, <code>OR</code> or + * <code>WITH</code>. */ private String parseExpression(String declared) { + List<String> tokens = new ArrayList<>(); Matcher m = EXPRESSION_TOKEN.matcher(declared); - StringBuffer result = new StringBuffer(); - int operators = 0; while (m.find()) { - String token = m.group(); - String upper = token.toUpperCase(Locale.ROOT); - if (upper.equals("AND") || upper.equals("OR") || upper.equals("WITH")) { + tokens.add(m.group()); + } + ExpressionParser parser = new ExpressionParser(tokens); + String result = parser.expression(); + return result != null && parser.next == tokens.size() && parser.operators > 0 ? result : null; + } + + /** Recursive descent over <code>term ((AND | OR) term)*</code>, where a term is an ID, optionally + * followed by <code>WITH</code> and an ID, or a parenthesised expression. */ + private final class ExpressionParser { + private final List<String> tokens; + + private int next; + + private int operators; + + ExpressionParser(List<String> tokens) { + this.tokens = tokens; + } + + String expression() { + String term = term(); + if (term == null) { + return null; + } + StringBuilder result = new StringBuilder(term); + while (next < tokens.size()) { + String operator = tokens.get(next).toUpperCase(Locale.ROOT); + if (!operator.equals("AND") && !operator.equals("OR")) { + break; + } + next++; operators++; - m.appendReplacement(result, upper); - continue; + term = term(); + if (term == null) { + return null; + } + result.append(' ').append(operator).append(' ').append(term); + } + return result.toString(); + } + + private String term() { + if (next >= tokens.size()) { + return null; + } + String token = tokens.get(next++); + if (token.equals("(")) { + String inner = expression(); + if (inner == null + || next >= tokens.size() + || !tokens.get(next++).equals(")")) { + return null; + } + return "(" + inner + ")"; } - String id = ids.get(token.toLowerCase(Locale.ROOT)); + String id = knownId(token); if (id == null) { return null; } - m.appendReplacement(result, Matcher.quoteReplacement(id)); + if (next < tokens.size() && tokens.get(next).equalsIgnoreCase("WITH")) { + next++; + operators++; + String exception = next < tokens.size() ? knownId(tokens.get(next++)) : null; + return exception == null ? null : id + " WITH " + exception; + } + return id; } - m.appendTail(result); - return operators > 0 ? result.toString() : null; } private static boolean isBlank(String s) { diff --git a/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java b/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java index c13617b..80f4af7 100644 --- a/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java +++ b/src/test/java/org/apache/maven/plugin/resources/remote/SpdxLicensesTest.java @@ -35,6 +35,7 @@ class SpdxLicensesTest { assertEquals("Apache-2.0", spdx.id("Apache-2.0", null)); assertEquals("Apache-2.0", spdx.id("apache-2.0", null)); assertEquals("GPL-2.0", spdx.id("GPL-2.0", null)); + assertEquals("LicenseRef-Widget", spdx.id("LicenseRef-Widget", null)); } @Test @@ -45,6 +46,13 @@ class SpdxLicensesTest { spdx.id("GPL-2.0-only with Classpath-exception-2.0", null)); assertNull(spdx.id("Apache-2.0 OR Unknown-1.0", null)); assertNull(spdx.id("Apache-2.0 MIT", null)); + assertNull(spdx.id("OR", null)); + assertNull(spdx.id("Apache-2.0 OR OR MIT", null)); + assertNull(spdx.id("(Apache-2.0 OR MIT", null)); + assertNull(spdx.id("Apache-2.0 OR MIT)", null)); + assertNull(spdx.id("MIT WITH", null)); + assertEquals("LicenseRef-Widget OR MIT", spdx.id("LicenseRef-Widget or MIT", null)); + assertEquals("(MIT OR Apache-2.0) AND BSD-3-Clause", spdx.id("( mit OR apache-2.0 ) and bsd-3-clause", null)); } @Test
