fix(restore): restore single file with [ or ] in name on Windows (#1254)

This commit is contained in:
mayerwin
2026-06-30 01:24:13 -07:00
committed by GitHub
parent 49a448a320
commit 91d76acfe1
2 changed files with 27 additions and 9 deletions
+20 -1
View File
@@ -454,9 +454,28 @@ func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
var globEscapeReplacer = strings.NewReplacer(`\`, `\\`, `*`, `\*`, `?`, `\?`, `[`, `\[`, `]`, `\]`)
// On Windows, restic's --include uses Go's path/filepath.Match, which treats
// '\' as a path separator (not an escape character) and rejects the POSIX
// class self-escape `[[]` / `[]]` as an invalid pattern. There is therefore
// no way to express a literal '[' or ']' as a match character on Windows.
//
// NTFS forbids '*' and '?' in real filenames, so the single-character
// wildcard '?' is safe to use as a stand-in for '[' and ']' in a leaf-name
// pattern: it can never collide with a sibling whose name is exactly the
// requested file. It may, however, match a same-length sibling whose name
// differs only in the position of the substituted bracket — typically zero,
// occasionally one or two real-world siblings.
//
// Trade-off: the prior behaviour silently restored zero files when the
// requested leaf had a bracket in its name (matches no snapshot path).
// With this fix the requested file is restored; same-length siblings may
// also land in the target dir. A caller that wants exact-set semantics
// should sweep the target after restore against the requested path list.
var windowsBracketReplacer = strings.NewReplacer(`[`, `?`, `]`, `?`)
func escapeGlob(s string) string {
if runtime.GOOS == "windows" {
return s // escaping is not supported on Windows
return windowsBracketReplacer.Replace(s)
}
return globEscapeReplacer.Replace(s)
}
+7 -8
View File
@@ -97,10 +97,13 @@ func TestBackup(t *testing.T) {
func TestRestore(t *testing.T) {
t.Parallel()
// Use a filepath that exercises a few of the glob characters to test escaping
messyFilePathToTestGlobs := "test.txt"
if runtime.GOOS != "windows" {
messyFilePathToTestGlobs = "test*?[].txt"
// Use a filepath that exercises a few of the glob characters to test escaping.
// On Windows, '*' and '?' are forbidden in filenames, so the messy name only
// contains the brackets — '[' and ']' are legal in NTFS and are the chars
// the Windows path of escapeGlob has to handle without backslash escape.
messyFilePathToTestGlobs := "test*?[].txt"
if runtime.GOOS == "windows" {
messyFilePathToTestGlobs = "test[brackets].txt"
}
testFile := path.Join(t.TempDir(), messyFilePathToTestGlobs)
@@ -144,10 +147,6 @@ func TestRestore(t *testing.T) {
}
t.Logf("restore summary: %+v", restoreSummary)
if runtime.GOOS == "windows" {
return
}
if restoreSummary.FilesRestored != 1 {
t.Errorf("expected 1 new file, got %d", restoreSummary.FilesRestored)
}