diff --git a/internal/restorer/fileswriter.go b/internal/restorer/fileswriter.go index 50f06c83d..39ad65da8 100644 --- a/internal/restorer/fileswriter.go +++ b/internal/restorer/fileswriter.go @@ -1,11 +1,15 @@ package restorer import ( + "fmt" + stdfs "io/fs" "os" "sync" + "syscall" "github.com/cespare/xxhash/v2" "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" ) @@ -39,13 +43,26 @@ func newFilesWriter(count int) *filesWriter { } } -func createFile(path string, createSize int64, sparse bool) (*os.File, error) { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) +func openFile(path string) (*os.File, error) { + f, err := os.OpenFile(path, os.O_WRONLY|fs.O_NOFOLLOW, 0600) if err != nil { - if !fs.IsAccessDenied(err) { - return nil, err - } + return nil, err + } + fi, err := f.Stat() + if err != nil { + _ = f.Close() + return nil, err + } + if !fi.Mode().IsRegular() { + _ = f.Close() + return nil, fmt.Errorf("unexpected file type %v at %q", fi.Mode().Type(), path) + } + return f, nil +} +func createFile(path string, createSize int64, sparse bool) (*os.File, error) { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|fs.O_NOFOLLOW, 0600) + if err != nil && fs.IsAccessDenied(err) { // If file is readonly, clear the readonly flag by resetting the // permissions of the file and try again // as the metadata will be set again in the second pass and the @@ -53,40 +70,75 @@ func createFile(path string, createSize int64, sparse bool) (*os.File, error) { if err = fs.ResetPermissions(path); err != nil { return nil, err } - if f, err = os.OpenFile(path, os.O_WRONLY, 0600); err != nil { + if f, err = os.OpenFile(path, os.O_WRONLY|fs.O_NOFOLLOW, 0600); err != nil { + return nil, err + } + } else if err != nil && (errors.Is(err, syscall.ELOOP) || errors.Is(err, syscall.EISDIR)) { + // symlink or directory, try to remove it later on + f = nil + } else if err != nil { + return nil, err + } + + var fi stdfs.FileInfo + if f != nil { + // stat to check that we've opened a regular file + fi, err = f.Stat() + if err != nil { + _ = f.Close() + return nil, err + } + } + if f == nil || !fi.Mode().IsRegular() { + // close handle if we still have it + if f != nil { + if err := f.Close(); err != nil { + return nil, err + } + } + + // not what we expected, try to get rid of it + if err := os.Remove(path); err != nil { + return nil, err + } + // create a new file, pass O_EXCL to make sure there are no surprises + f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL|fs.O_NOFOLLOW, 0600) + if err != nil { + return nil, err + } + fi, err = f.Stat() + if err != nil { + _ = f.Close() return nil, err } } + return ensureSize(f, fi, createSize, sparse) +} + +func ensureSize(f *os.File, fi stdfs.FileInfo, createSize int64, sparse bool) (*os.File, error) { if sparse { - err = truncateSparse(f, createSize) + err := truncateSparse(f, createSize) if err != nil { _ = f.Close() return nil, err } - } else { - info, err := f.Stat() + } else if fi.Size() > createSize { + // file is too long must shorten it + err := f.Truncate(createSize) if err != nil { _ = f.Close() return nil, err } - if info.Size() > createSize { - // file is too long must shorten it - err = f.Truncate(createSize) - if err != nil { - _ = f.Close() - return nil, err - } - } else if createSize > 0 { - err := fs.PreallocateFile(f, createSize) - if err != nil { - // Just log the preallocate error but don't let it cause the restore process to fail. - // Preallocate might return an error if the filesystem (implementation) does not - // support preallocation or our parameters combination to the preallocate call - // This should yield a syscall.ENOTSUP error, but some other errors might also - // show up. - debug.Log("Failed to preallocate %v with size %v: %v", path, createSize, err) - } + } else if createSize > 0 { + err := fs.PreallocateFile(f, createSize) + if err != nil { + // Just log the preallocate error but don't let it cause the restore process to fail. + // Preallocate might return an error if the filesystem (implementation) does not + // support preallocation or our parameters combination to the preallocate call + // This should yield a syscall.ENOTSUP error, but some other errors might also + // show up. + debug.Log("Failed to preallocate %v with size %v: %v", f.Name(), createSize, err) } } return f, nil @@ -110,7 +162,7 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create if err != nil { return nil, err } - } else if f, err = os.OpenFile(path, os.O_WRONLY, 0600); err != nil { + } else if f, err = openFile(path); err != nil { return nil, err } diff --git a/internal/restorer/fileswriter_test.go b/internal/restorer/fileswriter_test.go index 7beb9a2dc..74bf479bb 100644 --- a/internal/restorer/fileswriter_test.go +++ b/internal/restorer/fileswriter_test.go @@ -1,9 +1,13 @@ package restorer import ( + "fmt" "os" + "path/filepath" + "syscall" "testing" + "github.com/restic/restic/internal/errors" rtest "github.com/restic/restic/internal/test" ) @@ -34,3 +38,76 @@ func TestFilesWriterBasic(t *testing.T) { rtest.OK(t, err) rtest.Equals(t, []byte{2, 2}, buf) } + +func TestCreateFile(t *testing.T) { + basepath := filepath.Join(t.TempDir(), "test") + + scenarios := []struct { + name string + create func(t testing.TB, path string) + err error + }{ + { + "file", + func(t testing.TB, path string) { + rtest.OK(t, os.WriteFile(path, []byte("test-test-test-data"), 0o400)) + }, + nil, + }, + { + "empty dir", + func(t testing.TB, path string) { + rtest.OK(t, os.Mkdir(path, 0o400)) + }, + nil, + }, + { + "symlink", + func(t testing.TB, path string) { + rtest.OK(t, os.Symlink("./something", path)) + }, + nil, + }, + { + "filled dir", + func(t testing.TB, path string) { + rtest.OK(t, os.Mkdir(path, 0o700)) + rtest.OK(t, os.WriteFile(filepath.Join(path, "file"), []byte("data"), 0o400)) + }, + syscall.ENOTEMPTY, + }, + } + + tests := []struct { + size int64 + isSparse bool + }{ + {5, false}, + {21, false}, + {100, false}, + {5, true}, + {21, true}, + {100, true}, + } + + for i, sc := range scenarios { + t.Run(sc.name, func(t *testing.T) { + for _, test := range tests { + path := basepath + fmt.Sprintf("%v", i) + sc.create(t, path) + f, err := createFile(path, test.size, test.isSparse) + if sc.err == nil { + rtest.OK(t, err) + fi, err := f.Stat() + rtest.OK(t, err) + rtest.Assert(t, fi.Mode().IsRegular(), "wrong filetype %v", fi.Mode()) + rtest.Assert(t, fi.Size() <= test.size, "unexpected file size expected %v, got %v", test.size, fi.Size()) + rtest.OK(t, f.Close()) + } else { + rtest.Assert(t, errors.Is(err, sc.err), "unexpected error got %v expected %v", err, sc.err) + } + rtest.OK(t, os.RemoveAll(path)) + } + }) + } +}