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 056338b  SLING-13426 PathBuilder returns normalized paths (#94)
056338b is described below

commit 056338b4c4c3467f771c96f5e33df2dcc55ba635
Author: Jörg Hoh <[email protected]>
AuthorDate: Fri Nov 13 13:38:06 2026 +0100

    SLING-13426 PathBuilder returns normalized paths (#94)
    
    add a new method build() to return the normalized path
---
 .../sling/api/resource/path/PathBuilder.java       | 47 ++++++++++++---
 .../sling/api/resource/path/package-info.java      |  2 +-
 .../sling/api/resource/path/PathBuilderTest.java   | 68 +++++++++++++++++++---
 3 files changed, 102 insertions(+), 15 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..7eb8e18 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,13 @@
  */
 package org.apache.sling.api.resource.path;
 
+import org.apache.sling.api.resource.ResourceUtil;
+import org.jetbrains.annotations.NotNull;
+
 /**
- * The {@code PathBuilder} offers a convenient way of creating a valid path 
from multiple fragments
+ * The {@code PathBuilder} offers a convenient way of creating an absolute 
path from multiple fragments.
+ * Use {@link #build()} to obtain a normalized path.
+ * Normalization does not confine the result to the initial path's subtree.
  *
  * @since 1.0.0 (Sling API Bundle 2.11.0)
  */
@@ -28,9 +33,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 performed only when {@link #build()} 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 +49,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 #build()} 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 +80,33 @@ public final class PathBuilder {
     }
 
     /**
-     * Returns the path
+     * Returns the joined path without normalization.
+     * In most cases, {@link #build()} is the better choice, as it
+     * resolves dot segments and removes redundant slashes.
      *
-     * @return the path
+     * @return the unnormalized absolute path
      */
     @Override
     public String toString() {
         return sb.toString();
     }
+
+    /**
+     * 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 normalized absolute path
+     * @throws IllegalArgumentException if the path cannot be normalized, for 
example
+     *         because it traverses above root
+     * @since 1.3.0
+     */
+    public @NotNull String build() {
+        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/main/java/org/apache/sling/api/resource/path/package-info.java 
b/src/main/java/org/apache/sling/api/resource/path/package-info.java
index 5de1514..4d2c5d7 100644
--- a/src/main/java/org/apache/sling/api/resource/path/package-info.java
+++ b/src/main/java/org/apache/sling/api/resource/path/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("1.2.3")
+@Version("1.3.0")
 package org.apache.sling.api.resource.path;
 
 import org.osgi.annotation.versioning.Version;
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..31dd03f 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
@@ -18,41 +18,95 @@
  */
 package org.apache.sling.api.resource.path;
 
-import org.hamcrest.Matchers;
 import org.junit.Test;
 
-import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
 
 public class PathBuilderTest {
 
     @Test
     public void noChangeNeeded_root() {
 
-        assertThat(new PathBuilder("/").append("path").toString(), 
Matchers.equalTo("/path"));
+        assertEquals("/path", new PathBuilder("/").append("path").toString());
     }
 
     @Test
     public void noChangeNeeded_intermediate() {
 
-        assertThat(new PathBuilder("/parent").append("/child").toString(), 
Matchers.equalTo("/parent/child"));
+        assertEquals(
+                "/parent/child", new 
PathBuilder("/parent").append("/child").toString());
     }
 
     @Test
     public void removeSlash_root() {
 
-        assertThat(new PathBuilder("/").append("/path").toString(), 
Matchers.equalTo("/path"));
+        assertEquals("/path", new PathBuilder("/").append("/path").toString());
     }
 
     @Test
     public void removeSlash_intermediate() {
 
-        assertThat(new PathBuilder("/parent/").append("/child").toString(), 
Matchers.equalTo("/parent/child"));
+        assertEquals(
+                "/parent/child", new 
PathBuilder("/parent/").append("/child").toString());
     }
 
     @Test
     public void addSlash() {
 
-        assertThat(new PathBuilder("/parent").append("child").toString(), 
Matchers.equalTo("/parent/child"));
+        assertEquals("/parent/child", new 
PathBuilder("/parent").append("child").toString());
+    }
+
+    @Test
+    public void normalizeJoinedPath() {
+        assertEquals(
+                "/parent/other",
+                new 
PathBuilder("/parent//").append("./child/../other/").build());
+        assertEquals("/", new PathBuilder("/parent").append("..").build());
+    }
+
+    @Test
+    public void normalizeInitialPath() {
+        assertEquals("/parent", new PathBuilder("/parent//child/../").build());
+        assertEquals("/", new PathBuilder("///").build());
+    }
+
+    @Test
+    public void allowTraversalOutsideInitialSubtree() {
+        assertEquals(
+                "/etc/keys",
+                new 
PathBuilder("/var/inbox").append("../../etc/keys").build());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectTraversalAboveRoot() {
+        final PathBuilder builder = new 
PathBuilder("/parent").append("../../secret");
+        assertEquals("/parent/../../secret", builder.toString()); // passes
+        builder.build(); // throws
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectUnnormalizableDotSegment() {
+        final PathBuilder builder = new 
PathBuilder("/parent").append(".../child");
+        assertEquals("/parent/.../child", builder.toString()); // passes
+        builder.build(); // throws
+    }
+
+    @Test
+    public void toStringPreservesUnnormalizedPath() {
+        final PathBuilder builder = new 
PathBuilder("/parent//").append("./child/../other/");
+        assertEquals("/parent//./child/../other/", builder.toString());
+        assertEquals("/parent/other", builder.build());
+        assertEquals("/parent//./child/../other/", builder.toString());
+    }
+
+    @Test
+    public void toStringAllowsUnnormalizablePaths() {
+        assertEquals(
+                "/parent/../../secret",
+                new PathBuilder("/parent").append("../../secret").toString());
+        assertEquals(
+                "/parent/.../child",
+                new PathBuilder("/parent").append(".../child").toString());
     }
 
     @Test(expected = IllegalArgumentException.class)

Reply via email to