From 7791b3e7d3723ca1b3f68615cf66cf0c1c52b4cf Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 17:05:58 +0800 Subject: [PATCH] fix: align directory wildcard matching --- chfs-ng/internal/auth/auth.go | 41 ++++++++++++++++------ chfs-ng/internal/auth/auth_test.go | 55 ++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/chfs-ng/internal/auth/auth.go b/chfs-ng/internal/auth/auth.go index 9a74e92..f7beadc 100644 --- a/chfs-ng/internal/auth/auth.go +++ b/chfs-ng/internal/auth/auth.go @@ -108,22 +108,43 @@ func matchPath(pattern, path string) bool { return false } for i, p := range pp { - if p == "*" { - return true - } - if p == "?" { - if ps[i] == "" { - return false - } - continue - } - if p != ps[i] { + if !matchSegment(p, ps[i]) { return false } } return true } +func matchSegment(pattern, value string) bool { + p := []rune(pattern) + v := []rune(value) + dp := make([][]bool, len(p)+1) + for i := range dp { + dp[i] = make([]bool, len(v)+1) + } + dp[0][0] = true + for i := 1; i <= len(p); i++ { + if p[i-1] == '*' { + dp[i][0] = dp[i-1][0] + } + } + for i := 1; i <= len(p); i++ { + for j := 1; j <= len(v); j++ { + switch p[i-1] { + case '*': + dp[i][j] = dp[i-1][j] || dp[i][j-1] + case '?': + dp[i][j] = dp[i-1][j-1] + default: + dp[i][j] = dp[i-1][j-1] && p[i-1] == v[j-1] + } + } + } + return dp[len(p)][len(v)] +} + +func MatchPath(pattern, path string) bool { return matchPath(pattern, path) } + func NewToken() string { b := make([]byte, 32) if _, e := rand.Read(b); e != nil { diff --git a/chfs-ng/internal/auth/auth_test.go b/chfs-ng/internal/auth/auth_test.go index dd06895..7804b3a 100644 --- a/chfs-ng/internal/auth/auth_test.go +++ b/chfs-ng/internal/auth/auth_test.go @@ -2,19 +2,52 @@ package auth import "testing" -func TestParseRules(t *testing.T) { - m, err := ParseRules([]string{"::r|admin:secret:rw:private:d"}) - if err != nil { - t.Fatal(err) +func TestMatchPath(t *testing.T) { + cases := []struct { + pattern, path string + want bool + }{ + {"test", "test", true}, + {"test", "test/sub", true}, + {"test", "testing", false}, + {"test*", "test2", true}, + {"test*", "test/sub", true}, + {"te?t", "test", true}, + {"te?t", "teat", true}, + {"te?t", "te", false}, + {"test/sub", "test/sub/file", true}, + {"test/sub", "test/other/file", false}, } - if !m.Guest.Mask.HasRead() || m.Guest.Mask.HasWrite() { - t.Fatal("guest mask") + for _, tc := range cases { + if got := MatchPath(tc.pattern, tc.path); got != tc.want { + t.Errorf("MatchPath(%q,%q)=%v want %v", tc.pattern, tc.path, got, tc.want) + } } - a := m.Accounts["admin"] - if a == nil || !a.Mask.HasWrite() || a.Mask.HasDelete() { - t.Fatal("admin mask") +} + +func TestMaskForLongestRule(t *testing.T) { + a := &Account{Mask: Read, Rules: []PathRule{ + {Dir: "team", Mask: Write}, + {Dir: "team/private", Mask: None}, + }} + if got := a.MaskFor("team/file"); got != Write { + t.Fatalf("team/file mask=%d", got) } - if got := a.MaskFor("private/x"); !got.HasDelete() { - t.Fatalf("subrule=%v", got) + if got := a.MaskFor("team/private/file"); got != None { + t.Fatalf("team/private/file mask=%d", got) + } + if got := a.MaskFor("other/file"); got != Read { + t.Fatalf("other/file mask=%d", got) + } +} + +func TestParseRulesGuestDefaultAndOverride(t *testing.T) { + m, err := ParseRules(nil) + if err != nil || m.Guest.Mask != (Read|Write|Delete) { + t.Fatalf("unexpected guest default: %#v err=%v", m.Guest, err) + } + m, err = ParseRules([]string{"::r|demo:pw:rw"}) + if err != nil || m.Guest.Mask != Read || m.Accounts["demo"].Mask != (Read|Write) { + t.Fatalf("unexpected parsed masks: %#v %#v err=%v", m.Guest, m.Accounts["demo"], err) } }