xref: /freebsd/tests/sys/fs/fusefs/lseek.cc (revision cfd6422a5217410fbd66f7a7a8a64d9d85e61229)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3  *
4  * Copyright (c) 2020 Alan Somers
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  *
27  * $FreeBSD$
28  */
29 
30 extern "C" {
31 #include <sys/param.h>
32 
33 #include <fcntl.h>
34 }
35 
36 #include "mockfs.hh"
37 #include "utils.hh"
38 
39 using namespace testing;
40 
41 class Lseek: public FuseTest {};
42 class LseekPathconf: public Lseek {};
43 class LseekPathconf_7_23: public LseekPathconf {
44 public:
45 virtual void SetUp() {
46 	m_kernel_minor_version = 23;
47 	FuseTest::SetUp();
48 }
49 };
50 class LseekSeekHole: public Lseek {};
51 class LseekSeekData: public Lseek {};
52 
53 /*
54  * If a previous lseek operation has already returned enosys, then pathconf can
55  * return EINVAL immediately.
56  */
57 TEST_F(LseekPathconf, already_enosys)
58 {
59 	const char FULLPATH[] = "mountpoint/some_file.txt";
60 	const char RELPATH[] = "some_file.txt";
61 	const uint64_t ino = 42;
62 	off_t fsize = 1 << 30;	/* 1 GiB */
63 	off_t offset_in = 1 << 28;
64 	int fd;
65 
66 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
67 	expect_open(ino, 0, 1);
68 	EXPECT_CALL(*m_mock, process(
69 		ResultOf([=](auto in) {
70 			return (in.header.opcode == FUSE_LSEEK);
71 		}, Eq(true)),
72 		_)
73 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
74 
75 	fd = open(FULLPATH, O_RDONLY);
76 
77 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
78 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
79 	EXPECT_EQ(EINVAL, errno);
80 }
81 
82 /*
83  * If a previous lseek operation has already returned successfully, then
84  * pathconf can return 1 immediately.  1 means "holes are reported, but size is
85  * not specified".
86  */
87 TEST_F(LseekPathconf, already_seeked)
88 {
89 	const char FULLPATH[] = "mountpoint/some_file.txt";
90 	const char RELPATH[] = "some_file.txt";
91 	const uint64_t ino = 42;
92 	off_t fsize = 1 << 30;	/* 1 GiB */
93 	off_t offset = 1 << 28;
94 	int fd;
95 
96 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
97 	expect_open(ino, 0, 1);
98 	EXPECT_CALL(*m_mock, process(
99 		ResultOf([=](auto in) {
100 			return (in.header.opcode == FUSE_LSEEK);
101 		}, Eq(true)),
102 		_)
103 	).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) {
104 		SET_OUT_HEADER_LEN(out, lseek);
105 		out.body.lseek.offset = i.body.lseek.offset;
106 	})));
107 	fd = open(FULLPATH, O_RDONLY);
108 	EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA));
109 
110 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
111 }
112 
113 /*
114  * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
115  * as a pathconf request comes in.
116  */
117 TEST_F(LseekPathconf, enosys_now)
118 {
119 	const char FULLPATH[] = "mountpoint/some_file.txt";
120 	const char RELPATH[] = "some_file.txt";
121 	const uint64_t ino = 42;
122 	off_t fsize = 1 << 30;	/* 1 GiB */
123 	int fd;
124 
125 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
126 	expect_open(ino, 0, 1);
127 	EXPECT_CALL(*m_mock, process(
128 		ResultOf([=](auto in) {
129 			return (in.header.opcode == FUSE_LSEEK);
130 		}, Eq(true)),
131 		_)
132 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
133 
134 	fd = open(FULLPATH, O_RDONLY);
135 
136 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
137 	EXPECT_EQ(EINVAL, errno);
138 }
139 
140 /*
141  * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
142  * as a pathconf request comes in.  This is the typical pattern of bsdtar.  It
143  * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
144  */
145 TEST_F(LseekPathconf, seek_now)
146 {
147 	const char FULLPATH[] = "mountpoint/some_file.txt";
148 	const char RELPATH[] = "some_file.txt";
149 	const uint64_t ino = 42;
150 	off_t fsize = 1 << 30;	/* 1 GiB */
151 	off_t offset_initial = 1 << 27;
152 	off_t offset_out = 1 << 29;
153 	int fd;
154 
155 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
156 	expect_open(ino, 0, 1);
157 	EXPECT_CALL(*m_mock, process(
158 		ResultOf([=](auto in) {
159 			return (in.header.opcode == FUSE_LSEEK);
160 		}, Eq(true)),
161 		_)
162 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
163 		SET_OUT_HEADER_LEN(out, lseek);
164 		out.body.lseek.offset = offset_out;
165 	})));
166 
167 	fd = open(FULLPATH, O_RDONLY);
168 	EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
169 	EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
170 	/* And check that the file pointer hasn't changed */
171 	EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
172 }
173 
174 /*
175  * For servers using older protocol versions, no FUSE_LSEEK should be attempted
176  */
177 TEST_F(LseekPathconf_7_23, already_enosys)
178 {
179 	const char FULLPATH[] = "mountpoint/some_file.txt";
180 	const char RELPATH[] = "some_file.txt";
181 	const uint64_t ino = 42;
182 	off_t fsize = 1 << 30;	/* 1 GiB */
183 	int fd;
184 
185 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
186 	expect_open(ino, 0, 1);
187 	EXPECT_CALL(*m_mock, process(
188 		ResultOf([=](auto in) {
189 			return (in.header.opcode == FUSE_LSEEK);
190 		}, Eq(true)),
191 		_)
192 	).Times(0);
193 
194 	fd = open(FULLPATH, O_RDONLY);
195 	EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
196 	EXPECT_EQ(EINVAL, errno);
197 }
198 
199 TEST_F(LseekSeekData, ok)
200 {
201 	const char FULLPATH[] = "mountpoint/some_file.txt";
202 	const char RELPATH[] = "some_file.txt";
203 	const uint64_t ino = 42;
204 	off_t fsize = 1 << 30;	/* 1 GiB */
205 	off_t offset_in = 1 << 28;
206 	off_t offset_out = 1 << 29;
207 	int fd;
208 
209 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
210 	expect_open(ino, 0, 1);
211 	EXPECT_CALL(*m_mock, process(
212 		ResultOf([=](auto in) {
213 			return (in.header.opcode == FUSE_LSEEK &&
214 				in.header.nodeid == ino &&
215 				in.body.lseek.fh == FH &&
216 				(off_t)in.body.lseek.offset == offset_in &&
217 				in.body.lseek.whence == SEEK_DATA);
218 		}, Eq(true)),
219 		_)
220 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
221 		SET_OUT_HEADER_LEN(out, lseek);
222 		out.body.lseek.offset = offset_out;
223 	})));
224 	fd = open(FULLPATH, O_RDONLY);
225 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
226 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
227 }
228 
229 /*
230  * If the server returns ENOSYS, fusefs should fall back to the default
231  * behavior, and never query the server again.
232  */
233 TEST_F(LseekSeekData, enosys)
234 {
235 	const char FULLPATH[] = "mountpoint/some_file.txt";
236 	const char RELPATH[] = "some_file.txt";
237 	const uint64_t ino = 42;
238 	off_t fsize = 1 << 30;	/* 1 GiB */
239 	off_t offset_in = 1 << 28;
240 	int fd;
241 
242 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
243 	expect_open(ino, 0, 1);
244 	EXPECT_CALL(*m_mock, process(
245 		ResultOf([=](auto in) {
246 			return (in.header.opcode == FUSE_LSEEK &&
247 				in.header.nodeid == ino &&
248 				in.body.lseek.fh == FH &&
249 				(off_t)in.body.lseek.offset == offset_in &&
250 				in.body.lseek.whence == SEEK_DATA);
251 		}, Eq(true)),
252 		_)
253 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
254 	fd = open(FULLPATH, O_RDONLY);
255 
256 	/*
257 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
258 	 * otherwise.
259 	 */
260 	EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
261 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
262 	EXPECT_EQ(ENXIO, errno);
263 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
264 	EXPECT_EQ(ENXIO, errno);
265 }
266 
267 TEST_F(LseekSeekHole, ok)
268 {
269 	const char FULLPATH[] = "mountpoint/some_file.txt";
270 	const char RELPATH[] = "some_file.txt";
271 	const uint64_t ino = 42;
272 	off_t fsize = 1 << 30;	/* 1 GiB */
273 	off_t offset_in = 1 << 28;
274 	off_t offset_out = 1 << 29;
275 	int fd;
276 
277 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
278 	expect_open(ino, 0, 1);
279 	EXPECT_CALL(*m_mock, process(
280 		ResultOf([=](auto in) {
281 			return (in.header.opcode == FUSE_LSEEK &&
282 				in.header.nodeid == ino &&
283 				in.body.lseek.fh == FH &&
284 				(off_t)in.body.lseek.offset == offset_in &&
285 				in.body.lseek.whence == SEEK_HOLE);
286 		}, Eq(true)),
287 		_)
288 	).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
289 		SET_OUT_HEADER_LEN(out, lseek);
290 		out.body.lseek.offset = offset_out;
291 	})));
292 	fd = open(FULLPATH, O_RDONLY);
293 	EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
294 	EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
295 }
296 
297 /*
298  * If the server returns ENOSYS, fusefs should fall back to the default
299  * behavior, and never query the server again.
300  */
301 TEST_F(LseekSeekHole, enosys)
302 {
303 	const char FULLPATH[] = "mountpoint/some_file.txt";
304 	const char RELPATH[] = "some_file.txt";
305 	const uint64_t ino = 42;
306 	off_t fsize = 1 << 30;	/* 1 GiB */
307 	off_t offset_in = 1 << 28;
308 	int fd;
309 
310 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
311 	expect_open(ino, 0, 1);
312 	EXPECT_CALL(*m_mock, process(
313 		ResultOf([=](auto in) {
314 			return (in.header.opcode == FUSE_LSEEK &&
315 				in.header.nodeid == ino &&
316 				in.body.lseek.fh == FH &&
317 				(off_t)in.body.lseek.offset == offset_in &&
318 				in.body.lseek.whence == SEEK_HOLE);
319 		}, Eq(true)),
320 		_)
321 	).WillOnce(Invoke(ReturnErrno(ENOSYS)));
322 	fd = open(FULLPATH, O_RDONLY);
323 
324 	/*
325 	 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
326 	 * otherwise.
327 	 */
328 	EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
329 	EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
330 	EXPECT_EQ(ENXIO, errno);
331 	EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
332 	EXPECT_EQ(ENXIO, errno);
333 }
334 
335 /* lseek should return ENXIO when offset points to EOF */
336 TEST_F(LseekSeekHole, enxio)
337 {
338 	const char FULLPATH[] = "mountpoint/some_file.txt";
339 	const char RELPATH[] = "some_file.txt";
340 	const uint64_t ino = 42;
341 	off_t fsize = 1 << 30;	/* 1 GiB */
342 	off_t offset_in = fsize;
343 	int fd;
344 
345 	expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
346 	expect_open(ino, 0, 1);
347 	EXPECT_CALL(*m_mock, process(
348 		ResultOf([=](auto in) {
349 			return (in.header.opcode == FUSE_LSEEK &&
350 				in.header.nodeid == ino &&
351 				in.body.lseek.fh == FH &&
352 				(off_t)in.body.lseek.offset == offset_in &&
353 				in.body.lseek.whence == SEEK_HOLE);
354 		}, Eq(true)),
355 		_)
356 	).WillOnce(Invoke(ReturnErrno(ENXIO)));
357 	fd = open(FULLPATH, O_RDONLY);
358 	EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
359 	EXPECT_EQ(ENXIO, errno);
360 }
361