diff --git a/internal/wclayer/legacy.go b/internal/wclayer/legacy.go index e73de48b00..499e8ae993 100644 --- a/internal/wclayer/legacy.go +++ b/internal/wclayer/legacy.go @@ -194,16 +194,30 @@ func (r *legacyLayerReader) reset() { func findBackupStreamSize(r io.Reader) (int64, error) { br := winio.NewBackupStreamReader(r) + var size int64 for { hdr, err := br.Next() if err != nil { if errors.Is(err, io.EOF) { - err = nil + // With no inline data, sparse blocks provide the logical size. + return size, nil } return 0, err } - if hdr.Id == winio.BackupData { - return hdr.Size, nil + switch hdr.Id { + case winio.BackupData: + // Non-sparse files and sparse files with inline data carry their + // logical size in BackupData. + if hdr.Size > 0 || hdr.Attributes&winio.StreamSparseAttributes == 0 { + return hdr.Size, nil + } + // A sparse BackupData stream without inline data is followed by + // sparse blocks that describe its logical size. + case winio.BackupSparseBlock: + // The terminal zero-length block has an offset at logical EOF. + if end := hdr.Offset + hdr.Size; end > size { + size = end + } } } } diff --git a/internal/wclayer/legacy_test.go b/internal/wclayer/legacy_test.go index cd916f9bca..398fe6171e 100644 --- a/internal/wclayer/legacy_test.go +++ b/internal/wclayer/legacy_test.go @@ -9,6 +9,9 @@ import ( "os" "path/filepath" "testing" + + winio "github.com/Microsoft/go-winio" + "golang.org/x/sys/windows" ) // errWriter always fails writes, used to simulate a full disk (ENOSPC) when the @@ -161,3 +164,118 @@ func Test_legacyLayerWriter_reset_ClosesFileOnSuccess(t *testing.T) { t.Errorf("expected temp file to be removable after reset (handle closed), got: %v", err) } } + +func setSparse(t *testing.T, f *os.File) { + t.Helper() + if err := windows.DeviceIoControl(windows.Handle(f.Fd()), windows.FSCTL_SET_SPARSE, nil, 0, nil, 0, nil, nil); err != nil { + t.Fatalf("set sparse: %v", err) + } +} + +func TestFindBackupStreamSizeSparse(t *testing.T) { + //nolint:gosec // G306: test files do not need restrictive permissions + for name, setup := range map[string]func(*testing.T) string{ + "normalFile": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + if err := os.WriteFile(path, []byte("testing 1 2 3\n"), 0644); err != nil { + t.Fatal(err) + } + return path + }, + "normalFileEmpty": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + f.Close() + return path + }, + "sparseEmpty": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + defer f.Close() + setSparse(t, f) + return path + }, + "sparseAllHoles": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + defer f.Close() + setSparse(t, f) + if err := f.Truncate(1048576); err != nil { + t.Fatal(err) + } + return path + }, + "sparseOneRange": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + defer f.Close() + setSparse(t, f) + if _, err := f.WriteString("test sparse data"); err != nil { + t.Fatal(err) + } + return path + }, + "sparseMultipleRanges": func(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "foo") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + defer f.Close() + setSparse(t, f) + if _, err = f.Write([]byte("leading data\n")); err != nil { + t.Fatal(err) + } + if _, err = f.Seek(1048576, 0); err != nil { + t.Fatal(err) + } + if _, err = f.Write([]byte("trailing data\n")); err != nil { + t.Fatal(err) + } + return path + }, + } { + t.Run(name, func(t *testing.T) { + path := setup(t) + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + br := winio.NewBackupFileReader(f, true) + defer br.Close() + + size, err := findBackupStreamSize(br) + if err != nil { + t.Fatalf("findBackupStreamSize: %v", err) + } + if size != fi.Size() { + t.Errorf("findBackupStreamSize = %d, want logical size %d", size, fi.Size()) + } + }) + } +}