package fs import ( "io" "os" "path/filepath" "slices" "testing" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" ) type fsLocalMetadataTestcase struct { name string follow bool setup func(t *testing.T, path string) nodeType restic.NodeType } func TestFSLocalMetadata(t *testing.T) { for _, test := range []fsLocalMetadataTestcase{ { name: "file", setup: func(t *testing.T, path string) { rtest.OK(t, os.WriteFile(path, []byte("example"), 0o600)) }, nodeType: restic.NodeTypeFile, }, { name: "directory", setup: func(t *testing.T, path string) { rtest.OK(t, os.Mkdir(path, 0o600)) }, nodeType: restic.NodeTypeDir, }, { name: "symlink", setup: func(t *testing.T, path string) { rtest.OK(t, os.Symlink(path+"old", path)) }, nodeType: restic.NodeTypeSymlink, }, { name: "symlink file", follow: true, setup: func(t *testing.T, path string) { rtest.OK(t, os.WriteFile(path+"file", []byte("example"), 0o600)) rtest.OK(t, os.Symlink(path+"file", path)) }, nodeType: restic.NodeTypeFile, }, } { runFSLocalTestcase(t, test) } } func runFSLocalTestcase(t *testing.T, test fsLocalMetadataTestcase) { t.Run(test.name, func(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "item") test.setup(t, path) testFs := &Local{} flags := 0 if !test.follow { flags |= O_NOFOLLOW } f, err := testFs.OpenFile(path, flags, true) rtest.OK(t, err) checkMetadata(t, f, path, test.follow, test.nodeType) rtest.OK(t, f.Close()) }) } func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType restic.NodeType) { fi, err := f.Stat() rtest.OK(t, err) var fi2 os.FileInfo if follow { fi2, err = os.Stat(path) } else { fi2, err = os.Lstat(path) } rtest.OK(t, err) assertFIEqual(t, fi2, fi) node, err := f.ToNode(false) rtest.OK(t, err) // ModTime is likely unique per file, thus it provides a good indication that it is from the correct file rtest.Equals(t, fi.ModTime, node.ModTime, "node ModTime") rtest.Equals(t, nodeType, node.Type, "node Type") } func assertFIEqual(t *testing.T, want os.FileInfo, got *ExtendedFileInfo) { t.Helper() rtest.Equals(t, want.Name(), got.Name, "Name") rtest.Equals(t, want.ModTime(), got.ModTime, "ModTime") rtest.Equals(t, want.Mode(), got.Mode, "Mode") rtest.Equals(t, want.Size(), got.Size, "Size") } func TestFSLocalRead(t *testing.T) { testFSLocalRead(t, false) testFSLocalRead(t, true) } func testFSLocalRead(t *testing.T, makeReadable bool) { tmp := t.TempDir() path := filepath.Join(tmp, "item") testdata := "example" rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600)) f := openReadable(t, path, makeReadable) checkMetadata(t, f, path, false, restic.NodeTypeFile) data, err := io.ReadAll(f) rtest.OK(t, err) rtest.Equals(t, testdata, string(data), "file content mismatch") rtest.OK(t, f.Close()) } func openReadable(t *testing.T, path string, useMakeReadable bool) File { testFs := &Local{} f, err := testFs.OpenFile(path, O_NOFOLLOW, useMakeReadable) rtest.OK(t, err) if useMakeReadable { // file was opened as metadataOnly. open for reading rtest.OK(t, f.MakeReadable()) } return f } func TestFSLocalReaddir(t *testing.T) { testFSLocalReaddir(t, false) testFSLocalReaddir(t, true) } func testFSLocalReaddir(t *testing.T, makeReadable bool) { tmp := t.TempDir() path := filepath.Join(tmp, "item") rtest.OK(t, os.Mkdir(path, 0o700)) entries := []string{"testfile"} rtest.OK(t, os.WriteFile(filepath.Join(path, entries[0]), []byte("example"), 0o600)) f := openReadable(t, path, makeReadable) checkMetadata(t, f, path, false, restic.NodeTypeDir) names, err := f.Readdirnames(-1) rtest.OK(t, err) slices.Sort(names) rtest.Equals(t, entries, names, "directory content mismatch") rtest.OK(t, f.Close()) } func TestFSLocalReadableRace(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "item") testdata := "example" rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600)) testFs := &Local{} f, err := testFs.OpenFile(path, O_NOFOLLOW, true) rtest.OK(t, err) pathNew := path + "new" rtest.OK(t, os.Rename(path, pathNew)) err = f.MakeReadable() if err == nil { // a file handle based implementation should still work checkMetadata(t, f, pathNew, false, restic.NodeTypeFile) data, err := io.ReadAll(f) rtest.OK(t, err) rtest.Equals(t, testdata, string(data), "file content mismatch") } rtest.OK(t, f.Close()) } func TestFSLocalTypeChange(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "item") testdata := "example" rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600)) testFs := &Local{} f, err := testFs.OpenFile(path, O_NOFOLLOW, true) rtest.OK(t, err) // cache metadata _, err = f.Stat() rtest.OK(t, err) pathNew := path + "new" // rename instead of unlink to let the test also work on windows rtest.OK(t, os.Rename(path, pathNew)) rtest.OK(t, os.Mkdir(path, 0o700)) rtest.OK(t, f.MakeReadable()) fi, err := f.Stat() rtest.OK(t, err) if !fi.Mode.IsDir() { // a file handle based implementation should still reference the file checkMetadata(t, f, pathNew, false, restic.NodeTypeFile) data, err := io.ReadAll(f) rtest.OK(t, err) rtest.Equals(t, testdata, string(data), "file content mismatch") } // else: // path-based implementation // nothing to test here. stat returned the new file type rtest.OK(t, f.Close()) }