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

kwin pushed a commit to branch feature/escape-resource-names
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git

commit 2f40662dc0dacf32012836cd8450a231beac172e
Author: Konrad Windszus <[email protected]>
AuthorDate: Mon Jun 2 18:46:30 2025 +0200

    SLING-12815 Expose methods to escape/unescape resource names
    
    This takes care of escaping characters not allowed by Sling API in
    resource names, namely "/" and names only consisting of ".".
    Highlight in javadocs where the escape methods may be useful.
---
 .../sling/api/resource/ResourceResolver.java       |  5 +-
 .../apache/sling/api/resource/ResourceUtil.java    | 66 ++++++++++++++++++++++
 .../sling/api/resource/SyntheticResource.java      |  9 ++-
 .../sling/api/resource/ResourceUtilTest.java       | 36 ++++++++----
 4 files changed, 102 insertions(+), 14 deletions(-)

diff --git a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java 
b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
index 51339d2..04e09c6 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
@@ -771,17 +771,18 @@ public interface ResourceResolver extends Adaptable, 
Closeable {
      * The changes are transient and require a call to {@link #commit()} for 
persisting.
      *
      * @param parent The parent resource
-     * @param name   The name of the child resource - this is a plain name, 
not a path!
+     * @param name   The name of the child resource - this is a plain name, 
not a path! The name must neither contain a slash and nor consist out of dots 
only. The underlying resource provider may impose further restrictions on the 
name.
      * @param properties Optional properties for the resource
      * @return The new resource
      *
      * @throws NullPointerException if the resource parameter or name 
parameter is null
-     * @throws IllegalArgumentException if the name contains a slash
+     * @throws IllegalArgumentException if the name contains a slash or 
consists out of dots only.
      * @throws UnsupportedOperationException If the underlying resource 
provider does not support write operations.
      * @throws PersistenceException If the operation fails in the underlying 
resource provider, e.g. in case a resource of that name does already exist.
      * @throws IllegalStateException if this resource resolver has already been
      *             {@link #close() closed}.
      * @since 2.2 (Sling API Bundle 2.2.0)
+     * @see ResourceUtil#escapeForName(String)
      */
     @NotNull
     Resource create(@NotNull Resource parent, @NotNull String name, 
Map<String, Object> properties)
diff --git a/src/main/java/org/apache/sling/api/resource/ResourceUtil.java 
b/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
index 9c2f816..10291fa 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceUtil.java
@@ -19,12 +19,17 @@
 package org.apache.sling.api.resource;
 
 import java.text.MessageFormat;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.sling.api.wrappers.ValueMapDecorator;
 import org.jetbrains.annotations.NotNull;
@@ -39,6 +44,7 @@ import org.jetbrains.annotations.Nullable;
  */
 public class ResourceUtil {
 
+    private static final Pattern UNICODE_ESCAPE_SEQUENCE_PATTERN = 
Pattern.compile("\\\\u[0-9a-fA-F]{4}");
     /**
      * Resolves relative path segments '.' and '..' in the path.
      * The path can either be relative or absolute. Relative paths are treated
@@ -291,6 +297,66 @@ public class ResourceUtil {
         return normalizedPath.substring(normalizedPath.lastIndexOf('/') + 1);
     }
 
+    /**
+     * Escapes the given <code>name</code> for use in a resource name. It 
escapes all invalid characters according to Sling API, i.e.
+     * it escapes the slash and names only consisting of dots. It uses Java 
UTF-16 unicode escape sequences for those characters.
+     * @param name
+     * @return the escaped name
+     * @see ResourceResolver#create(Resource, String, Map)
+     * @see SyntheticResource#create(ResourceResolver, String, Map)
+     * @see <a href="https://www.rfc-editor.org/rfc/rfc5137#section-6.3";>RFC 
5137, section 6.3</a>
+     * @since 2.14.0 (Sling API Bundle 3.0.0)
+     */
+    public static @NotNull String escapeName(@NotNull String name) {
+        if (name.chars().allMatch(c -> c == '.')) {
+            return escapeWithUnicode(name, '.');
+        }
+        return escapeWithUnicode(name, '/');
+    }
+
+    /**
+     * Unescapes the given <code>escapedName</code> previously escaped using 
{@link #escapeForName(String)}.
+     * It replaces the unicode escape sequences with the original characters.
+     *
+     * @param escapedName The escaped name to unescape.
+     * @return The unescaped name.
+     * @see Resource#getName()
+     * @see <a href="https://www.rfc-editor.org/rfc/rfc5137#section-6.3";>RFC 
5137, section 6.3</a>
+     * @since 2.14.0 (Sling API Bundle 3.0.0)
+     */
+    public static @NotNull String unescapeName(@NotNull String escapedName) {
+        return unescapeWithUnicode(escapedName);
+    }
+    
+    private static String escapeWithUnicode(String text, Character... 
additionalCharactersToEscape) {
+        List<Character> charactersToEscape = new LinkedList<>();
+        charactersToEscape.add('\\'); // always escape the backslash as it 
used for unicode escaping itself
+        charactersToEscape.addAll(Arrays.asList(additionalCharactersToEscape));
+        for (Character characterToEscape : charactersToEscape) {
+            String escapedChar = getUnicodeEscapeSequence(characterToEscape);
+            text = text.replace(characterToEscape.toString(), escapedChar);
+        }
+        return text;
+    }
+
+    private static String getUnicodeEscapeSequence(char c) {
+        return String.format("\\u%04X", (int) c);
+    }
+
+    private static String unescapeWithUnicode(String escapedText) {
+        Matcher matcher = UNICODE_ESCAPE_SEQUENCE_PATTERN.matcher(escapedText);
+
+        StringBuilder decodedString = new StringBuilder();
+
+        while (matcher.find()) {
+            String unicodeSequence = matcher.group();
+            char unicodeChar = (char) 
Integer.parseInt(unicodeSequence.substring(2), 16);
+            matcher.appendReplacement(decodedString, 
Character.toString(unicodeChar));
+        }
+        matcher.appendTail(decodedString);
+        return decodedString.toString();
+    }
+
     /**
      * Returns <code>true</code> if the resource <code>res</code> is a 
synthetic
      * resource.
diff --git a/src/main/java/org/apache/sling/api/resource/SyntheticResource.java 
b/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
index d869aa0..b66245c 100644
--- a/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
+++ b/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
@@ -43,13 +43,18 @@ public class SyntheticResource extends AbstractResource {
      * Creates a synthetic resource with the given <code>path</code> and
      * <code>resourceType</code>.
      * @param resourceResolver The resource resolver
-     * @param path The resource path
+     * @param path The absolute resource path including the name. Make sure 
that each segment of the path only contains valid characters in Sling API 
resource names.
      * @param resourceType The type of the resource
+     * @throws IllegalArgumentException If the path is not valid
+     * @see ResourceUtil#escapeName(String)
      */
     public SyntheticResource(
             @NotNull ResourceResolver resourceResolver, @NotNull String path, 
@NotNull String resourceType) {
         this.resourceResolver = resourceResolver;
-        this.path = path;
+        this.path = ResourceUtil.normalize(path); // resolve ".." and "." 
because otherwise the resource cannot be retrieved
+        if (this.path == null) {
+            throw new IllegalArgumentException("Invalid resource path: " + 
path);
+        }
         this.resourceType = resourceType;
         this.resourceMetadata = new ResourceMetadata();
         this.resourceMetadata.setResolutionPath(path);
diff --git a/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java 
b/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
index 8826020..1f9c597 100644
--- a/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
+++ b/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
@@ -18,16 +18,6 @@
  */
 package org.apache.sling.api.resource;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
-
-import org.apache.sling.api.wrappers.ValueMapDecorator;
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -38,6 +28,16 @@ import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.junit.Test;
+
 public class ResourceUtilTest {
 
     @Test
@@ -433,4 +433,20 @@ public class ResourceUtilTest {
     public void testFindResourceSuperType() {
         assertNull(ResourceUtil.findResourceSuperType(null));
     }
+
+    @Test
+    public void testEscapeAndUnescapeNameWithSlash() {
+        String nameWithSpecialChars = "this/is/..//u1234test";
+        String escapedName = ResourceUtil.escapeName(nameWithSpecialChars);
+        assertEquals(nameWithSpecialChars, 
ResourceUtil.unescapeName(escapedName));
+        assertFalse(escapedName.contains("/"));
+    }
+
+    @Test
+    public void testEscapeAndUnescapeNameWithDots() {
+        String nameWithSpecialChars = "....";
+        String escapedName = ResourceUtil.escapeName(nameWithSpecialChars);
+        assertEquals(nameWithSpecialChars, 
ResourceUtil.unescapeName(escapedName));
+        assertFalse(escapedName.contains("."));
+    }
 }

Reply via email to