Skip to content

Commit

Permalink
Append path separator to Shadow Copy root directory on Windows.
Browse files Browse the repository at this point in the history
Fixes kopia#3842.
  • Loading branch information
Hakkin committed May 31, 2024
1 parent fcb8197 commit 516fe9e
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 1 deletion.
28 changes: 27 additions & 1 deletion fs/localfs/local_fs_os.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ func (it *filesystemDirectoryIterator) Close() {
func (fsd *filesystemDirectory) Iterate(ctx context.Context) (fs.DirectoryIterator, error) {
fullPath := fsd.fullPath()

f, direrr := os.Open(fullPath) //nolint:gosec
openPath := maybeAppendPathSeparatorForVSSOnWindows(fullPath)

f, direrr := os.Open(openPath) //nolint:gosec
if direrr != nil {
return nil, errors.Wrap(direrr, "unable to read directory")
}
Expand All @@ -91,6 +93,30 @@ func (fsd *filesystemDirectory) Child(ctx context.Context, name string) (fs.Entr
return entryFromDirEntry(st, fullPath+string(filepath.Separator)), nil
}

// maybeAppendPathSeparatorForVSSOnWindows appends a path separator to the given path if the path is the root of a Windows Shadow Copy.
// Listing the contents of the shadow volume root directory may fail without a trailing \.
// See https://github.com/kopia/kopia/issues/3842
func maybeAppendPathSeparatorForVSSOnWindows(fullPath string) string {
//nolint:goconst
if runtime.GOOS != "windows" {
return fullPath
}

const shadowVolumePrefix = `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy`
if !strings.HasPrefix(fullPath, shadowVolumePrefix) {
return fullPath
}

// Verify whether we're at the root of the shadow volume by looking for a path separator after the shadow volume prefix
// If we find a path separator, we're either in a subdirectory or there was already a path separator appended to this path
remainder := fullPath[len(shadowVolumePrefix):]
if strings.ContainsRune(remainder, os.PathSeparator) {
return fullPath
}

return fullPath + string(os.PathSeparator)
}

func toDirEntryOrNil(dirEntry os.DirEntry, prefix string) (fs.Entry, error) {
fi, err := os.Lstat(prefix + dirEntry.Name())
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions fs/localfs/local_fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,28 @@ func TestDirPrefix(t *testing.T) {
}
}

func TestVSSAppendPath(t *testing.T) {
cases := map[string]string{
`C:\`: `C:\`,
`C:\temp`: `C:\temp`,
`/`: `/`,
`/unix/path`: `/unix/path`,
`\\server\path\subdir`: `\\server\path\subdir`,
`\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05\`: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05\`,
`\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05\hello`: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05\hello`,
}

if runtime.GOOS == "windows" {
cases[`\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05`] = `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05\`
} else {
cases[`\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05`] = `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy05`
}

for input, want := range cases {
require.Equal(t, want, maybeAppendPathSeparatorForVSSOnWindows(input), input)
}
}

func assertNoError(t *testing.T, err error) {
t.Helper()

Expand Down

0 comments on commit 516fe9e

Please sign in to comment.