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
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:
246
weed/util/wildcard/collection_filter.go
Normal file
246
weed/util/wildcard/collection_filter.go
Normal 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
|
||||
}
|
||||
74
weed/util/wildcard/collection_filter_test.go
Normal file
74
weed/util/wildcard/collection_filter_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
weed/util/wildcard/filter.go
Normal file
39
weed/util/wildcard/filter.go
Normal 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
|
||||
}
|
||||
186
weed/util/wildcard/wildcard_matcher.go
Normal file
186
weed/util/wildcard/wildcard_matcher.go
Normal 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)
|
||||
}
|
||||
211
weed/util/wildcard/wildcard_matcher_test.go
Normal file
211
weed/util/wildcard/wildcard_matcher_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user