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");
