Skip to content

Commit 3e1d0cd

Browse files
committed
refactor(fakestorage): use doublestar for MatchGlob matching
Replace the custom globToRegex translator with doublestar.MatchUnvalidated, per review feedback on #1985. - doublestar treats a mid-segment ** (img/**.jpg) as a single *, so rewrite the two test patterns that relied on it to the segment form (img/**/*.jpg); the expected results do not change - fix a test name that embedded \a as a bell character
1 parent 5c29e24 commit 3e1d0cd

4 files changed

Lines changed: 8 additions & 122 deletions

File tree

fakestorage/object.go

Lines changed: 2 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import (
1414
"fmt"
1515
"io"
1616
"net/http"
17-
"regexp"
1817
"slices"
1918
"sort"
2019
"strconv"
2120
"strings"
2221
"time"
2322

2423
"cloud.google.com/go/storage"
24+
"github.com/bmatcuk/doublestar/v4"
2525
"github.com/fsouza/fake-gcs-server/internal/backend"
2626
"github.com/fsouza/fake-gcs-server/internal/notification"
2727
"github.com/fsouza/fake-gcs-server/internal/urlhelper"
@@ -405,7 +405,7 @@ func (s *Server) ListObjectsWithOptionsPaginated(bucketName string, options List
405405
continue
406406
}
407407

408-
if options.MatchGlob != "" && !matchGlob(options.MatchGlob, obj.Name) {
408+
if options.MatchGlob != "" && !doublestar.MatchUnvalidated(options.MatchGlob, obj.Name) {
409409
continue
410410
}
411411

@@ -1462,120 +1462,3 @@ func (s *Server) composeObject(r *http.Request) jsonResponse {
14621462

14631463
return jsonResponse{data: newObjectResponse(obj.ObjectAttrs, urlhelper.GetBaseURL(r))}
14641464
}
1465-
1466-
// matchGlob matches a glob pattern against a string following GCS glob syntax.
1467-
// See https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob
1468-
// Supports:
1469-
// - * matches zero or more characters (excluding /)
1470-
// - ** matches zero or more characters (including /)
1471-
// - ? matches any single character (excluding /)
1472-
// - [abc] matches exactly one character in the set
1473-
// - [a-z] matches one character in a range
1474-
// - [!abc] or [^abc] matches one character NOT in the set
1475-
// - {abc,xyz} matches one of the specified options
1476-
// Doesn't currently support:
1477-
// - nested {} braces
1478-
// - patterns inside {} braces e.g. {*foo,bar*}
1479-
func matchGlob(pattern, name string) bool {
1480-
regex := globToRegex(pattern)
1481-
matched, err := regexp.MatchString(regex, name)
1482-
if err != nil {
1483-
return false
1484-
}
1485-
return matched
1486-
}
1487-
1488-
func globToRegex(pattern string) string {
1489-
var result strings.Builder
1490-
result.WriteString("^")
1491-
1492-
i := 0
1493-
for i < len(pattern) {
1494-
switch pattern[i] {
1495-
case '*':
1496-
if i+1 < len(pattern) && pattern[i+1] == '*' {
1497-
// ** matches zero or more characters including /
1498-
result.WriteString(".*")
1499-
i += 2
1500-
} else {
1501-
// * matches zero or more characters excluding /
1502-
result.WriteString("[^/]*")
1503-
i++
1504-
}
1505-
case '?':
1506-
// ? matches any single character excluding /
1507-
result.WriteString("[^/]")
1508-
i++
1509-
case '[':
1510-
// Character class [abc] or [a-z] or [!abc] or [^abc]
1511-
j := i + 1
1512-
if j < len(pattern) && (pattern[j] == '!' || pattern[j] == '^') {
1513-
j++
1514-
}
1515-
for j < len(pattern) && pattern[j] != ']' {
1516-
j++
1517-
}
1518-
if j < len(pattern) {
1519-
// Valid character class
1520-
charClass := pattern[i : j+1]
1521-
// Convert [!...] to [^...] for regex
1522-
if strings.HasPrefix(charClass, "[!") {
1523-
charClass = "[^" + charClass[2:]
1524-
}
1525-
result.WriteString(charClass)
1526-
i = j + 1
1527-
} else {
1528-
// Invalid character class, treat as literal
1529-
result.WriteString(regexp.QuoteMeta(string(pattern[i])))
1530-
i++
1531-
}
1532-
case '{':
1533-
// Brace expansion {abc,xyz}
1534-
j := i + 1
1535-
depth := 1
1536-
for j < len(pattern) && depth > 0 {
1537-
switch pattern[j] {
1538-
case '{':
1539-
depth++
1540-
case '}':
1541-
depth--
1542-
}
1543-
j++
1544-
}
1545-
if depth == 0 {
1546-
// Valid brace expansion
1547-
braceContent := pattern[i+1 : j-1]
1548-
options := strings.Split(braceContent, ",")
1549-
result.WriteString("(")
1550-
for k, option := range options {
1551-
if k > 0 {
1552-
result.WriteString("|")
1553-
}
1554-
result.WriteString(regexp.QuoteMeta(option))
1555-
}
1556-
result.WriteString(")")
1557-
i = j
1558-
} else {
1559-
// Invalid brace expansion, treat as literal
1560-
result.WriteString(regexp.QuoteMeta(string(pattern[i])))
1561-
i++
1562-
}
1563-
case '\\':
1564-
// Escape character
1565-
if i+1 < len(pattern) {
1566-
result.WriteString(regexp.QuoteMeta(string(pattern[i+1])))
1567-
i += 2
1568-
} else {
1569-
result.WriteString(regexp.QuoteMeta(string(pattern[i])))
1570-
i++
1571-
}
1572-
default:
1573-
// Regular character, escape for regex
1574-
result.WriteString(regexp.QuoteMeta(string(pattern[i])))
1575-
i++
1576-
}
1577-
}
1578-
1579-
result.WriteString("$")
1580-
return result.String()
1581-
}

fakestorage/object_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ func getTestCasesForListTests(versioningEnabled, withOverwrites bool) []listTest
10061006
{
10071007
fmt.Sprintf("filtering MatchGlob **, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
10081008
"some-bucket",
1009-
&storage.Query{MatchGlob: "img/**.jpg"},
1009+
&storage.Query{MatchGlob: "img/**/*.jpg"},
10101010
[]string{
10111011
"img/brand.jpg",
10121012
"img/hi-res/party-01.jpg",
@@ -1069,7 +1069,7 @@ func getTestCasesForListTests(versioningEnabled, withOverwrites bool) []listTest
10691069
{
10701070
fmt.Sprintf("filtering MatchGlob ** and {abc,xyz}, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
10711071
"some-bucket",
1072-
&storage.Query{MatchGlob: "img/**{brand,party-01,party-02}.jpg"},
1072+
&storage.Query{MatchGlob: "img/**/{brand,party-01,party-02}.jpg"},
10731073
[]string{
10741074
"img/brand.jpg",
10751075
"img/hi-res/party-01.jpg",
@@ -1092,7 +1092,7 @@ func getTestCasesForListTests(versioningEnabled, withOverwrites bool) []listTest
10921092
nil,
10931093
},
10941094
{
1095-
fmt.Sprintf("filtering MatchGlob ** and \a, versioning %t and overwrites %t", versioningEnabled, withOverwrites),
1095+
fmt.Sprintf(`filtering MatchGlob ** and \a, versioning %t and overwrites %t`, versioningEnabled, withOverwrites),
10961096
"some-bucket",
10971097
&storage.Query{MatchGlob: "img/**/party\\-*.jpg"},
10981098
[]string{

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module github.com/fsouza/fake-gcs-server
22

33
require (
44
cloud.google.com/go/storage v1.64.0
5+
github.com/bmatcuk/doublestar/v4 v4.10.0
56
github.com/fsouza/slognil v0.4.3
67
github.com/google/go-cmp v0.7.0
78
github.com/gorilla/handlers v1.5.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
3232
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
3333
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
3434
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
35+
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
36+
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
3537
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
3638
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
3739
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

0 commit comments

Comments
 (0)