Files
go-fuse/fs/bridge_test.go

282 lines
7.2 KiB
Go

// Copyright 2019 the Go-FUSE Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fs
import (
"context"
"log"
"os"
"strings"
"sync/atomic"
"syscall"
"testing"
"time"
"unsafe"
"github.com/hanzoai/go-fuse/v2/fuse"
)
// TestBridgeReaddirPlusVirtualEntries looks at "." and ".." in the ReadDirPlus
// output. They should exist, but the NodeId should be zero.
func TestBridgeReaddirPlusVirtualEntries(t *testing.T) {
// Set suppressDebug as we do our own logging
tc := newTestCase(t, &testOptions{suppressDebug: true})
rb := tc.rawFS.(*rawBridge)
// We only populate what rawBridge.OpenDir() actually looks at.
openIn := fuse.OpenIn{}
openIn.NodeId = 1 // root node always has id 1 and always exists
openOut := fuse.OpenOut{}
status := rb.OpenDir(nil, &openIn, &openOut)
if !status.Ok() {
t.Fatal(status)
}
releaseIn := fuse.ReleaseIn{
Fh: openOut.Fh,
}
releaseIn.NodeId = 1
defer rb.ReleaseDir(&releaseIn)
// We only populate what rawBridge.ReadDirPlus() actually looks at.
readIn := fuse.ReadIn{}
readIn.NodeId = 1
readIn.Fh = openOut.Fh
buf := make([]byte, 400)
dirents := fuse.NewDirEntryList(buf, 0)
status = rb.ReadDirPlus(nil, &readIn, dirents)
if !status.Ok() {
t.Fatal(status)
}
// Parse the output buffer. Looks like this in memory:
// 1) fuse.EntryOut
// 2) fuse._Dirent
// 3) Name (null-terminated)
// 4) Padding to align to 8 bytes
// [repeat]
const entryOutSize = int(unsafe.Sizeof(fuse.EntryOut{}))
// = unsafe.Sizeof(fuse._Dirent{}), see fuse/types.go
const direntSize = 24
// Round up to 8.
const entry2off = (entryOutSize + direntSize + len(".\x00") + 7) / 8 * 8
names := map[string]*fuse.EntryOut{}
// 1st entry should be "."
entry1 := (*fuse.EntryOut)(unsafe.Pointer(&buf[0]))
name1 := string(buf[entryOutSize+direntSize : entryOutSize+direntSize+2])
names[name1] = entry1
// 2nd entry should be ".."
entry2 := (*fuse.EntryOut)(unsafe.Pointer(&buf[entry2off]))
name2 := string(buf[entry2off+entryOutSize+direntSize : entry2off+entryOutSize+direntSize+2])
names[name2] = entry2
if len(names) != 2 || names[".\000"] == nil || names[".."] == nil {
t.Fatalf(`got %v, want {".\\0", ".."}`, names)
}
for k, v := range names {
if v.NodeId != 0 {
t.Errorf("entry %q NodeId should be 0, but is %d", k, v.NodeId)
}
}
}
// TestTypeChange simulates inode number reuse that happens on real
// filesystems. For go-fuse, inode number reuse can look like a file changing
// to a directory or vice versa. Acutally, the old inode does not exist anymore,
// we just have not received the FORGET yet.
func TestTypeChange(t *testing.T) {
rootNode := testTypeChangeIno{}
mnt, _ := testMount(t, &rootNode, nil)
for i := 0; i < 100; i++ {
fi, err := os.Stat(mnt + "/file")
if err != nil {
t.Fatalf("Stat(file): %v", err)
}
syscall.Unlink(mnt + "/file")
fi, err = os.Stat(mnt + "/dir")
if err != nil {
t.Fatalf("Stat(dir): %v", err)
}
if !fi.IsDir() {
t.Fatal("should be a dir now")
}
syscall.Rmdir(mnt + "/dir")
fi, _ = os.Stat(mnt + "/file")
if fi.IsDir() {
t.Fatal("should be a file now")
}
}
}
type testTypeChangeIno struct {
Inode
}
// Lookup function for TestTypeChange:
// If name == "dir", returns a node of type dir,
// if name == "file" of type file,
// otherwise ENOENT.
func (fn *testTypeChangeIno) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
var mode uint32
switch name {
case "file":
mode = syscall.S_IFREG
case "dir":
mode = syscall.S_IFDIR
default:
return nil, syscall.ENOENT
}
stable := StableAttr{
Mode: mode,
Ino: 1234,
}
childFN := &testTypeChangeIno{}
child := fn.NewInode(ctx, childFN, stable)
return child, syscall.F_OK
}
// TestDeletedInodePath checks that Inode.Path returns ".deleted" if an Inode is
// disconnected from the hierarchy (=orphaned)
func TestDeletedInodePath(t *testing.T) {
rootNode := testDeletedIno{}
mnt, _ := testMount(t, &rootNode, &Options{Logger: log.New(os.Stderr, "", 0)})
// Open a file handle so the kernel cannot FORGET the inode
fd, err := os.Open(mnt + "/dir")
if err != nil {
t.Fatal(err)
}
defer fd.Close()
// Delete it so the inode does not have a path anymore
err = syscall.Rmdir(mnt + "/dir")
if err != nil {
t.Fatal(err)
}
atomic.StoreInt32(&rootNode.deleted, 1)
// Our Getattr implementation `testDeletedIno.Getattr` should return
// ENFILE when everything looks ok, EILSEQ otherwise.
var st syscall.Stat_t
err = syscall.Fstat(int(fd.Fd()), &st)
if err != syscall.ENFILE {
t.Error(err)
}
}
type testDeletedIno struct {
Inode
deleted int32
}
func (n *testDeletedIno) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
ino := n.Root().Operations().(*testDeletedIno)
if atomic.LoadInt32(&ino.deleted) == 1 {
return nil, syscall.ENOENT
}
if name != "dir" {
return nil, syscall.ENOENT
}
childNode := &testDeletedIno{}
stable := StableAttr{Mode: syscall.S_IFDIR, Ino: 999}
child := n.NewInode(ctx, childNode, stable)
return child, syscall.F_OK
}
var _ NodeOpendirHandler = (*testDeletedIno)(nil)
func (n *testDeletedIno) OpendirHandle(ctx context.Context, flags uint32) (fh FileHandle, fuseFlags uint32, errno syscall.Errno) {
return &struct{}{}, 0, OK
}
func (n *testDeletedIno) Getattr(ctx context.Context, f FileHandle, out *fuse.AttrOut) syscall.Errno {
prefix := ".go-fuse"
p := n.Path(n.Root())
if strings.HasPrefix(p, prefix) {
// Return ENFILE when things look ok
return syscall.ENFILE
}
// Otherwise EILSEQ
return syscall.EILSEQ
}
// TestIno1 tests that inode number 1 is allowed.
//
// We used to panic like this because inode number 1 was special:
//
// panic: using reserved ID 1 for inode number
func TestIno1(t *testing.T) {
rootNode := testIno1{}
mnt, _ := testMount(t, &rootNode, nil)
var st syscall.Stat_t
err := syscall.Stat(mnt+"/ino1", &st)
if err != nil {
t.Fatal(err)
}
if st.Ino != 1 {
t.Errorf("wrong inode number: want=1 have=%d", st.Ino)
}
}
type testIno1 struct {
Inode
}
func (fn *testIno1) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
if name != "ino1" {
return nil, syscall.ENOENT
}
stable := StableAttr{
Mode: syscall.S_IFREG,
Ino: 1,
}
child := fn.NewInode(ctx, &testIno1{}, stable)
return child, 0
}
type lookupCountRoot struct {
Inode
count uint32
}
func (r *lookupCountRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*Inode, syscall.Errno) {
if name == "TestNegativeLookupCache" {
atomic.AddUint32(&r.count, 1)
}
return nil, syscall.ENOENT
}
func TestNegativeLookupCache(t *testing.T) {
for count, negCache := range []bool{true, false} {
r := lookupCountRoot{}
opt := &Options{}
if negCache {
sec := time.Second
opt.NegativeTimeout = &sec
}
mnt, _ := testMount(t, &r, opt)
fn := mnt + "/TestNegativeLookupCache"
for i := 0; i < 2; i++ {
var st syscall.Stat_t
if err := syscall.Lstat(fn, &st); err != syscall.ENOENT {
t.Errorf("got %v, want ENOENT", err)
}
}
want := uint32(count + 1)
if got := atomic.LoadUint32(&r.count); got != want {
t.Errorf("negCache=%v: got count %d, want %d", negCache, got, want)
}
}
}