diff --git a/internal/fuse/dir.go b/internal/fuse/dir.go index cfb2aa71d..74ff50070 100644 --- a/internal/fuse/dir.go +++ b/internal/fuse/dir.go @@ -182,7 +182,7 @@ func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { } ret = append(ret, fuse.Dirent{ - Inode: fs.GenerateDynamicInode(d.inode, name), + Inode: inodeFromNode(d.inode, node), Type: typ, Name: name, }) @@ -206,13 +206,13 @@ func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) { } switch node.Type { case "dir": - return newDir(d.root, fs.GenerateDynamicInode(d.inode, name), d.inode, node) + return newDir(d.root, inodeFromNode(d.inode, node), d.inode, node) case "file": - return newFile(d.root, fs.GenerateDynamicInode(d.inode, name), node) + return newFile(d.root, inodeFromNode(d.inode, node), node) case "symlink": - return newLink(d.root, fs.GenerateDynamicInode(d.inode, name), node) + return newLink(d.root, inodeFromNode(d.inode, node), node) case "dev", "chardev", "fifo", "socket": - return newOther(d.root, fs.GenerateDynamicInode(d.inode, name), node) + return newOther(d.root, inodeFromNode(d.inode, node), node) default: debug.Log(" node %v has unknown type %v", name, node.Type) return nil, fuse.ENOENT diff --git a/internal/fuse/fuse_test.go b/internal/fuse/fuse_test.go index a46f9ba15..57125302a 100644 --- a/internal/fuse/fuse_test.go +++ b/internal/fuse/fuse_test.go @@ -118,7 +118,7 @@ func TestFuseFile(t *testing.T) { } root := &Root{repo: repo, blobCache: bloblru.New(blobCacheSize)} - inode := fs.GenerateDynamicInode(1, "foo") + inode := inodeFromNode(1, node) f, err := newFile(root, inode, node) rtest.OK(t, err) of, err := f.Open(context.TODO(), nil, nil) @@ -161,8 +161,8 @@ func TestFuseDir(t *testing.T) { ChangeTime: time.Unix(1606773732, 0), ModTime: time.Unix(1606773733, 0), } - parentInode := fs.GenerateDynamicInode(0, "parent") - inode := fs.GenerateDynamicInode(1, "foo") + parentInode := inodeFromName(0, "parent") + inode := inodeFromName(1, "foo") d, err := newDir(root, inode, parentInode, node) rtest.OK(t, err) @@ -219,3 +219,40 @@ func testTopUIDGID(t *testing.T, cfg Config, repo restic.Repository, uid, gid ui rtest.Equals(t, uint32(0), attr.Uid) rtest.Equals(t, uint32(0), attr.Gid) } + +func TestInodeFromNode(t *testing.T) { + node := &restic.Node{Name: "foo.txt", Type: "chardev", Links: 2} + ino1 := inodeFromNode(1, node) + ino2 := inodeFromNode(2, node) + rtest.Assert(t, ino1 == ino2, "inodes %d, %d of hard links differ", ino1, ino2) + + node.Links = 1 + ino1 = inodeFromNode(1, node) + ino2 = inodeFromNode(2, node) + rtest.Assert(t, ino1 != ino2, "same inode %d but different parent", ino1) +} + +var sink uint64 + +func BenchmarkInode(b *testing.B) { + for _, sub := range []struct { + name string + node restic.Node + }{ + { + name: "no_hard_links", + node: restic.Node{Name: "a somewhat long-ish filename.svg.bz2", Type: "fifo"}, + }, + { + name: "hard_link", + node: restic.Node{Name: "some other filename", Type: "file", Links: 2}, + }, + } { + b.Run(sub.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + sink = inodeFromNode(1, &sub.node) + } + }) + } +} diff --git a/internal/fuse/inode.go b/internal/fuse/inode.go new file mode 100644 index 000000000..de975b167 --- /dev/null +++ b/internal/fuse/inode.go @@ -0,0 +1,44 @@ +//go:build darwin || freebsd || linux +// +build darwin freebsd linux + +package fuse + +import ( + "encoding/binary" + + "github.com/cespare/xxhash/v2" + "github.com/restic/restic/internal/restic" +) + +// inodeFromName generates an inode number for a file in a meta dir. +func inodeFromName(parent uint64, name string) uint64 { + inode := parent ^ xxhash.Sum64String(cleanupNodeName(name)) + + // Inode 0 is invalid and 1 is the root. Remap those. + if inode < 2 { + inode += 2 + } + return inode +} + +// inodeFromNode generates an inode number for a file within a snapshot. +func inodeFromNode(parent uint64, node *restic.Node) (inode uint64) { + if node.Links > 1 && node.Type != "dir" { + // If node has hard links, give them all the same inode, + // irrespective of the parent. + var buf [16]byte + binary.LittleEndian.PutUint64(buf[:8], node.DeviceID) + binary.LittleEndian.PutUint64(buf[8:], node.Inode) + inode = xxhash.Sum64(buf[:]) + } else { + // Else, use the name and the parent inode. + // node.{DeviceID,Inode} may not even be reliable. + inode = parent ^ xxhash.Sum64String(cleanupNodeName(node.Name)) + } + + // Inode 0 is invalid and 1 is the root. Remap those. + if inode < 2 { + inode += 2 + } + return inode +} diff --git a/internal/fuse/snapshots_dir.go b/internal/fuse/snapshots_dir.go index 79c8378d8..21f29fe23 100644 --- a/internal/fuse/snapshots_dir.go +++ b/internal/fuse/snapshots_dir.go @@ -78,7 +78,7 @@ func (d *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { for name, entry := range meta.names { d := fuse.Dirent{ - Inode: fs.GenerateDynamicInode(d.inode, name), + Inode: inodeFromName(d.inode, name), Name: name, Type: fuse.DT_Dir, } @@ -105,11 +105,11 @@ func (d *SnapshotsDir) Lookup(ctx context.Context, name string) (fs.Node, error) entry := meta.names[name] if entry != nil { if entry.linkTarget != "" { - return newSnapshotLink(d.root, fs.GenerateDynamicInode(d.inode, name), entry.linkTarget, entry.snapshot) + return newSnapshotLink(d.root, inodeFromName(d.inode, name), entry.linkTarget, entry.snapshot) } else if entry.snapshot != nil { - return newDirFromSnapshot(d.root, fs.GenerateDynamicInode(d.inode, name), entry.snapshot) + return newDirFromSnapshot(d.root, inodeFromName(d.inode, name), entry.snapshot) } else { - return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.inode, name), d.inode, d.dirStruct, d.prefix+"/"+name), nil + return NewSnapshotsDir(d.root, inodeFromName(d.inode, name), d.inode, d.dirStruct, d.prefix+"/"+name), nil } }