This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch 
WW-5674-isclassbelongstopackages-allocation
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 197071def50f5b7a5b6febcd63b4b2f8132146db
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Aug 3 12:54:27 2026 +0200

    WW-5674 test(ognl): characterise SecurityMemberAccess package matching
    
    Pins the current behaviour of isClassBelongsToPackages and toPackageName
    before the WW-5674 rewrite, including the default-package empty-string edge
    reachable via struts.excludedPackageNames="." and the package-boundary case
    where org.apache.struts2x must not match org.apache.struts2.
---
 .../SecurityMemberAccessPackageMatchingTest.java   | 178 +++++++++++++++++++++
 1 file changed, 178 insertions(+)

diff --git 
a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
 
b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
new file mode 100644
index 000000000..9f8af5556
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.ognl;
+
+import org.apache.struts2.util.ConfigParseUtil;
+import org.junit.Test;
+
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
+
+import static java.util.Collections.emptySet;
+import static 
org.apache.struts2.ognl.SecurityMemberAccess.isClassBelongsToPackages;
+import static org.apache.struts2.ognl.SecurityMemberAccess.toPackageName;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Characterisation and equivalence tests for the static package-matching 
helpers in
+ * {@link SecurityMemberAccess}, covering WW-5674.
+ * <p>
+ * These helpers gate OGNL member access, so the rewrite in WW-5674 must be 
exactly
+ * behaviour-preserving. That is proven here by running the replaced 
implementation
+ * side by side with the new one over a matrix of inputs.
+ */
+public class SecurityMemberAccessPackageMatchingTest {
+
+    /**
+     * The implementation replaced by WW-5674, retained verbatim apart from 
taking the package
+     * name directly instead of a {@link Class}. Used as the reference oracle 
for the rewrite.
+     */
+    private static boolean legacyPrefixMatch(String packageName, Set<String> 
matchingPackages) {
+        List<String> packageParts = List.of(packageName.split("\\."));
+        return IntStream.range(0, packageParts.size())
+                .mapToObj(i -> String.join(".", packageParts.subList(0, i + 
1)))
+                .anyMatch(matchingPackages::contains);
+    }
+
+    /**
+     * The {@code toPackageName} implementation replaced by WW-5674, retained 
as the reference oracle.
+     */
+    private static String legacyToPackageName(Class<?> clazz) {
+        if (clazz.getPackage() == null) {
+            return "";
+        }
+        return clazz.getPackage().getName();
+    }
+
+    /**
+     * Package-name shapes. Deliberately excludes trailing-dot inputs such as 
{@code "a.b."}:
+     * {@code split} drops trailing empty segments where an index walk would 
not, and
+     * {@code Class.getPackage().getName()} cannot produce a trailing dot, so 
the shape is
+     * unreachable through every caller. See the spec's "Verified current 
semantics" section.
+     */
+    private static final List<String> PACKAGE_NAMES = List.of(
+            "",
+            "java",
+            "a.b.c",
+            "a..b",
+            ".a",
+            "org.apache.struts2",
+            "org.apache.struts2.ognl",
+            "org.apache.struts2x",
+            "java.io",
+            "java.io.tmp",
+            "javax.servlet.http");
+
+    private static final List<Set<String>> CANDIDATE_SETS = List.of(
+            emptySet(),
+            Set.of(""),
+            Set.of("java"),
+            Set.of("java.io"),
+            Set.of("org.apache.struts2"),
+            Set.of("a"),
+            Set.of("a.b"),
+            Set.of("zzz.not.matching"),
+            Set.of("java.io", "org.apache.struts2", "javax"));
+
+    private static List<Class<?>> classShapes() throws Exception {
+        return List.of(
+                String.class,
+                Map.Entry.class,
+                SecurityMemberAccess.class,
+                Class.forName("PackagelessAction"),
+                int.class,
+                void.class,
+                int[].class,
+                String[].class,
+                String[][].class,
+                ((Runnable) () -> {
+                }).getClass(),
+                Proxy.newProxyInstance(
+                        
SecurityMemberAccessPackageMatchingTest.class.getClassLoader(),
+                        new Class<?>[]{Runnable.class},
+                        (proxy, method, args) -> null).getClass());
+    }
+
+    @Test
+    public void siblingPackageWithSharedCharacterPrefixDoesNotMatch() {
+        Set<String> excluded = Set.of("org.apache.struts2");
+
+        assertThat(legacyPrefixMatch("org.apache.struts2x", excluded))
+                .as("a sibling package sharing a character prefix must not 
match")
+                .isFalse();
+        assertThat(legacyPrefixMatch("org.apache.struts2", excluded))
+                .as("an exact match must match")
+                .isTrue();
+        assertThat(legacyPrefixMatch("org.apache.struts2.ognl", excluded))
+                .as("a sub-package must match")
+                .isTrue();
+    }
+
+    @Test
+    public void dotOnlyConfigurationYieldsEmptyStringPackageName() {
+        assertThat(ConfigParseUtil.toPackageNamesSet("."))
+                .as("struts.excludedPackageNames=\".\" strips to the empty 
string")
+                .containsExactly("");
+    }
+
+    @Test
+    public void defaultPackageMatchesOnlyWhenEmptyStringConfigured() throws 
Exception {
+        Class<?> packageless = Class.forName("PackagelessAction");
+
+        assertThat(toPackageName(packageless)).isEmpty();
+        assertThat(isClassBelongsToPackages(packageless, Set.of("")))
+                .as("a default-package class is matched by the empty-string 
entry")
+                .isTrue();
+        assertThat(isClassBelongsToPackages(packageless, Set.of("java")))
+                .as("a default-package class is not matched by an unrelated 
entry")
+                .isFalse();
+    }
+
+    @Test
+    public void toPackageNameMatchesLegacyAcrossClassShapes() throws Exception 
{
+        for (Class<?> clazz : classShapes()) {
+            assertThat(toPackageName(clazz))
+                    .as("toPackageName(%s)", clazz.getName())
+                    .isEqualTo(legacyToPackageName(clazz));
+        }
+    }
+
+    @Test
+    public void arraysAndPrimitivesResolveToTheEmptyPackage() {
+        assertThat(toPackageName(int.class)).isEmpty();
+        assertThat(toPackageName(void.class)).isEmpty();
+        assertThat(toPackageName(int[].class)).isEmpty();
+        assertThat(toPackageName(String[].class)).isEmpty();
+        assertThat(toPackageName(String[][].class)).isEmpty();
+    }
+
+    @Test
+    public void classEntryPointMatchesLegacyAcrossCandidateSets() throws 
Exception {
+        for (Class<?> clazz : classShapes()) {
+            for (Set<String> candidates : CANDIDATE_SETS) {
+                assertThat(isClassBelongsToPackages(clazz, candidates))
+                        .as("clazz=[%s] candidates=%s", clazz.getName(), 
candidates)
+                        
.isEqualTo(legacyPrefixMatch(legacyToPackageName(clazz), candidates));
+            }
+        }
+    }
+}

Reply via email to