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

Gargi-jais11 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new b18ea9284c4 HDDS-15600. Fix ListObjects response for encoding-type and 
empty delimiter (#10586).
b18ea9284c4 is described below

commit b18ea9284c4acd3753409888e77e2baf20931b27
Author: Gargi Jaiswal <[email protected]>
AuthorDate: Wed Jul 15 10:10:59 2026 +0530

    HDDS-15600. Fix ListObjects response for encoding-type and empty delimiter 
(#10586).
---
 .../hadoop/ozone/om/OmMetadataManagerImpl.java     |  4 +--
 .../hadoop/ozone/om/TestOmMetadataManager.java     | 33 +++++++++++++++++++
 .../ozone/s3/commontypes/ObjectKeyNameAdapter.java |  3 +-
 .../ozone/s3/commontypes/RequestParameters.java    | 11 +++++++
 .../hadoop/ozone/s3/endpoint/BucketEndpoint.java   | 12 ++++---
 .../org/apache/hadoop/ozone/s3/util/S3Utils.java   | 11 +++++++
 .../s3/commontypes/TestObjectKeyNameAdapter.java   |  2 +-
 .../hadoop/ozone/s3/endpoint/TestBucketList.java   | 38 ++++++++++++++++++++--
 8 files changed, 103 insertions(+), 11 deletions(-)

diff --git 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
index 69b1ef67bfe..283bb493358 100644
--- 
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
+++ 
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmMetadataManagerImpl.java
@@ -1095,11 +1095,11 @@ public ListKeysResult listKeys(String volumeName, 
String bucketName,
     } else {
       // This allows us to seek directly to the first key with the right 
prefix.
       seekKey = getOzoneKey(volumeName, bucketName,
-          StringUtils.isNotBlank(keyPrefix) ? keyPrefix : OM_KEY_PREFIX);
+          StringUtils.isNotEmpty(keyPrefix) ? keyPrefix : OM_KEY_PREFIX);
     }
 
     String seekPrefix;
-    if (StringUtils.isNotBlank(keyPrefix)) {
+    if (StringUtils.isNotEmpty(keyPrefix)) {
       seekPrefix = getOzoneKey(volumeName, bucketName, keyPrefix);
     } else {
       seekPrefix = getBucketKey(volumeName, bucketName) + OM_KEY_PREFIX;
diff --git 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
index 17131e0c7db..ec241f9dcb3 100644
--- 
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
+++ 
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/TestOmMetadataManager.java
@@ -1319,4 +1319,37 @@ public void testListKeysSpecialKeyNames() throws 
Exception {
 
     assertEquals(keyNames, listedKeys);
   }
+
+  @Test
+  public void testListKeysWithWhitespaceAndNewlinePrefix() throws Exception {
+    String volumeName = "volumeA";
+    String bucketName = "bucketA";
+    OMRequestTestUtils.addVolumeToDB(volumeName, omMetadataManager);
+    addBucketsToCache(volumeName, bucketName);
+
+    String spaceOnly = " ";
+    String spacePrefixed = " x";
+    String doubleSpacePrefixed = "  y";
+    String newlinePrefixed = "\nbar";
+    String normalKey = "normal";
+
+    List<String> allKeys = Arrays.asList(
+        spaceOnly, spacePrefixed, doubleSpacePrefixed, newlinePrefixed, 
normalKey);
+    for (int i = 0; i < allKeys.size(); i++) {
+      addKeysToOM(volumeName, bucketName, allKeys.get(i), i);
+    }
+
+    List<String> spacePrefixMatches = omMetadataManager.listKeys(volumeName, 
bucketName,
+        null, spaceOnly, 100).getKeys().stream()
+        .map(OmKeyInfo::getKeyName)
+        .collect(Collectors.toList());
+    assertEquals(Arrays.asList(spaceOnly, doubleSpacePrefixed, spacePrefixed),
+        spacePrefixMatches);
+
+    List<String> newlinePrefixMatches = omMetadataManager.listKeys(volumeName, 
bucketName,
+        null, "\n", 100).getKeys().stream()
+        .map(OmKeyInfo::getKeyName)
+        .collect(Collectors.toList());
+    assertEquals(Collections.singletonList(newlinePrefixed), 
newlinePrefixMatches);
+  }
 }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/ObjectKeyNameAdapter.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/ObjectKeyNameAdapter.java
index 2f3ee0bdcf7..99b66381704 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/ObjectKeyNameAdapter.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/ObjectKeyNameAdapter.java
@@ -41,7 +41,8 @@ public EncodingTypeObject unmarshal(String s) {
   public String marshal(EncodingTypeObject s)
       throws UnsupportedEncodingException {
     if (s.getEncodingType() != null && s.getEncodingType().equals("url")) {
-      return StringUtils.replaceEach(S3Utils.urlEncode(s.getName()), 
URL_ENCODE_SEARCH_CHARS, URL_ENCODE_REPLACE_CHARS);
+      return StringUtils.replaceEach(S3Utils.s3urlEncode(s.getName()),
+          URL_ENCODE_SEARCH_CHARS, URL_ENCODE_REPLACE_CHARS);
     }
     return s.getName();
   }
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/RequestParameters.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/RequestParameters.java
index 85ff5fae535..127abe87121 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/RequestParameters.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/commontypes/RequestParameters.java
@@ -47,6 +47,12 @@ default int getInt(String key, int defaultValue) {
     }
   }
 
+  /**
+   * @return true if the query parameter is present, even when its value is
+   * an empty string (eg. {@code delimiter=}).
+   */
+  boolean containsKey(String key);
+
   /** Additional methods for tests. */
   interface Mutable extends RequestParameters {
 
@@ -72,6 +78,11 @@ public String get(String key) {
       return params.getFirst(key);
     }
 
+    @Override
+    public boolean containsKey(String key) {
+      return params.containsKey(key);
+    }
+
     @Override
     public void set(String key, String value) {
       params.putSingle(key, value);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index fbe48cbc72c..438ed93670c 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -106,7 +106,8 @@ public Response get(
   @Override
   Response handleGetRequest(S3RequestContext context, String bucketName) 
throws IOException, OS3Exception {
     final String continueToken = 
queryParams().get(QueryParams.CONTINUATION_TOKEN);
-    final String delimiter = queryParams().get(QueryParams.DELIMITER);
+    final String delimiter = queryParams().containsKey(QueryParams.DELIMITER) ?
+        queryParams().get(QueryParams.DELIMITER) : null;
     final String encodingType = queryParams().get(QueryParams.ENCODING_TYPE);
     final String marker = queryParams().get(QueryParams.MARKER);
     int maxKeys = queryParams().getInt(QueryParams.MAX_KEYS, 1000);
@@ -161,15 +162,16 @@ Response handleGetRequest(S3RequestContext context, 
String bucketName) throws IO
     if (encodingType != null && !encodingType.equals(ENCODING_TYPE)) {
       throw S3ErrorTable.newError(S3ErrorTable.INVALID_ARGUMENT, encodingType);
     }
-
     // If you specify the encoding-type request parameter,should return
-    // encoded key name values in the following response elements:
-    //   Delimiter, Prefix, Key, and StartAfter.
+    // encoded key name values in the following response elements: Delimiter, 
Prefix, Key, and StartAfter
     //
     // For detail refer:
     // 
https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#AmazonS3-ListObjectsV2-response-EncodingType
     ListObjectResponse response = new ListObjectResponse();
-    response.setDelimiter(EncodingTypeObject.createNullable(delimiter, 
encodingType));
+    // AWS omits Delimiter from the response when the client passes delimiter= 
or does not specify delimiter at all.
+    if (StringUtils.isNotEmpty(delimiter)) {
+      response.setDelimiter(EncodingTypeObject.createNullable(delimiter, 
encodingType));
+    }
     response.setName(bucketName);
     response.setPrefix(EncodingTypeObject.createNullable(prefix, 
encodingType));
     response.setMarker(marker == null ? "" : marker);
diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
index 29c556e4d31..4f9fe6c2a27 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/util/S3Utils.java
@@ -62,6 +62,17 @@ public static String urlEncode(String str)
     return URLEncoder.encode(str, UTF_8.name());
   }
 
+  /**
+   * Percent-encode a string for S3 {@code encoding-type=url} responses.
+   *
+   * <p>Unlike {@link URLEncoder} (application/x-www-form-urlencoded), AWS S3
+   * uses percent-encoding where spaces are {@code %20}, not {@code +}.
+   */
+  public static String s3urlEncode(String str)
+      throws UnsupportedEncodingException {
+    return urlEncode(str).replace("+", "%20");
+  }
+
   private S3Utils() {
     // no instances
   }
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/commontypes/TestObjectKeyNameAdapter.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/commontypes/TestObjectKeyNameAdapter.java
index f2b10ac4e1b..c98d0a27981 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/commontypes/TestObjectKeyNameAdapter.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/commontypes/TestObjectKeyNameAdapter.java
@@ -31,7 +31,7 @@ public class TestObjectKeyNameAdapter {
   public void testEncodeResult() throws Exception {
     assertEquals("abc/", getAdapter()
         .marshal(EncodingTypeObject.createNullable("abc/", ENCODING_TYPE)));
-    assertEquals("a+b+c/", getAdapter()
+    assertEquals("a%20b%20c/", getAdapter()
         .marshal(EncodingTypeObject.createNullable("a b c/", ENCODING_TYPE)));
     assertEquals("a%2Bb%2Bc/", getAdapter()
         .marshal(EncodingTypeObject.createNullable("a+b+c/", ENCODING_TYPE)));
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java
index 5d0ad2de0a8..f78c2cc620c 100644
--- 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestBucketList.java
@@ -36,6 +36,7 @@
 import org.apache.hadoop.ozone.client.OzoneClient;
 import org.apache.hadoop.ozone.client.OzoneClientStub;
 import org.apache.hadoop.ozone.s3.commontypes.EncodingTypeObject;
+import org.apache.hadoop.ozone.s3.commontypes.ObjectKeyNameAdapter;
 import org.apache.hadoop.ozone.s3.exception.OS3Exception;
 import org.apache.hadoop.ozone.s3.exception.S3ErrorTable;
 import org.apache.hadoop.ozone.s3.util.S3Consts.QueryParams;
@@ -216,6 +217,7 @@ public void listWithPrefixAndEmptyStrDelimiter()
 
     assertEquals(0, getBucketResponse.getCommonPrefixes().size());
     assertEquals(4, getBucketResponse.getContents().size());
+    assertNull(getBucketResponse.getDelimiter());
     assertEquals("dir1/",
         getBucketResponse.getContents().get(0).getKey().getName());
     assertEquals("dir1/dir2/",
@@ -403,7 +405,7 @@ public void testEncodingType() throws IOException, 
OS3Exception {
         <?xml version="1.0" encoding="UTF-8"?>
           <ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/";>
               ...
-              <Prefix>data%3D</Prefix>
+               <Prefix>data%3D</Prefix>
               <StartAfter>data%3D</StartAfter>
               <Delimiter>%3D</Delimiter>
               <EncodingType>url</EncodingType>
@@ -417,7 +419,7 @@ public void testEncodingType() throws IOException, 
OS3Exception {
               </CommonPrefixes>
           </ListBucketResult>
 
-      if encodingType == null , the = will not be encoded to "%3D"
+      if encodingType == null , the = will not be encoded to "%3D
     * */
 
     OzoneClient ozoneClient =
@@ -568,6 +570,38 @@ public void 
testListObjectsRespectsConfiguredMaxKeysLimit() throws Exception {
     assertEquals(Integer.parseInt(configuredMaxKeysLimit), 
response.getContents().size());
   }
 
+  @Test
+  public void testListObjectsUrlEncodingUsesPercentTwentyForSpaces()
+      throws Exception {
+    OzoneClient client = createClientWithKeys(
+        "foo+1/bar", "foo/bar/xyzzy", "quux ab/thud", "asdf+b");
+    BucketEndpoint endpoint = 
newBucketEndpointBuilder().setClient(client).build();
+
+    endpoint.queryParamsForTest().set(QueryParams.DELIMITER, "/");
+    endpoint.queryParamsForTest().set(QueryParams.ENCODING_TYPE, 
ENCODING_TYPE);
+    ListObjectResponse response = (ListObjectResponse) 
endpoint.get("b1").getEntity();
+
+    ObjectKeyNameAdapter adapter = new ObjectKeyNameAdapter();
+    assertEquals("asdf%2Bb", 
adapter.marshal(response.getContents().get(0).getKey()));
+    assertEquals(3, response.getCommonPrefixes().size());
+    assertEquals("foo%2B1/", 
adapter.marshal(response.getCommonPrefixes().get(0).getPrefix()));
+    assertEquals("foo/", 
adapter.marshal(response.getCommonPrefixes().get(1).getPrefix()));
+    assertEquals("quux%20ab/", 
adapter.marshal(response.getCommonPrefixes().get(2).getPrefix()));
+  }
+
+  @Test
+  public void testListObjectsOmitsDelimiterWhenEmpty() throws Exception {
+    OzoneClient client = createClientWithKeys("bar", "baz", "cab", "foo");
+    BucketEndpoint endpoint = 
newBucketEndpointBuilder().setClient(client).build();
+
+    endpoint.queryParamsForTest().set(QueryParams.DELIMITER, "");
+    ListObjectResponse response = (ListObjectResponse) 
endpoint.get("b1").getEntity();
+
+    assertNull(response.getDelimiter());
+    assertEquals(4, response.getContents().size());
+    assertEquals(0, response.getCommonPrefixes().size());
+  }
+
   private void assertEncodingTypeObject(
       String exceptName, String exceptEncodingType, EncodingTypeObject object) 
{
     assertEquals(exceptName, object.getName());


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to