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

joerghoh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git


The following commit(s) were added to refs/heads/master by this push:
     new 0d2ad59  SLING-13373 Path.matches() should respect ../ segments (#83)
0d2ad59 is described below

commit 0d2ad596714e192084a4ec4a43a057035ee0bdec
Author: Jörg Hoh <[email protected]>
AuthorDate: Thu Oct 8 19:04:35 2026 +0200

    SLING-13373 Path.matches() should respect ../ segments (#83)
---
 .../org/apache/sling/api/resource/path/Path.java   | 45 +++++++++---
 .../apache/sling/api/resource/path/PathSet.java    |  3 +
 .../sling/api/resource/path/PathSetTest.java       | 54 ++++++++++++++
 .../apache/sling/api/resource/path/PathTest.java   | 85 ++++++++++++++++++++++
 4 files changed, 175 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/apache/sling/api/resource/path/Path.java 
b/src/main/java/org/apache/sling/api/resource/path/Path.java
index 77326aa..a83b732 100644
--- a/src/main/java/org/apache/sling/api/resource/path/Path.java
+++ b/src/main/java/org/apache/sling/api/resource/path/Path.java
@@ -20,6 +20,7 @@ package org.apache.sling.api.resource.path;
 
 import java.util.regex.Pattern;
 
+import org.apache.sling.api.resource.ResourceUtil;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -43,6 +44,11 @@ public class Path implements Comparable<Path> {
     /**
      * <p>Create a new path object either from a concrete path or from a glob 
pattern.</p>
      *
+     * <p>Concrete paths are normalized using {@link 
ResourceUtil#normalize(String)}
+     * before being stored, resolving {@code .} and {@code ..} segments and
+     * collapsing consecutive slashes. Paths that cannot be normalized are 
rejected.
+     * Glob pattern segments are not normalized.</p>
+     *
      * <p>A glob pattern must start with the {@code glob:} prefix (e.g. 
<code>glob:**&#47;*.html</code>). The following rules are used
      * to interpret glob patterns:</p>
      * <ul>
@@ -51,16 +57,22 @@ public class Path implements Comparable<Path> {
      * </ul>
      *
      * @param path The resource path or a glob pattern.
-     * @throws NullPointerException If {@code otherPath} is {@code null}
-     * @throws IllegalArgumentException If the provided path is not absolute, 
or if the glob pattern does not start with a slash.
+     * @throws NullPointerException If {@code path} is {@code null}
+     * @throws IllegalArgumentException If the provided path is not absolute or
+     *         cannot be normalized, or if the glob pattern does not start 
with a slash.
      */
     public Path(@NotNull final String path) {
-        if (path.equals("/")) {
-            this.path = "/";
-        } else if (path.endsWith("/")) {
-            this.path = path.substring(0, path.length() - 1);
+        if (path.startsWith(GLOB_PREFIX)) {
+            this.path = path.endsWith("/") ? path.substring(0, path.length() - 
1) : path;
         } else {
-            this.path = path;
+            if (!path.startsWith("/")) {
+                throw new IllegalArgumentException("Path must be absolute: " + 
path);
+            }
+            final String normalizedPath = ResourceUtil.normalize(path);
+            if (normalizedPath == null) {
+                throw new IllegalArgumentException("Path cannot be normalized: 
" + path);
+            }
+            this.path = normalizedPath;
         }
         if (this.path.startsWith(GLOB_PREFIX)) {
             final String patternPath = path.substring(GLOB_PREFIX.length());
@@ -102,8 +114,13 @@ public class Path implements Comparable<Path> {
      * provided path matches the pattern. If this path object holds a pattern
      * and a pattern is provided as the argument, it returns only {@code true}
      * if the pattern is the same.
-     * If the provided argument is not an absolute path (e.g. if it is a 
relative
-     * path or a pattern), this method returns {@code false}.
+     * If the provided argument is a concrete path, it is normalized using
+     * {@link ResourceUtil#normalize(String)} before matching, resolving 
{@code .}
+     * and {@code ..} segments and collapsing consecutive slashes.
+     * If normalization fails, this method returns {@code false}.
+     * Glob pattern arguments retain their pattern semantics.
+     * If the provided argument is not an absolute path or an absolute glob
+     * pattern, this method throws {@code IllegalArgumentException}.
      *
      * @param otherPath Absolute path to check.
      * @return {@code true} If other path is within the sub tree of this path
@@ -153,14 +170,18 @@ public class Path implements Comparable<Path> {
         if (!otherPath.startsWith("/")) {
             throw new IllegalArgumentException("Path must be absolute: " + 
otherPath);
         }
+        final String normalizedOtherPath = ResourceUtil.normalize(otherPath);
+        if (normalizedOtherPath == null) {
+            return false;
+        }
         if (isPattern) {
-            return this.regexPattern.matcher(otherPath).matches();
+            return this.regexPattern.matcher(normalizedOtherPath).matches();
         }
-        return this.path.equals(otherPath) || 
otherPath.startsWith(this.prefix);
+        return this.path.equals(normalizedOtherPath) || 
normalizedOtherPath.startsWith(this.prefix);
     }
 
     /**
-     * Return the path if this {@code Path} object holds a path,
+     * Return the normalized path if this {@code Path} object holds a path,
      * returns the pattern otherwise.
      * @return The path or pattern.
      * @see #isPattern()
diff --git a/src/main/java/org/apache/sling/api/resource/path/PathSet.java 
b/src/main/java/org/apache/sling/api/resource/path/PathSet.java
index 3bb6324..373cb28 100644
--- a/src/main/java/org/apache/sling/api/resource/path/PathSet.java
+++ b/src/main/java/org/apache/sling/api/resource/path/PathSet.java
@@ -125,6 +125,9 @@ public class PathSet implements Iterable<Path> {
     /**
      * Check whether the provided path is in the sub tree of any
      * of the paths in this set.
+     * Concrete path arguments are normalized before matching; paths that
+     * cannot be normalized do not match. Glob pattern arguments retain their
+     * pattern semantics.
      * @param otherPath The path to match
      * @return The path which matches the provided path, {@code null} 
otherwise.
      * @see Path#matches(String)
diff --git a/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java 
b/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java
index 734eadd..b8b2617 100644
--- a/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java
+++ b/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java
@@ -113,6 +113,60 @@ public class PathSetTest {
         assertEquals(new Path("/x/y"), set.matches("/x/y/g/e"));
     }
 
+    @Test
+    public void testMatchingNormalizesTraversalSegments() {
+        for (final String entry : new String[] {"/apps", "glob:/apps/**"}) {
+            final PathSet set = PathSet.fromStrings(entry);
+
+            assertNull(set.matches("/apps/../etc/secret"));
+            assertNull(set.matches("/apps/foo/../../etc"));
+            assertNull(set.matches("/../apps/foo"));
+            assertNull(set.matches("/apps/..."));
+            assertEquals(new Path(entry), set.matches("/apps/foo/../bar"));
+            assertEquals(new Path(entry), set.matches("/libs/../apps/foo"));
+            assertEquals(new Path(entry), set.matches("/apps//foo"));
+        }
+    }
+
+    @Test
+    public void testSubsetNormalizesTraversalSegments() {
+        final PathSet set = PathSet.fromStrings("/apps/../etc/secret", 
"/apps/foo");
+
+        assertEqualSets(set.getSubset("/apps"), "/apps/foo");
+        assertEqualSets(set.getSubset(PathSet.fromStrings("/apps")), 
"/apps/foo");
+    }
+
+    @Test
+    public void testNonNormalizedEntriesMatchTheirCanonicalSubtree() {
+        final PathSet set = PathSet.fromStrings("/apps/../etc", 
"//content//./site/");
+
+        assertEqualSets(set, "/etc", "/content/site");
+        assertEquals(new Path("/etc"), set.matches("/apps/../etc"));
+        assertEquals(new Path("/etc"), set.matches("/etc/secret"));
+        assertEquals(new Path("/content/site"), 
set.matches("/content/site/page"));
+        assertNull(set.matches("/apps"));
+        assertNull(set.matches("/etc2/secret"));
+    }
+
+    @Test
+    public void testSubsetsWithNonNormalizedEntriesAndFilters() {
+        final PathSet set = PathSet.fromStrings("/apps/../etc/secret", 
"/content");
+        final PathSet filter = PathSet.fromStrings("/apps/../etc");
+
+        assertEqualSets(set.getSubset(filter), "/etc/secret");
+        assertEqualSets(set.getSubset(new Path("//etc/./")), "/etc/secret");
+        assertEqualSets(set.getSubset("/apps/../etc"), "/etc/secret");
+        assertEqualSets(set.getSubset(set), "/etc/secret", "/content");
+    }
+
+    @Test
+    public void testOptimizeCanonicalizesAndDeduplicatesConcreteEntries() {
+        final PathSet set = PathSet.fromStrings("/apps/../etc", "/etc", 
"//etc/secret/.");
+
+        assertEqualSets(set, "/etc");
+        assertNotNull(set.matches("/etc/secret/value"));
+    }
+
     @Test
     public void testToStringSet() {
         final PathSet set = PathSet.fromStrings("/a", "/x/y");
diff --git a/src/test/java/org/apache/sling/api/resource/path/PathTest.java 
b/src/test/java/org/apache/sling/api/resource/path/PathTest.java
index 0450693..10062ed 100644
--- a/src/test/java/org/apache/sling/api/resource/path/PathTest.java
+++ b/src/test/java/org/apache/sling/api/resource/path/PathTest.java
@@ -22,7 +22,9 @@ import java.util.UUID;
 
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -63,6 +65,89 @@ public class PathTest {
         assertMatch(p, "/content", "/content/a", "/content/a/b");
     }
 
+    @Test
+    public void testConstructorNormalizesConcretePaths() {
+        final Path canonical = new Path("/etc");
+        for (final String input : new String[] {"/apps/../etc", "/etc/.", 
"//etc//", "/etc/child/.."}) {
+            final Path path = new Path(input);
+
+            assertEquals("/etc", path.getPath());
+            assertMatch(path, input, path.getPath(), "/etc/secret");
+            assertNoMatch(path, "/apps", "/etc2/secret");
+            assertEquals(canonical, path);
+            assertEquals(canonical.hashCode(), path.hashCode());
+            assertEquals(0, canonical.compareTo(path));
+        }
+    }
+
+    @Test
+    public void testConstructorNormalizesRootPaths() {
+        for (final String input : new String[] {"/", "///", "/apps/..", "/."}) 
{
+            final Path path = new Path(input);
+
+            assertEquals("/", path.getPath());
+            assertMatch(path, input, "/", "/etc/secret");
+        }
+    }
+
+    @Test
+    public void testConstructorRejectsInvalidConcretePaths() {
+        for (final String input :
+                new String[] {"/../etc", "/apps/../../etc", "/apps/...", 
"/apps/..../etc", "", "etc"}) {
+            assertThrows(IllegalArgumentException.class, () -> new 
Path(input));
+        }
+    }
+
+    @Test
+    public void testConstructorPreservesGlobSegments() {
+        final String pattern = "glob:/apps/*/../**";
+        final Path path = new Path(pattern);
+
+        assertTrue(path.isPattern());
+        assertEquals(pattern, path.getPath());
+        assertMatch(path, pattern);
+    }
+
+    @Test
+    public void testMatchesNormalizesTraversalSegments() {
+        final Path path = new Path("/apps");
+
+        assertNoMatch(path, "/apps/../etc/secret", "/apps/./../etc/secret", 
"/apps/foo/../../etc");
+        assertMatch(
+                path,
+                "/apps/foo/../bar",
+                "/libs/../apps/foo",
+                "/apps//foo",
+                "/apps/foo/.",
+                "/apps/foo/..",
+                "/apps/.hidden",
+                "/apps/foo..bar");
+        assertNoMatch(path, "/../apps/foo", "/apps/../../..", "/apps/...", 
"/apps/..../foo");
+    }
+
+    @Test
+    public void testPatternMatchNormalizesTraversalSegments() {
+        final Path glob = new Path("glob:/apps/**");
+
+        assertNoMatch(glob, "/apps/../etc/secret", "/apps/foo/../../etc", 
"/../apps/foo", "/apps/...");
+        assertMatch(glob, "/apps/foo/../bar", "/libs/../apps/foo", 
"/apps//foo");
+
+        final Path singleSegmentGlob = new Path("glob:/apps/*");
+        assertMatch(singleSegmentGlob, "/apps/foo/../bar", "/apps//bar");
+        assertNoMatch(singleSegmentGlob, "/apps/foo/bar", "/apps/../etc");
+    }
+
+    @Test
+    public void testRootMatchRejectsInvalidPaths() {
+        final Path root = new Path("/");
+        final Path glob = new Path("glob:/**");
+
+        assertNoMatch(root, "/../apps", "/apps/../../etc", "/apps/...");
+        assertNoMatch(glob, "/../apps", "/apps/../../etc", "/apps/...");
+        assertMatch(root, "/apps/..", "//apps//foo", "/apps/.");
+        assertMatch(glob, "/apps/..", "//apps//foo", "/apps/.");
+    }
+
     @Test
     public void testPatternMatchingA() {
         final Path p = new Path("glob:/apps/**/*.html");

Reply via email to