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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4391d8d7f38 Fix GCS glob matching for multi-byte characters (#39972)
4391d8d7f38 is described below

commit 4391d8d7f3811d7cec0b6a2c9b9f3c465265a396
Author: Shizuma5 <[email protected]>
AuthorDate: Thu Sep 3 05:17:56 2026 +0900

    Fix GCS glob matching for multi-byte characters (#39972)
---
 CHANGES.md                                     |  1 +
 sdks/go/pkg/beam/io/filesystem/gcs/gcs.go      | 22 ++++++++++++----------
 sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go |  8 ++++++++
 3 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index fc34c155256..365605c7065 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -95,6 +95,7 @@
 * (Java) KafkaIO dynamic reads no longer require the obsolete `beam_fn_api` 
experiment ([#29998](https://github.com/apache/beam/issues/29998)).
 * (Prism) Self-checkpointing splittable DoFns now resume after their requested 
delay instead of immediately, so polling SDFs no longer busy-spin 
([#39848](https://github.com/apache/beam/issues/39848)).
 * (Java) MongoDbIO read splitting now preserves non-ObjectId `_id` types (e.g. 
string ids) instead of failing to parse the generated range filters 
([#39900](https://github.com/apache/beam/issues/39900)).
+* (Go) Fixed GCS glob matching silently dropping objects when the glob pattern 
contains multi-byte characters 
([#39969](https://github.com/apache/beam/issues/39969)).
 
 ## Security Fixes
 
diff --git a/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go 
b/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go
index 8d6ce28559f..6a990f9cbe7 100644
--- a/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go
+++ b/sdks/go/pkg/beam/io/filesystem/gcs/gcs.go
@@ -53,14 +53,16 @@ func globToRegex(pattern string) (*regexp.Regexp, error) {
        var result strings.Builder
        result.WriteString("^")
 
-       for i := 0; i < len(pattern); i++ {
-               c := pattern[i]
+       // Scan rune by rune so multi-byte characters are handled correctly.
+       runes := []rune(pattern)
+       for i := 0; i < len(runes); i++ {
+               c := runes[i]
                switch c {
                case '*':
                        // Check for ** (double asterisk)
-                       if i+1 < len(pattern) && pattern[i+1] == '*' {
+                       if i+1 < len(runes) && runes[i+1] == '*' {
                                // Check if followed by / (e.g., "**/" matches 
zero or more path segments)
-                               if i+2 < len(pattern) && pattern[i+2] == '/' {
+                               if i+2 < len(runes) && runes[i+2] == '/' {
                                        // **/ matches "" or "something/" or 
"a/b/c/"
                                        result.WriteString("(?:.*/)?")
                                        i += 2 // Skip the second * and the /
@@ -77,26 +79,26 @@ func globToRegex(pattern string) (*regexp.Regexp, error) {
                case '[':
                        // Character class - find the closing bracket
                        j := i + 1
-                       if j < len(pattern) && pattern[j] == '!' {
+                       if j < len(runes) && runes[j] == '!' {
                                j++
                        }
-                       if j < len(pattern) && pattern[j] == ']' {
+                       if j < len(runes) && runes[j] == ']' {
                                j++
                        }
-                       for j < len(pattern) && pattern[j] != ']' {
+                       for j < len(runes) && runes[j] != ']' {
                                j++
                        }
-                       if j >= len(pattern) {
+                       if j >= len(runes) {
                                return nil, fmt.Errorf("syntax error: unclosed 
'[' in pattern %q", pattern)
                        } else {
                                // Copy the character class, converting ! to ^ 
for negation
                                result.WriteByte('[')
-                               content := pattern[i+1 : j]
+                               content := runes[i+1 : j]
                                if len(content) > 0 && content[0] == '!' {
                                        result.WriteByte('^')
                                        content = content[1:]
                                }
-                               result.WriteString(content)
+                               result.WriteString(string(content))
                                result.WriteByte(']')
                                i = j
                        }
diff --git a/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go 
b/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go
index cd6aab2a236..351b0b58c13 100644
--- a/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go
+++ b/sdks/go/pkg/beam/io/filesystem/gcs/gcs_test.go
@@ -322,6 +322,14 @@ func TestGlobToRegex(t *testing.T) {
                {"file.txt", "file.txt", true},
                {"file.txt", "fileXtxt", false},
                {"file(1).txt", "file(1).txt", true},
+
+               // Multi-byte characters must be preserved
+               {"résumé.txt", "résumé.txt", true},
+               {"résumé.txt", "resume.txt", false},
+               {"résumé/*.txt", "résumé/file.txt", true},
+               {"*/résumé.txt", "dir/résumé.txt", true},
+               {"*.txt", "résumé.txt", true},
+               {"ïé.txt", "ïé.txt", true},
        }
 
        for _, tt := range tests {

Reply via email to