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

kwin 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 5767e54  SLING-12815 Expose methods to escape/unescape resource names 
(#60)
5767e54 is described below

commit 5767e54f23e043bce2b2d3785b467b053d06dda4
Author: Konrad Windszus <[email protected]>
AuthorDate: Tue Jun 17 14:15:38 2025 +0200

    SLING-12815 Expose methods to escape/unescape resource names (#60)
    
    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      |  3 +-
 .../sling/api/resource/ResourceUtilTest.java       | 16 ++++++
 4 files changed, 87 insertions(+), 3 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 5f26517..a3197cc 100644
--- a/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
+++ b/src/main/java/org/apache/sling/api/resource/ResourceResolver.java
@@ -780,17 +780,18 @@ public interface ResourceResolver extends Adaptable, 
Closeable {
      * The optional resource super type is determined by the property {@value 
ResourceResolver#PROPERTY_RESOURCE_SUPER_TYPE}.
      *
      * @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 nor consist out of dots only. 
The underlying resource provider may impose further restrictions on the name.
      * @param properties Optional properties for the resource (may be 
<code>null</code>).
      * @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#escapeName(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..e3355cf 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#SyntheticResource(ResourceResolver, String, 
String)
+     * @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 #escapeName(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 4fd047b..86f17c0 100644
--- a/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
+++ b/src/main/java/org/apache/sling/api/resource/SyntheticResource.java
@@ -48,8 +48,9 @@ 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
+     * @see ResourceUtil#escapeName(String)
      */
     public SyntheticResource(
             @NotNull ResourceResolver resourceResolver, @NotNull String path, 
@NotNull String resourceType) {
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..b957bd0 100644
--- a/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
+++ b/src/test/java/org/apache/sling/api/resource/ResourceUtilTest.java
@@ -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