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)