1 Commits
Author SHA1 Message Date
clawbot 5ca0d46932 Refuse an exclude pattern that does not compile when the config loads (closes #455)
check / check (push) Canceled after 0s
An exclude pattern that did not compile was skipped with a log warning during the backup, which then stored what it meant to leave out. Config.Validate now refuses such a pattern, global or per snapshot, naming the setting and quoting the pattern. The compile step moves to a new package, internal/exclude, which both the config and the scanner call.

Model: opus-5-5
2026-11-11 10:58:54 +01:00
4 changed files with 77 additions and 3 deletions

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ func validateExcludeList(setting string, patterns []string) error {
for i, pattern := range patterns { for i, pattern := range patterns {
_, _, err := exclude.Compile(pattern) _, _, err := exclude.Compile(pattern)
if err != nil { if err != nil {
return fmt.Errorf("%s[%d]: %q %w", setting, i, pattern, err) return fmt.Errorf("%s[%d]: %w", setting, i, err)
} }
} }
+2 -1
View File
@@ -47,7 +47,8 @@ func Compile(pattern string) ([]glob.Glob, bool, error) {
for _, variant := range doubleStarVariants(trimmed) { for _, variant := range doubleStarVariants(trimmed) {
g, err := glob.Compile(variant, '/') g, err := glob.Compile(variant, '/')
if err != nil { if err != nil {
return nil, false, fmt.Errorf("%w: %w", errBadExcludePattern, err) return nil, false, fmt.Errorf("%q %w: %w",
pattern, errBadExcludePattern, err)
} }
globs = append(globs, g) globs = append(globs, g)
+73
View File
@@ -0,0 +1,73 @@
package exclude_test
import (
"slices"
"strings"
"testing"
"github.com/gobwas/glob"
"sneak.berlin/go/vaultik/internal/exclude"
)
// TestCompile checks the error, the anchoring and the globs that Compile
// returns for one exclude pattern.
func TestCompile(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pattern string
wantErr string
wantAnchored bool
// wantMatches lists paths that one of the globs must match. When
// it is empty, Compile must return no globs.
wantMatches []string
}{
{name: "does not compile", pattern: "build[",
wantErr: `"build[" is not a valid exclude pattern`},
{name: "empty", pattern: ""},
{name: "leading slash anchors", pattern: "/build",
wantAnchored: true, wantMatches: []string{"build"}},
{name: "double star after the slash clears anchoring",
pattern: "/**/build", wantMatches: []string{"build"}},
{name: "double star in the middle", pattern: "a/**/*.log",
wantMatches: []string{"a/x.log", "a/b/x.log"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
globs, anchored, err := exclude.Compile(tt.pattern)
if tt.wantErr == "" && err != nil {
t.Fatalf("Compile(%q) error = %v, want none", tt.pattern, err)
}
if tt.wantErr != "" &&
(err == nil || !strings.HasPrefix(err.Error(), tt.wantErr)) {
t.Fatalf("Compile(%q) error = %v, want one starting with %q",
tt.pattern, err, tt.wantErr)
}
if anchored != tt.wantAnchored {
t.Errorf("Compile(%q) anchored = %t, want %t",
tt.pattern, anchored, tt.wantAnchored)
}
if len(tt.wantMatches) == 0 && len(globs) != 0 {
t.Errorf("Compile(%q) returned %d globs, want none",
tt.pattern, len(globs))
}
for _, path := range tt.wantMatches {
matched := slices.ContainsFunc(globs, func(g glob.Glob) bool {
return g.Match(path)
})
if !matched {
t.Errorf("Compile(%q) globs do not match %q",
tt.pattern, path)
}
}
})
}
}
+1 -1
View File
@@ -2331,7 +2331,7 @@ func compileExcludePatterns(patterns []string) ([]compiledPattern, error) {
for _, p := range patterns { for _, p := range patterns {
globs, anchored, err := exclude.Compile(p) globs, anchored, err := exclude.Compile(p)
if err != nil { if err != nil {
return nil, fmt.Errorf("%q %w", p, err) return nil, err
} }
for _, g := range globs { for _, g := range globs {