mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
282 lines
7.2 KiB
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)
|
|
}
|
|
}
|
|
}
|