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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 4adce1d41b [common] Support unescaped characters in blob file paths 
(#8931)
4adce1d41b is described below

commit 4adce1d41bc56d795077f446baa9e0f1ca41f8b0
Author: LsomeYeah <[email protected]>
AuthorDate: Thu Jul 30 23:04:49 2026 +0800

    [common] Support unescaped characters in blob file paths (#8931)
---
 .../org/apache/paimon/utils/UriReaderFactory.java  | 12 ++++-
 .../apache/paimon/utils/UriReaderFactoryTest.java  | 21 +++++++++
 .../org/apache/paimon/flink/BlobTableITCase.java   | 53 ++++++++++++++++++++++
 3 files changed, 85 insertions(+), 1 deletion(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/utils/UriReaderFactory.java 
b/paimon-common/src/main/java/org/apache/paimon/utils/UriReaderFactory.java
index 215a96295d..4d1f84cfcb 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/UriReaderFactory.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/UriReaderFactory.java
@@ -60,6 +60,15 @@ public class UriReaderFactory implements Serializable {
         try {
             return URI.create(input);
         } catch (IllegalArgumentException e) {
+            // File paths may contain unescaped characters accepted by Path.
+            try {
+                URI pathUri = new Path(input).toUri();
+                if (!isHttp(pathUri)) {
+                    return pathUri;
+                }
+            } catch (IllegalArgumentException ignored) {
+                // Throw the sanitized exception below.
+            }
             throw SensitiveConfigUtils.invalidUri(input);
         }
     }
@@ -94,7 +103,8 @@ public class UriReaderFactory implements Serializable {
     }
 
     private static boolean isHttp(URI uri) {
-        return "http".equals(uri.getScheme()) || 
"https".equals(uri.getScheme());
+        return "http".equalsIgnoreCase(uri.getScheme())
+                || "https".equalsIgnoreCase(uri.getScheme());
     }
 
     private static final class ProvidedFileIOUriReaderFactory extends 
UriReaderFactory {
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/utils/UriReaderFactoryTest.java 
b/paimon-common/src/test/java/org/apache/paimon/utils/UriReaderFactoryTest.java
index 5e1fb018ac..16ea5676fb 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/utils/UriReaderFactoryTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/utils/UriReaderFactoryTest.java
@@ -85,6 +85,12 @@ public class UriReaderFactoryTest {
                         });
     }
 
+    @Test
+    public void testInvalidUpperCaseHttpUriDoesNotFallBackToFileReader() {
+        assertThatThrownBy(() -> factory.create("HTTPS://example.com/bad 
path"))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
     @Test
     public void testCreateHttpsUriReader() {
         UriReader reader = factory.create("https://example.com/file.txt";);
@@ -97,6 +103,21 @@ public class UriReaderFactoryTest {
         assertThat(reader).isInstanceOf(FileUriReader.class);
     }
 
+    @Test
+    public void testReadFileUriWithUnescapedCharacters() throws Exception {
+        java.nio.file.Path file = tempPath.resolve("\u4ed5\u5e9c\u516c\u9986 
(2).jpg");
+        Files.write(file, new byte[] {1, 2});
+        String fileUri = "file://" + file.toAbsolutePath();
+
+        UriReader reader = factory.create(fileUri);
+
+        assertThat(reader).isInstanceOf(FileUriReader.class);
+        try (SeekableInputStream inputStream = reader.newInputStream(fileUri)) 
{
+            assertThat(inputStream.read()).isEqualTo(1);
+            assertThat(inputStream.read()).isEqualTo(2);
+        }
+    }
+
     @Test
     public void testProvidedFileIOSurvivesSerialization() throws Exception {
         java.nio.file.Path file = tempPath.resolve("file.txt");
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BlobTableITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BlobTableITCase.java
index d9e894646e..37dcf37bd3 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BlobTableITCase.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/BlobTableITCase.java
@@ -23,11 +23,15 @@ import org.apache.paimon.data.Blob;
 import org.apache.paimon.data.BlobDescriptor;
 import org.apache.paimon.data.BlobRef;
 import org.apache.paimon.data.BlobViewStruct;
+import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.rest.TestHttpWebServer;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
 import org.apache.paimon.types.DataTypeRoot;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.UriReader;
@@ -690,6 +694,55 @@ public class BlobTableITCase extends CatalogITCaseBase {
                 .hasStackTraceContaining("does not support creating blob 
presigned URLs");
     }
 
+    @Test
+    public void testWriteBlobWithUnicodeAndSpaceInPath() throws Exception {
+        byte[] blobData = "image-content".getBytes();
+        FileIO fileIO = new LocalFileIO();
+        String uri = "file://" + warehouse + "/\u4ed5\u5e9c\u516c\u9986 
(2).jpg";
+        try (OutputStream outputStream =
+                fileIO.newOutputStream(new org.apache.paimon.fs.Path(uri), 
true)) {
+            outputStream.write(blobData);
+        }
+
+        batchSql(
+                "INSERT INTO blob_table_descriptor VALUES"
+                        + " (1, 'paimon', sys.path_to_descriptor('"
+                        + uri
+                        + "'))");
+        batchSql("ALTER TABLE blob_table_descriptor SET 
('blob-as-descriptor'='false')");
+
+        assertThat(batchSql("SELECT picture FROM blob_table_descriptor"))
+                .containsExactly(Row.of(blobData));
+    }
+
+    @Test
+    public void testReadDescriptorBlobWithUnicodeAndSpaceInPath() throws 
Exception {
+        byte[] blobData = "image-content".getBytes();
+        FileIO fileIO = new LocalFileIO();
+        String uri = "file://" + warehouse + "/\u4ed5\u5e9c\u516c\u9986 
(2).jpg";
+        try (OutputStream outputStream =
+                fileIO.newOutputStream(new org.apache.paimon.fs.Path(uri), 
true)) {
+            outputStream.write(blobData);
+        }
+
+        tEnv.executeSql(
+                "CREATE TABLE external_blob_source (id INT, picture BYTES)"
+                        + " WITH ('row-tracking.enabled'='true',"
+                        + " 'data-evolution.enabled'='true',"
+                        + " 'blob-descriptor-field'='picture')");
+        FileStoreTable table = paimonTable("external_blob_source");
+        BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+        try (BatchTableWrite write = writeBuilder.newWrite();
+                BatchTableCommit commit = writeBuilder.newCommit()) {
+            BlobDescriptor descriptor = new BlobDescriptor(uri, 0, 
blobData.length);
+            write.write(GenericRow.of(1, new 
BlobRef(UriReader.fromFile(fileIO), descriptor)));
+            commit.commit(write.prepareCommit());
+        }
+
+        assertThat(batchSql("SELECT picture FROM external_blob_source"))
+                .containsExactly(Row.of(blobData));
+    }
+
     @Test
     public void testWriteBlobViewWithBuiltInFunction() throws Exception {
         tEnv.executeSql(

Reply via email to