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

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

commit ea22019257af335cfb6ff5860bd4b1ebc2cc965b
Author: Joerg Hoh <[email protected]>
AuthorDate: Thu Nov 12 15:44:39 2026 +0100

    SLING-13426 PathBuilder returns normalized paths
---
 .../sling/api/resource/path/PathBuilder.java       | 35 +++++++++++++++++-----
 .../sling/api/resource/path/PathBuilderTest.java   | 28 +++++++++++++++++
 2 files changed, 55 insertions(+), 8 deletions(-)

diff --git a/src/main/java/org/apache/sling/api/resource/path/PathBuilder.java 
b/src/main/java/org/apache/sling/api/resource/path/PathBuilder.java
index 7a87c76..c98d6bf 100644
--- a/src/main/java/org/apache/sling/api/resource/path/PathBuilder.java
+++ b/src/main/java/org/apache/sling/api/resource/path/PathBuilder.java
@@ -18,8 +18,11 @@
  */
 package org.apache.sling.api.resource.path;
 
+import org.apache.sling.api.resource.ResourceUtil;
+
 /**
- * The {@code PathBuilder} offers a convenient way of creating a valid path 
from multiple fragments
+ * The {@code PathBuilder} offers a convenient way of creating a normalized 
absolute path from multiple fragments.
+ * Normalization does not confine the result to the initial path's subtree.
  *
  * @since 1.0.0 (Sling API Bundle 2.11.0)
  */
@@ -28,9 +31,11 @@ public final class PathBuilder {
     private final StringBuilder sb = new StringBuilder();
 
     /**
-     * Creates a new {@code PathBuilder} instance
+     * Creates a new {@code PathBuilder} instance with an absolute initial 
path.
+     * Normalization is deferred until {@link #toString()} is called.
      *
-     * @param path the initial path
+     * @param path the nonempty initial path, starting with {@code /}
+     * @throws IllegalArgumentException if the path is null, empty, or relative
      */
     public PathBuilder(final String path) {
 
@@ -42,10 +47,15 @@ public final class PathBuilder {
     }
 
     /**
-     * Appends a new path fragment
+     * Appends a path fragment to the accumulated path.
+     * Fragments may be relative and contain {@code .} or {@code ..} segments,
+     * which are resolved when {@link #toString()} is called.
+     * A leading {@code /} does not replace the accumulated path; the fragment
+     * is joined to it with a slash at the boundary.
      *
-     * @param path the path fragment to append
+     * @param path the nonempty path fragment to append
      * @return this instance
+     * @throws IllegalArgumentException if the fragment is null or empty
      */
     public PathBuilder append(final String path) {
 
@@ -68,12 +78,21 @@ public final class PathBuilder {
     }
 
     /**
-     * Returns the path
+     * Returns the normalized path without changing the accumulated fragments.
+     * Dot segments are resolved, duplicate slashes are collapsed, and trailing
+     * slashes are removed except for the root path.
      *
-     * @return the path
+     * @return the normalized absolute path
+     * @throws IllegalArgumentException if the path cannot be normalized, for 
example
+     *         because it traverses above root
      */
     @Override
     public String toString() {
-        return sb.toString();
+        final String path = sb.toString();
+        final String normalizedPath = ResourceUtil.normalize(path);
+        if (normalizedPath == null) {
+            throw new IllegalArgumentException("Path cannot be normalized");
+        }
+        return normalizedPath;
     }
 }
diff --git 
a/src/test/java/org/apache/sling/api/resource/path/PathBuilderTest.java 
b/src/test/java/org/apache/sling/api/resource/path/PathBuilderTest.java
index cb1739c..74f281f 100644
--- a/src/test/java/org/apache/sling/api/resource/path/PathBuilderTest.java
+++ b/src/test/java/org/apache/sling/api/resource/path/PathBuilderTest.java
@@ -55,6 +55,34 @@ public class PathBuilderTest {
         assertThat(new PathBuilder("/parent").append("child").toString(), 
Matchers.equalTo("/parent/child"));
     }
 
+    @Test
+    public void normalizeJoinedPath() {
+        assertThat(
+                new 
PathBuilder("/parent//").append("./child/../other/").toString(), 
Matchers.equalTo("/parent/other"));
+        assertThat(new PathBuilder("/parent").append("..").toString(), 
Matchers.equalTo("/"));
+    }
+
+    @Test
+    public void normalizeInitialPath() {
+        assertThat(new PathBuilder("/parent//child/../").toString(), 
Matchers.equalTo("/parent"));
+        assertThat(new PathBuilder("///").toString(), Matchers.equalTo("/"));
+    }
+
+    @Test
+    public void allowTraversalOutsideInitialSubtree() {
+        assertThat(new 
PathBuilder("/var/inbox").append("../../etc/keys").toString(), 
Matchers.equalTo("/etc/keys"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectTraversalAboveRoot() {
+        new PathBuilder("/parent").append("../../secret").toString();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectUnnormalizableDotSegment() {
+        new PathBuilder("/parent").append(".../child").toString();
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void relativeInitialPaths() {
         new PathBuilder("relative");

Reply via email to