diff --git a/changelog/unreleased/pull-3819 b/changelog/unreleased/pull-3819 new file mode 100644 index 000000000..7bbce5005 --- /dev/null +++ b/changelog/unreleased/pull-3819 @@ -0,0 +1,10 @@ +Enhancement: Validate include/exclude patterns before restoring + +Patterns provided to `restic restore` via `--exclude`, `--iexclude`, +`--include` and `--iinclude` weren't validated before running the restore. +Invalid patterns would result in error messages being printed repeatedly +and possibly unwanted files being restored. +restic now validates all patterns before running the restore and aborts with +a fatal error if an invalid pattern is detected. + +https://github.com/restic/restic/pull/3819 diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 3c7baeb0f..addd36661 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -70,6 +70,28 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { hasExcludes := len(opts.Exclude) > 0 || len(opts.InsensitiveExclude) > 0 hasIncludes := len(opts.Include) > 0 || len(opts.InsensitiveInclude) > 0 + // Validate provided patterns + if len(opts.Exclude) > 0 { + if valid, invalidPatterns := filter.ValidatePatterns(opts.Exclude); !valid { + return errors.Fatalf("--exclude: invalid pattern(s) provided:\n%s", strings.Join(invalidPatterns, "\n")) + } + } + if len(opts.InsensitiveExclude) > 0 { + if valid, invalidPatterns := filter.ValidatePatterns(opts.InsensitiveExclude); !valid { + return errors.Fatalf("--iexclude: invalid pattern(s) provided:\n%s", strings.Join(invalidPatterns, "\n")) + } + } + if len(opts.Include) > 0 { + if valid, invalidPatterns := filter.ValidatePatterns(opts.Include); !valid { + return errors.Fatalf("--include: invalid pattern(s) provided:\n%s", strings.Join(invalidPatterns, "\n")) + } + } + if len(opts.InsensitiveInclude) > 0 { + if valid, invalidPatterns := filter.ValidatePatterns(opts.InsensitiveInclude); !valid { + return errors.Fatalf("--iinclude: invalid pattern(s) provided:\n%s", strings.Join(invalidPatterns, "\n")) + } + } + for i, str := range opts.InsensitiveExclude { opts.InsensitiveExclude[i] = strings.ToLower(str) } diff --git a/cmd/restic/integration_filter_pattern_test.go b/cmd/restic/integration_filter_pattern_test.go index e1534d841..c0c1d932f 100644 --- a/cmd/restic/integration_filter_pattern_test.go +++ b/cmd/restic/integration_filter_pattern_test.go @@ -67,3 +67,40 @@ func TestBackupFailsWhenUsingInvalidPatternsFromFile(t *testing.T) { *[._]log[.-][0-9] !*[._]log[.-][0-9]`, err.Error()) } + +func TestRestoreFailsWhenUsingInvalidPatterns(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + testRunInit(t, env.gopts) + + var err error + + // Test --exclude + err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{Exclude: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}, env.gopts) + + rtest.Equals(t, `Fatal: --exclude: invalid pattern(s) provided: +*[._]log[.-][0-9] +!*[._]log[.-][0-9]`, err.Error()) + + // Test --iexclude + err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{InsensitiveExclude: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}, env.gopts) + + rtest.Equals(t, `Fatal: --iexclude: invalid pattern(s) provided: +*[._]log[.-][0-9] +!*[._]log[.-][0-9]`, err.Error()) + + // Test --include + err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{Include: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}, env.gopts) + + rtest.Equals(t, `Fatal: --include: invalid pattern(s) provided: +*[._]log[.-][0-9] +!*[._]log[.-][0-9]`, err.Error()) + + // Test --iinclude + err = testRunRestoreAssumeFailure(t, "latest", RestoreOptions{InsensitiveInclude: []string{"*[._]log[.-][0-9]", "!*[._]log[.-][0-9]"}}, env.gopts) + + rtest.Equals(t, `Fatal: --iinclude: invalid pattern(s) provided: +*[._]log[.-][0-9] +!*[._]log[.-][0-9]`, err.Error()) +} diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index cefea852a..f15b3d9fd 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -131,6 +131,12 @@ func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snaps rtest.OK(t, runRestore(opts, gopts, []string{snapshotID.String()})) } +func testRunRestoreAssumeFailure(t testing.TB, snapshotID string, opts RestoreOptions, gopts GlobalOptions) error { + err := runRestore(opts, gopts, []string{snapshotID}) + + return err +} + func testRunCheck(t testing.TB, gopts GlobalOptions) { opts := CheckOptions{ ReadData: true,