File: bridge_test.go

package info (click to toggle)
golang-github-hanwen-go-fuse 2.1.0%2Bgit20220822.58a7e14-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,292 kB
  • sloc: cpp: 78; sh: 43; makefile: 16
file content (240 lines) | stat: -rw-r--r-- 6,270 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// 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"
	"unsafe"

	"github.com/hanwen/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})
	defer tc.Clean()

	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, _, clean := testMount(t, &rootNode, nil)
	defer clean()

	for i := 0; i < 100; i++ {
		fi, _ := os.Stat(mnt + "/file")
		syscall.Unlink(mnt + "/file")
		fi, _ = os.Stat(mnt + "/dir")
		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, _, clean := testMount(t, &rootNode, &Options{Logger: log.New(os.Stderr, "", 0)})
	defer clean()

	// 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
}

func (n *testDeletedIno) Opendir(ctx context.Context) syscall.Errno {
	return 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, _, clean := testMount(t, &rootNode, nil)
	defer clean()

	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
}