diff --git a/rules/rules.go b/rules/rules.go index 7c6ef11b..71f66930 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -31,7 +31,16 @@ func (r *Rule) Matches(path string) bool { return r.Regexp.MatchString(path) } - return strings.HasPrefix(path, r.Path) + if path == r.Path { + return true + } + + prefix := r.Path + if prefix != "/" && !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + return strings.HasPrefix(path, prefix) } // Regexp is a wrapper to the native regexp type where we diff --git a/rules/rules_test.go b/rules/rules_test.go index 570f921f..3046a2bd 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -2,6 +2,37 @@ package rules import "testing" +func TestRuleMatches(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + rulePath string + testPath string + want bool + }{ + {"exact match", "/uploads", "/uploads", true}, + {"child path", "/uploads", "/uploads/file.txt", true}, + {"sibling prefix", "/uploads", "/uploads_backup/secret.txt", false}, + {"root rule", "/", "/anything", true}, + {"trailing slash rule", "/uploads/", "/uploads/file.txt", true}, + {"trailing slash no sibling", "/uploads/", "/uploads_backup/file.txt", false}, + {"nested child", "/data/shared", "/data/shared/docs/file.txt", true}, + {"nested sibling", "/data/shared", "/data/shared_private/file.txt", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + r := &Rule{Path: tc.rulePath} + got := r.Matches(tc.testPath) + if got != tc.want { + t.Errorf("Rule{Path: %q}.Matches(%q) = %v; want %v", tc.rulePath, tc.testPath, got, tc.want) + } + }) + } +} + func TestMatchHidden(t *testing.T) { cases := map[string]bool{ "/": false,