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:**/*.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");