snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
package wildcard
import (
"fmt"
"regexp"
"strings"
)
// CollectionFilterMode controls how collections are interpreted during
// detection. The two recognized sentinels short-circuit matching:
// - CollectionFilterAll: pool every collection together (default).
// - CollectionFilterEach: run detection separately per collection.
//
// Any other non-empty value is a comma-separated list of collection patterns.
type CollectionFilterMode string
const (
CollectionFilterAll CollectionFilterMode = "ALL_COLLECTIONS"
CollectionFilterEach CollectionFilterMode = "EACH_COLLECTION"
// CollectionDefault is the entry that matches the empty-named collection.
// "_default" avoids colliding with a collection literally named "default".
CollectionDefault = "_default"
)
// regexMetaCharacters mark a filter entry as a regex; "*" and "?" stay wildcards.
// "." is not one of them: a collection named "my.bucket" is a name, not a
// pattern that would also match "my-bucket". A "." only counts as regex syntax
// when it is quantified, as in "bucket.*".
const regexMetaCharacters = `^$+()[]{}|\`
// CollectionMatcher matches a volume collection against a collection_filter value.
type CollectionMatcher struct {
filter string
matchEmpty bool
literals []string
wildcards []string
regexes []*regexp.Regexp
}
// CompileCollectionMatcher parses a collection_filter into a matcher. A nil
// matcher accepts every collection: that is the empty filter, "*", and both
// mode sentinels. Any other value is a comma-separated list, and a collection
// passes when one entry matches it. An entry is a name, optionally with "*" and
// "?" wildcards, CollectionDefault for the empty-named collection, or a regex
// when it carries regex syntax. A regex entry must match the whole name unless
// it anchors itself with "^" or "$", and always matches its own spelling too,
// so a collection named "logs(2024)" stays reachable by name.
func CompileCollectionMatcher(filter string) (*CollectionMatcher, error) {
trimmed := strings.TrimSpace(filter)
mode := CollectionFilterMode(trimmed)
if trimmed == "" || trimmed == "*" || mode == CollectionFilterAll || mode == CollectionFilterEach {
return nil, nil
}
matcher := &CollectionMatcher{filter: trimmed}
for _, entry := range splitCollectionEntries(trimmed) {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
if entry == CollectionDefault {
matcher.matchEmpty = true
continue
}
if !isRegexEntry(entry) {
matcher.wildcards = append(matcher.wildcards, entry)
continue
}
pattern := entry
if !strings.ContainsAny(entry, "^$") {
pattern = "^(?:" + entry + ")$"
}
compiled, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("invalid collection_filter entry %q: %w", entry, err)
}
matcher.literals = append(matcher.literals, entry)
matcher.regexes = append(matcher.regexes, compiled)
}
if !matcher.matchEmpty && len(matcher.wildcards) == 0 && len(matcher.regexes) == 0 {
// Only a genuinely empty filter means "every collection". A value like ","
// is a typo, and matching everything on it would encode or delete far more
// than the operator asked for.
return nil, fmt.Errorf("collection_filter %q has no collection names", trimmed)
}
return matcher, nil
}
// splitCollectionEntries splits a filter on the commas that separate entries.
// A comma inside a closed regex construct - a character class, a repetition
// count, or a group - belongs to that construct. An unclosed one is a literal
// brace or bracket in a name, so the commas after it still separate entries.
func splitCollectionEntries(filter string) []string {
entries := make([]string, 0, 4)
start := 0
for i := 0; i < len(filter); i++ {
switch filter[i] {
case '\\':
i++
case '[':
if end := classEnd(filter, i); end > i {
i = end
}
case '{':
if end := repeatCountEnd(filter, i); end > i {
i = end
}
case '(':
if end := groupEnd(filter, i); end > i {
i = end
}
case ',':
entries = append(entries, filter[start:i])
start = i + 1
}
}
return append(entries, filter[start:])
}
// classEnd returns the index of the "]" closing the character class opened at
// start, or -1 when it is never closed. It follows the regexp parser: a "]"
// leading the class is a member of it, and a POSIX class such as "[:alpha:]"
// carries a "]" of its own.
func classEnd(filter string, start int) int {
i := start + 1
if i < len(filter) && filter[i] == '^' {
i++
}
if i < len(filter) && filter[i] == ']' {
i++
}
for ; i < len(filter); i++ {
switch filter[i] {
case '\\':
i++
case '[':
if end := posixClassEnd(filter, i); end > i {
i = end
}
case ']':
return i
}
}
return -1
}
// posixClassEnd returns the index of the "]" closing the POSIX class opened at
// start, or -1 when start does not open one.
func posixClassEnd(filter string, start int) int {
if !strings.HasPrefix(filter[start:], "[:") {
return -1
}
end := strings.Index(filter[start+2:], ":]")
if end < 0 {
return -1
}
return start + 2 + end + 1
}
// repeatCountEnd returns the index of the "}" closing the repetition count
// opened at start, or -1 when what follows is not a count.
func repeatCountEnd(filter string, start int) int {
for i := start + 1; i < len(filter); i++ {
if filter[i] == '}' {
return i
}
if filter[i] != ',' && (filter[i] < '0' || filter[i] > '9') {
return -1
}
}
return -1
}
// groupEnd returns the index of the ")" closing the group opened at start, or
// -1 when it is never closed.
func groupEnd(filter string, start int) int {
depth := 0
for i := start; i < len(filter); i++ {
switch filter[i] {
case '\\':
i++
case '[':
// a ")" inside a character class is a literal, not the group's end
if end := classEnd(filter, i); end > i {
i = end
}
case '(':
depth++
case ')':
if depth--; depth == 0 {
return i
}
}
}
return -1
}
// isRegexEntry reports whether an entry carries regex syntax rather than being a
// plain collection name with optional "*" and "?" wildcards.
func isRegexEntry(entry string) bool {
if strings.ContainsAny(entry, regexMetaCharacters) {
return true
}
for i := 0; i+1 < len(entry); i++ {
if entry[i] == '.' && (entry[i+1] == '*' || entry[i+1] == '?') {
return true
}
}
return false
}
// Matches reports whether a collection passes the filter. A nil matcher accepts everything.
func (m *CollectionMatcher) Matches(collection string) bool {
if m == nil {
return true
}
if m.matchEmpty && collection == "" {
return true
}
for _, literal := range m.literals {
if literal == collection {
return true
}
}
for _, pattern := range m.wildcards {
if MatchesWildcard(pattern, collection) {
return true
}
}
for _, re := range m.regexes {
if re.MatchString(collection) {
return true
}
}
return false
}
// String returns the filter this matcher was compiled from.
func (m *CollectionMatcher) String() string {
if m == nil {
return ""
}
return m.filter
}

View File

@@ -0,0 +1,74 @@
package wildcard
import "testing"
func TestCompileCollectionMatcher(t *testing.T) {
cases := []struct {
filter string
matches []string
misses []string
}{
{filter: "", matches: []string{"", "photos"}},
{filter: "*", matches: []string{"", "photos"}},
{filter: string(CollectionFilterAll), matches: []string{"", "photos"}},
{filter: string(CollectionFilterEach), matches: []string{"", "photos"}},
{filter: "photos", matches: []string{"photos"}, misses: []string{"", "photos-backup", "myphotos"}},
{filter: "collection-a,collection-b", matches: []string{"collection-a", "collection-b"}, misses: []string{"collection-c"}},
{filter: " collection-a , collection-b ,", matches: []string{"collection-a", "collection-b"}, misses: []string{"collection-c"}},
{filter: "photos*,videos", matches: []string{"photos", "photos-backup", "videos"}, misses: []string{"clips"}},
{filter: "photo?", matches: []string{"photos"}, misses: []string{"photo", "photos-backup"}},
// A regex entry matches the whole name unless it anchors itself.
{filter: "photos|videos", matches: []string{"photos", "videos"}, misses: []string{"photos-backup"}},
{filter: "^photos", matches: []string{"photos", "photos-backup"}, misses: []string{"videos"}},
{filter: "photos-.*,videos", matches: []string{"photos-backup", "videos"}, misses: []string{"photos"}},
// A dot is part of a collection name unless it is quantified.
{filter: "my.bucket", matches: []string{"my.bucket"}, misses: []string{"my-bucket", "myxbucket"}},
{filter: "my.bucket,videos", matches: []string{"my.bucket", "videos"}, misses: []string{"my-bucket"}},
// A name made of regex syntax is still reachable by its own spelling.
{filter: "logs(2024)", matches: []string{"logs(2024)", "logs2024"}, misses: []string{"logs"}},
// Escaping reaches a name whose regex syntax does not parse on its own.
{filter: `logs\(2024`, matches: []string{"logs(2024"}, misses: []string{"logs2024", "logs"}},
// A comma inside a character class or a repetition count is not a separator.
{filter: "bucket[0-9]{1,3}", matches: []string{"bucket1", "bucket123"}, misses: []string{"bucket", "bucketx", "bucket1234"}},
{filter: "[a,b]", matches: []string{"a", "b", ","}, misses: []string{"ab", "c"}},
{filter: "bucket[0-9]{1,3},videos", matches: []string{"bucket7", "videos"}, misses: []string{"bucketx"}},
{filter: "bucket(foo,bar)", matches: []string{"bucketfoo,bar", "bucket(foo,bar)"}, misses: []string{"bucketfoo"}},
{filter: "logs(2024),videos", matches: []string{"logs(2024)", "logs2024", "videos"}, misses: []string{"logs"}},
{filter: "(a[)],b)", matches: []string{"a),b"}, misses: []string{"a", "b"}},
{filter: "(a[]),],b)", matches: []string{"a],b", "a),b"}, misses: []string{"a,b"}},
{filter: "x[[:alpha:],b]y", matches: []string{"xay", "xby", "x,y"}, misses: []string{"xy"}},
// A comma separates entries unless it is escaped, so a name holding one is
// reachable as "\(a\,b\)".
{filter: `\(a\,b\)`, matches: []string{"(a,b)"}, misses: []string{"(a", "b)"}},
// An unclosed brace is a literal in a name, so the comma after it still separates.
{filter: "foo{bar,videos", matches: []string{"foo{bar", "videos"}, misses: []string{"foo"}},
{filter: "foo{2,videos", matches: []string{"foo{2", "videos"}, misses: []string{"foo"}},
{filter: CollectionDefault, matches: []string{""}, misses: []string{"photos"}},
{filter: "photos," + CollectionDefault, matches: []string{"photos", ""}, misses: []string{"videos"}},
}
for _, c := range cases {
matcher, err := CompileCollectionMatcher(c.filter)
if err != nil {
t.Fatalf("CompileCollectionMatcher(%q): %v", c.filter, err)
}
for _, collection := range c.matches {
if !matcher.Matches(collection) {
t.Errorf("filter %q should match collection %q", c.filter, collection)
}
}
for _, collection := range c.misses {
if matcher.Matches(collection) {
t.Errorf("filter %q should not match collection %q", c.filter, collection)
}
}
}
}
func TestCompileCollectionMatcherInvalidEntry(t *testing.T) {
for _, filter := range []string{"photos,[invalid", ",", ", ,"} {
if _, err := CompileCollectionMatcher(filter); err == nil {
t.Errorf("filter %q should not compile", filter)
}
}
}

View File

@@ -0,0 +1,39 @@
package wildcard
import "strings"
// CompileWildcardMatchers parses comma-separated wildcard patterns and compiles them.
// Empty tokens are ignored. Invalid patterns are skipped.
func CompileWildcardMatchers(filter string) []*WildcardMatcher {
parts := strings.Split(filter, ",")
matchers := make([]*WildcardMatcher, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
continue
}
matcher, err := NewWildcardMatcher(trimmed)
if err != nil {
continue
}
matchers = append(matchers, matcher)
}
if len(matchers) == 0 {
return nil
}
return matchers
}
// MatchesAnyWildcard returns true when no matcher is provided,
// or when any matcher matches the given value.
func MatchesAnyWildcard(matchers []*WildcardMatcher, value string) bool {
if len(matchers) == 0 {
return true
}
for _, matcher := range matchers {
if matcher != nil && matcher.Match(value) {
return true
}
}
return false
}

View File

@@ -0,0 +1,186 @@
package wildcard
import (
"regexp"
"strings"
"sync"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// WildcardMatcher provides unified wildcard matching functionality.
type WildcardMatcher struct {
useRegex bool
regex *regexp.Regexp
pattern string
}
// WildcardMatcherCache provides caching for WildcardMatcher instances.
type WildcardMatcherCache struct {
mu sync.RWMutex
matchers map[string]*WildcardMatcher
maxSize int
accessOrder []string
}
// NewWildcardMatcherCache creates a new WildcardMatcherCache with a configurable maxSize.
func NewWildcardMatcherCache(maxSize int) *WildcardMatcherCache {
if maxSize <= 0 {
maxSize = 1000
}
return &WildcardMatcherCache{
matchers: make(map[string]*WildcardMatcher),
maxSize: maxSize,
}
}
var wildcardMatcherCache = NewWildcardMatcherCache(1000)
// GetCachedWildcardMatcher gets or creates a cached WildcardMatcher for the given pattern.
func GetCachedWildcardMatcher(pattern string) (*WildcardMatcher, error) {
wildcardMatcherCache.mu.RLock()
if matcher, exists := wildcardMatcherCache.matchers[pattern]; exists {
wildcardMatcherCache.mu.RUnlock()
wildcardMatcherCache.updateAccessOrder(pattern)
return matcher, nil
}
wildcardMatcherCache.mu.RUnlock()
wildcardMatcherCache.mu.Lock()
defer wildcardMatcherCache.mu.Unlock()
if matcher, exists := wildcardMatcherCache.matchers[pattern]; exists {
wildcardMatcherCache.updateAccessOrderLocked(pattern)
return matcher, nil
}
matcher, err := NewWildcardMatcher(pattern)
if err != nil {
return nil, err
}
if len(wildcardMatcherCache.matchers) >= wildcardMatcherCache.maxSize {
wildcardMatcherCache.evictLeastRecentlyUsed()
}
wildcardMatcherCache.matchers[pattern] = matcher
wildcardMatcherCache.accessOrder = append(wildcardMatcherCache.accessOrder, pattern)
return matcher, nil
}
func (c *WildcardMatcherCache) updateAccessOrder(pattern string) {
c.mu.Lock()
defer c.mu.Unlock()
c.updateAccessOrderLocked(pattern)
}
func (c *WildcardMatcherCache) updateAccessOrderLocked(pattern string) {
for i, p := range c.accessOrder {
if p == pattern {
c.accessOrder = append(c.accessOrder[:i], c.accessOrder[i+1:]...)
break
}
}
c.accessOrder = append(c.accessOrder, pattern)
}
func (c *WildcardMatcherCache) evictLeastRecentlyUsed() {
if len(c.accessOrder) == 0 {
return
}
lruPattern := c.accessOrder[0]
c.accessOrder = c.accessOrder[1:]
delete(c.matchers, lruPattern)
}
func (c *WildcardMatcherCache) ClearCache() {
c.mu.Lock()
defer c.mu.Unlock()
c.matchers = make(map[string]*WildcardMatcher)
c.accessOrder = c.accessOrder[:0]
}
func (c *WildcardMatcherCache) GetCacheStats() (size int, maxSize int) {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.matchers), c.maxSize
}
// NewWildcardMatcher creates a new wildcard matcher for the given pattern.
func NewWildcardMatcher(pattern string) (*WildcardMatcher, error) {
matcher := &WildcardMatcher{pattern: pattern, useRegex: false}
return matcher, nil
}
// Match checks if a string matches the wildcard pattern.
func (m *WildcardMatcher) Match(str string) bool {
if m.useRegex {
return m.regex.MatchString(str)
}
return matchWildcardString(m.pattern, str)
}
// MatchesWildcard provides a simple function interface for wildcard matching.
func MatchesWildcard(pattern, str string) bool {
return matchWildcardString(pattern, str)
}
// FastMatchesWildcard uses a cached WildcardMatcher for repeated pattern matching.
func FastMatchesWildcard(pattern, str string) bool {
matcher, err := GetCachedWildcardMatcher(pattern)
if err != nil {
glog.Errorf("Error getting cached WildcardMatcher for pattern %s: %v. Falling back to MatchesWildcard.", pattern, err)
return MatchesWildcard(pattern, str)
}
return matcher.Match(str)
}
// CompileWildcardPattern converts a wildcard pattern to a compiled regex.
func CompileWildcardPattern(pattern string) (*regexp.Regexp, error) {
return compileWildcardPattern(pattern)
}
func matchWildcardString(pattern, str string) bool {
if pattern == "*" {
return true
}
if pattern == str {
return true
}
targetIndex := 0
patternIndex := 0
lastStarIndex := -1
lastStarMatchIndex := 0
for targetIndex < len(str) {
switch {
case patternIndex < len(pattern) && (pattern[patternIndex] == '?' || pattern[patternIndex] == str[targetIndex]):
targetIndex++
patternIndex++
case patternIndex < len(pattern) && pattern[patternIndex] == '*':
lastStarIndex = patternIndex
lastStarMatchIndex = targetIndex
patternIndex++
case lastStarIndex != -1:
patternIndex = lastStarIndex + 1
lastStarMatchIndex++
targetIndex = lastStarMatchIndex
default:
return false
}
}
for patternIndex < len(pattern) && pattern[patternIndex] == '*' {
patternIndex++
}
return patternIndex == len(pattern)
}
func compileWildcardPattern(pattern string) (*regexp.Regexp, error) {
escaped := regexp.QuoteMeta(pattern)
escaped = strings.ReplaceAll(escaped, `\*`, `.*`)
escaped = strings.ReplaceAll(escaped, `\?`, `.`)
escaped = "^" + escaped + "$"
return regexp.Compile(escaped)
}

View File

@@ -0,0 +1,211 @@
package wildcard
import "testing"
func TestMatchesWildcard(t *testing.T) {
tests := []struct {
name string
pattern string
str string
expected bool
}{
{"Exact match", "test", "test", true},
{"Single wildcard", "*", "anything", true},
{"Empty string with wildcard", "*", "", true},
{"Prefix wildcard", "test*", "test123", true},
{"Suffix wildcard", "*test", "123test", true},
{"Middle wildcard", "test*123", "testABC123", true},
{"Multiple wildcards", "test*abc*123", "testXYZabcDEF123", true},
{"No match", "test*", "other", false},
{"Single question mark", "test?", "test1", true},
{"Multiple question marks", "test??", "test12", true},
{"Question mark no match", "test?", "test12", false},
{"Mixed wildcards", "test*abc?def", "testXYZabc1def", true},
{"Empty pattern", "", "", true},
{"Empty pattern with string", "", "test", false},
{"Pattern with string empty", "test", "", false},
{"Pattern with regex special chars", "test[abc]", "test[abc]", true},
{"Pattern with dots", "test.txt", "test.txt", true},
{"Pattern with dots and wildcard", "*.txt", "test.txt", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MatchesWildcard(tt.pattern, tt.str)
if got != tt.expected {
t.Errorf("MatchesWildcard(%q, %q) = %v, want %v", tt.pattern, tt.str, got, tt.expected)
}
})
}
}
func TestWildcardMatcherMatch(t *testing.T) {
tests := []struct {
name string
pattern string
inputs []string
expected []bool
}{
{"Simple star", "test*", []string{"test", "test123", "testing", "other"}, []bool{true, true, true, false}},
{"Question mark", "test?", []string{"test1", "test2", "test", "test12"}, []bool{true, true, false, false}},
{"Extension filter", "*.txt", []string{"file.txt", "test.txt", "file.doc", "txt"}, []bool{true, true, false, false}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m, err := NewWildcardMatcher(tt.pattern)
if err != nil {
t.Fatalf("NewWildcardMatcher: %v", err)
}
for i, s := range tt.inputs {
got := m.Match(s)
if got != tt.expected[i] {
t.Errorf("Match(%q) = %v, want %v", s, got, tt.expected[i])
}
}
})
}
}
func TestCompileWildcardPattern(t *testing.T) {
tests := []struct {
pattern string
input string
want bool
}{
{"s3:Get*", "s3:GetObject", true},
{"s3:Get?bject", "s3:GetObject", true},
{"s3:*Object*", "s3:GetObjectAcl", true},
}
for _, tt := range tests {
t.Run(tt.pattern, func(t *testing.T) {
re, err := CompileWildcardPattern(tt.pattern)
if err != nil {
t.Fatalf("CompileWildcardPattern: %v", err)
}
if got := re.MatchString(tt.input); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
func TestFastMatchesWildcard(t *testing.T) {
tests := []struct {
pattern string
input string
want bool
}{
{"s3:Get*", "s3:GetObject", true},
{"s3:Put*", "s3:GetObject", false},
{"arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket/file.txt", true},
{"user:admin-*", "user:admin-john", true},
{"user:admin-*", "user:guest-john", false},
}
for _, tt := range tests {
t.Run(tt.pattern+"_"+tt.input, func(t *testing.T) {
got := FastMatchesWildcard(tt.pattern, tt.input)
if got != tt.want {
t.Errorf("FastMatchesWildcard(%q, %q) = %v, want %v", tt.pattern, tt.input, got, tt.want)
}
})
}
}
func TestWildcardMatcherCaching(t *testing.T) {
m1, err := GetCachedWildcardMatcher("s3:Get*")
if err != nil {
t.Fatal(err)
}
m2, err := GetCachedWildcardMatcher("s3:Get*")
if err != nil {
t.Fatal(err)
}
if m1 != m2 {
t.Error("expected same cached instance")
}
if !m1.Match("s3:GetObject") {
t.Error("expected match")
}
}
func TestWildcardMatcherCacheBounding(t *testing.T) {
wildcardMatcherCache.ClearCache()
orig := wildcardMatcherCache.maxSize
wildcardMatcherCache.maxSize = 3
defer func() {
wildcardMatcherCache.maxSize = orig
wildcardMatcherCache.ClearCache()
}()
for _, p := range []string{"p1", "p2", "p3"} {
GetCachedWildcardMatcher(p)
}
size, maxSize := wildcardMatcherCache.GetCacheStats()
if size != 3 {
t.Errorf("expected size 3, got %d", size)
}
if maxSize != 3 {
t.Errorf("expected maxSize 3, got %d", maxSize)
}
GetCachedWildcardMatcher("p4")
size, _ = wildcardMatcherCache.GetCacheStats()
if size != 3 {
t.Errorf("expected size 3 after eviction, got %d", size)
}
wildcardMatcherCache.mu.RLock()
defer wildcardMatcherCache.mu.RUnlock()
if _, ok := wildcardMatcherCache.matchers["p1"]; ok {
t.Error("p1 should have been evicted (LRU)")
}
if _, ok := wildcardMatcherCache.matchers["p4"]; !ok {
t.Error("p4 should be in cache")
}
}
func TestWildcardMatcherCacheLRU(t *testing.T) {
wildcardMatcherCache.ClearCache()
orig := wildcardMatcherCache.maxSize
wildcardMatcherCache.maxSize = 3
defer func() {
wildcardMatcherCache.maxSize = orig
wildcardMatcherCache.ClearCache()
}()
for _, p := range []string{"p1", "p2", "p3"} {
GetCachedWildcardMatcher(p)
}
GetCachedWildcardMatcher("p1") // access p1 to make it most-recently used
GetCachedWildcardMatcher("p4") // should evict p2 (now LRU)
wildcardMatcherCache.mu.RLock()
defer wildcardMatcherCache.mu.RUnlock()
if _, ok := wildcardMatcherCache.matchers["p2"]; ok {
t.Error("p2 should be evicted (least recently used)")
}
if _, ok := wildcardMatcherCache.matchers["p1"]; !ok {
t.Error("p1 should remain (recently accessed)")
}
if _, ok := wildcardMatcherCache.matchers["p3"]; !ok {
t.Error("p3 should remain")
}
if _, ok := wildcardMatcherCache.matchers["p4"]; !ok {
t.Error("p4 should be in cache")
}
}
func TestWildcardMatcherCacheClear(t *testing.T) {
GetCachedWildcardMatcher("test")
wildcardMatcherCache.ClearCache()
size, _ := wildcardMatcherCache.GetCacheStats()
if size != 0 {
t.Errorf("expected 0 after clear, got %d", size)
}
}
func BenchmarkMatchesWildcard(b *testing.B) {
for i := 0; i < b.N; i++ {
MatchesWildcard("s3:Get*", "s3:GetObject")
}
}
func BenchmarkFastMatchesWildcard(b *testing.B) {
for i := 0; i < b.N; i++ {
FastMatchesWildcard("s3:Get*", "s3:GetObject")
}
}