aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/mockfs.hh
blob: 4cf6daea7e7685bcedb07bb1d0cafb8fb0acd9b5 (plain) (blame)
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
/*-
 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
 *
 * Copyright (c) 2019 The FreeBSD Foundation
 *
 * This software was developed by BFF Storage Systems, LLC under sponsorship
 * from the FreeBSD Foundation.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD$
 */

extern "C" {
#include <sys/types.h>

#include <pthread.h>

#include "fuse_kernel.h"
}

#include <gmock/gmock.h>

#define TIME_T_MAX (std::numeric_limits<time_t>::max())

/* 
 * A pseudo-fuse errno used indicate that a fuse operation should have no
 * response, at least not immediately
 */
#define FUSE_NORESPONSE 9999

#define SET_OUT_HEADER_LEN(out, variant) { \
	(out).header.len = (sizeof((out).header) + \
			    sizeof((out).body.variant)); \
}

/*
 * Create an expectation on FUSE_LOOKUP and return it so the caller can set
 * actions.
 *
 * This must be a macro instead of a method because EXPECT_CALL returns a type
 * with a deleted constructor.
 */
#define EXPECT_LOOKUP(parent, path)					\
	EXPECT_CALL(*m_mock, process(					\
		ResultOf([=](auto in) {					\
			return (in.header.opcode == FUSE_LOOKUP &&	\
				in.header.nodeid == (parent) &&	\
				strcmp(in.body.lookup, (path)) == 0);	\
		}, Eq(true)),						\
		_)							\
	)

extern int verbosity;

/* This struct isn't defined by fuse_kernel.h or libfuse, but it should be */
struct fuse_create_out {
	struct fuse_entry_out	entry;
	struct fuse_open_out	open;
};

/* Protocol 7.8 version of struct fuse_attr */
struct fuse_attr_7_8
{
	uint64_t	ino;
	uint64_t	size;
	uint64_t	blocks;
	uint64_t	atime;
	uint64_t	mtime;
	uint64_t	ctime;
	uint32_t	atimensec;
	uint32_t	mtimensec;
	uint32_t	ctimensec;
	uint32_t	mode;
	uint32_t	nlink;
	uint32_t	uid;
	uint32_t	gid;
	uint32_t	rdev;
};

/* Protocol 7.8 version of struct fuse_attr_out */
struct fuse_attr_out_7_8
{
	uint64_t	attr_valid;
	uint32_t	attr_valid_nsec;
	uint32_t	dummy;
	struct fuse_attr_7_8 attr;
};

/* Protocol 7.8 version of struct fuse_entry_out */
struct fuse_entry_out_7_8 {
	uint64_t	nodeid;		/* Inode ID */
	uint64_t	generation;	/* Inode generation: nodeid:gen must
				   be unique for the fs's lifetime */
	uint64_t	entry_valid;	/* Cache timeout for the name */
	uint64_t	attr_valid;	/* Cache timeout for the attributes */
	uint32_t	entry_valid_nsec;
	uint32_t	attr_valid_nsec;
	struct fuse_attr_7_8 attr;
};

/* Output struct for FUSE_CREATE for protocol 7.8 servers */
struct fuse_create_out_7_8 {
	struct fuse_entry_out_7_8	entry;
	struct fuse_open_out	open;
};

/* Output struct for FUSE_INIT for protocol 7.22 and earlier servers */
struct fuse_init_out_7_22 {
	uint32_t	major;
	uint32_t	minor;
	uint32_t	max_readahead;
	uint32_t	flags;
	uint16_t	max_background;
	uint16_t	congestion_threshold;
	uint32_t	max_write;
};

union fuse_payloads_in {
	fuse_access_in	access;
	fuse_bmap_in	bmap;
	/* value is from fuse_kern_chan.c in fusefs-libs */
	uint8_t		bytes[0x21000 - sizeof(struct fuse_in_header)];
	fuse_create_in	create;
	fuse_flush_in	flush;
	fuse_fsync_in	fsync;
	fuse_fsync_in	fsyncdir;
	fuse_forget_in	forget;
	fuse_getattr_in	getattr;
	fuse_interrupt_in interrupt;
	fuse_lk_in	getlk;
	fuse_getxattr_in getxattr;
	fuse_init_in	init;
	fuse_link_in	link;
	fuse_listxattr_in listxattr;
	char		lookup[0];
	fuse_mkdir_in	mkdir;
	fuse_mknod_in	mknod;
	fuse_open_in	open;
	fuse_open_in	opendir;
	fuse_read_in	read;
	fuse_read_in	readdir;
	fuse_release_in	release;
	fuse_release_in	releasedir;
	fuse_rename_in	rename;
	char		rmdir[0];
	fuse_setattr_in	setattr;
	fuse_setxattr_in setxattr;
	fuse_lk_in	setlk;
	fuse_lk_in	setlkw;
	char		unlink[0];
	fuse_write_in	write;
};

struct mockfs_buf_in {
	fuse_in_header		header;
	union fuse_payloads_in	body;
};

union fuse_payloads_out {
	fuse_attr_out		attr;
	fuse_attr_out_7_8	attr_7_8;
	fuse_bmap_out		bmap;
	fuse_create_out		create;
	fuse_create_out_7_8	create_7_8;
	/*
	 * The protocol places no limits on the size of bytes.  Choose
	 * a size big enough for anything we'll test.
	 */
	uint8_t			bytes[0x20000];
	fuse_entry_out		entry;
	fuse_entry_out_7_8	entry_7_8;
	fuse_lk_out		getlk;
	fuse_getxattr_out	getxattr;
	fuse_init_out		init;
	fuse_init_out_7_22	init_7_22;
	/* The inval_entry structure should be followed by the entry's name */
	fuse_notify_inval_entry_out	inval_entry;
	fuse_notify_inval_inode_out	inval_inode;
	/* The store structure should be followed by the data to store */
	fuse_notify_store_out		store;
	fuse_listxattr_out	listxattr;
	fuse_open_out		open;
	fuse_statfs_out		statfs;
	/*
	 * The protocol places no limits on the length of the string.  This is
	 * merely convenient for testing.
	 */
	char			str[80];
	fuse_write_out		write;
};

struct mockfs_buf_out {
	fuse_out_header		header;
	union fuse_payloads_out	body;

	/* Default constructor: zero everything */
	mockfs_buf_out() {
		memset(this, 0, sizeof(*this));
	}
};

/* A function that can be invoked in place of MockFS::process */
typedef std::function<void (const mockfs_buf_in& in,
			    std::vector<std::unique_ptr<mockfs_buf_out>> &out)>
ProcessMockerT;

/*
 * Helper function used for setting an error expectation for any fuse operation.
 * The operation will return the supplied error
 */
ProcessMockerT ReturnErrno(int error);

/* Helper function used for returning negative cache entries for LOOKUP */
ProcessMockerT ReturnNegativeCache(const struct timespec *entry_valid);

/* Helper function used for returning a single immediate response */
ProcessMockerT ReturnImmediate(
	std::function<void(const mockfs_buf_in& in,
			   struct mockfs_buf_out &out)> f);

/* How the daemon should check /dev/fuse for readiness */
enum poll_method {
	BLOCKING,
	SELECT,
	POLL,
	KQ
};

/*
 * Fake FUSE filesystem
 *
 * "Mounts" a filesystem to a temporary directory and services requests
 * according to the programmed expectations.
 *
 * Operates directly on the fusefs(4) kernel API, not the libfuse(3) user api.
 */
class MockFS {
	/*
	 * thread id of the fuse daemon thread
	 *
	 * It must run in a separate thread so it doesn't deadlock with the
	 * client test code.
	 */
	pthread_t m_daemon_id;

	/* file descriptor of /dev/fuse control device */
	int m_fuse_fd;
	
	/* The minor version of the kernel API that this mock daemon targets */
	uint32_t m_kernel_minor_version;

	int m_kq;

	/* The max_readahead file system option */
	uint32_t m_maxreadahead;

	/* pid of the test process */
	pid_t m_pid;

	/* Method the daemon should use for I/O to and from /dev/fuse */
	enum poll_method m_pm;

	/* Timestamp granularity in nanoseconds */
	unsigned m_time_gran;

	void audit_request(const mockfs_buf_in &in, ssize_t buflen);
	void debug_request(const mockfs_buf_in&, ssize_t buflen);
	void debug_response(const mockfs_buf_out&);

	/* Initialize a session after mounting */
	void init(uint32_t flags);

	/* Is pid from a process that might be involved in the test? */
	bool pid_ok(pid_t pid);

	/* Default request handler */
	void process_default(const mockfs_buf_in&,
		std::vector<std::unique_ptr<mockfs_buf_out>>&);

	/* Entry point for the daemon thread */
	static void* service(void*);

	/*
	 * Read, but do not process, a single request from the kernel
	 *
	 * @param in	Return storage for the FUSE request
	 * @param res	Return value of read(2).  If positive, the amount of
	 *		data read from the fuse device.
	 */
	void read_request(mockfs_buf_in& in, ssize_t& res);

	/* Write a single response back to the kernel */
	void write_response(const mockfs_buf_out &out);

	public:
	/* pid of child process, for two-process test cases */
	pid_t m_child_pid;

	/* Maximum size of a FUSE_WRITE write */
	uint32_t m_maxwrite;

	/* 
	 * Number of events that were available from /dev/fuse after the last
	 * kevent call.  Only valid when m_pm = KQ.
	 */
	int m_nready;

	/* Tell the daemon to shut down ASAP */
	bool m_quit;

	/* Create a new mockfs and mount it to a tempdir */
	MockFS(int max_readahead, bool allow_other,
		bool default_permissions, bool push_symlinks_in, bool ro,
		enum poll_method pm, uint32_t flags,
		uint32_t kernel_minor_version, uint32_t max_write, bool async,
		bool no_clusterr, unsigned time_gran, bool nointr);

	virtual ~MockFS();

	/* Kill the filesystem daemon without unmounting the filesystem */
	void kill_daemon();

	/* Process FUSE requests endlessly */
	void loop();

	/*
	 * Send an asynchronous notification to invalidate a directory entry.
	 * Similar to libfuse's fuse_lowlevel_notify_inval_entry
	 *
	 * This method will block until the client has responded, so it should
	 * generally be run in a separate thread from request processing.
	 *
	 * @param	parent	Parent directory's inode number
	 * @param	name	name of dirent to invalidate
	 * @param	namelen	size of name, including the NUL
	 */
	int notify_inval_entry(ino_t parent, const char *name, size_t namelen);

	/*
	 * Send an asynchronous notification to invalidate an inode's cached
	 * data and/or attributes.  Similar to libfuse's
	 * fuse_lowlevel_notify_inval_inode.
	 *
	 * This method will block until the client has responded, so it should
	 * generally be run in a separate thread from request processing.
	 *
	 * @param	ino	File's inode number
	 * @param	off	offset at which to begin invalidation.  A
	 * 			negative offset means to invalidate attributes
	 * 			only.
	 * @param	len	Size of region of data to invalidate.  0 means
	 * 			to invalidate all cached data.
	 */
	int notify_inval_inode(ino_t ino, off_t off, ssize_t len);

	/*
	 * Send an asynchronous notification to store data directly into an
	 * inode's cache.  Similar to libfuse's fuse_lowlevel_notify_store.
	 *
	 * This method will block until the client has responded, so it should
	 * generally be run in a separate thread from request processing.
	 *
	 * @param	ino	File's inode number
	 * @param	off	Offset at which to store data
	 * @param	data	Pointer to the data to cache
	 * @param	len	Size of data
	 */
	int notify_store(ino_t ino, off_t off, const void* data, ssize_t size);

	/* 
	 * Request handler
	 *
	 * This method is expected to provide the responses to each FUSE
	 * operation.  For an immediate response, push one buffer into out.
	 * For a delayed response, push nothing.  For an immediate response
	 * plus a delayed response to an earlier operation, push two bufs.
	 * Test cases must define each response using Googlemock expectations
	 */
	MOCK_METHOD2(process, void(const mockfs_buf_in&,
				std::vector<std::unique_ptr<mockfs_buf_out>>&));

	/* Gracefully unmount */
	void unmount();
};