xref: /freebsd/tests/sys/fs/fusefs/rename.cc (revision 0bf48626aaa33768078f5872b922b1487b3a9296)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3  *
4  * Copyright (c) 2019 The FreeBSD Foundation
5  *
6  * This software was developed by BFF Storage Systems, LLC under sponsorship
7  * from the FreeBSD Foundation.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in the
16  *    documentation and/or other materials provided with the distribution.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28  * SUCH DAMAGE.
29  */
30 
31 extern "C" {
32 #include <stdlib.h>
33 #include <unistd.h>
34 }
35 
36 #include "mockfs.hh"
37 #include "utils.hh"
38 
39 using namespace testing;
40 
41 class Rename: public FuseTest {
42 	public:
43 	int tmpfd = -1;
44 	char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
45 
46 	virtual void TearDown() {
47 		if (tmpfd >= 0) {
48 			close(tmpfd);
49 			unlink(tmpfile);
50 		}
51 
52 		FuseTest::TearDown();
53 	}
54 
55 	void expect_getattr(uint64_t ino, mode_t mode)
56 	{
57 		EXPECT_CALL(*m_mock, process(
58 			ResultOf([=](auto in) {
59 				return (in.header.opcode == FUSE_GETATTR &&
60 					in.header.nodeid == ino);
61 			}, Eq(true)),
62 			_)
63 		).WillOnce(Invoke(
64 			ReturnImmediate([=](auto i __unused, auto& out) {
65 			SET_OUT_HEADER_LEN(out, attr);
66 			out.body.attr.attr.ino = ino;	// Must match nodeid
67 			out.body.attr.attr.mode = mode;
68 			out.body.attr.attr_valid = UINT64_MAX;
69 		})));
70 	}
71 
72 };
73 
74 // EINVAL, dst is subdir of src
75 TEST_F(Rename, einval)
76 {
77 	const char FULLDST[] = "mountpoint/src/dst";
78 	const char RELDST[] = "dst";
79 	const char FULLSRC[] = "mountpoint/src";
80 	const char RELSRC[] = "src";
81 	uint64_t src_ino = 42;
82 
83 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
84 	expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
85 	EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
86 
87 	ASSERT_NE(0, rename(FULLSRC, FULLDST));
88 	ASSERT_EQ(EINVAL, errno);
89 }
90 
91 // source does not exist
92 TEST_F(Rename, enoent)
93 {
94 	const char FULLDST[] = "mountpoint/dst";
95 	const char FULLSRC[] = "mountpoint/src";
96 	const char RELSRC[] = "src";
97 	// FUSE hardcodes the mountpoint to inode 1
98 
99 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC)
100 	.WillOnce(Invoke(ReturnErrno(ENOENT)));
101 
102 	ASSERT_NE(0, rename(FULLSRC, FULLDST));
103 	ASSERT_EQ(ENOENT, errno);
104 }
105 
106 /*
107  * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst
108  */
109 TEST_F(Rename, entry_cache_negative)
110 {
111 	const char FULLDST[] = "mountpoint/dst";
112 	const char RELDST[] = "dst";
113 	const char FULLSRC[] = "mountpoint/src";
114 	const char RELSRC[] = "src";
115 	uint64_t dst_dir_ino = FUSE_ROOT_ID;
116 	uint64_t ino = 42;
117 	/*
118 	 * Set entry_valid = 0 because this test isn't concerned with whether
119 	 * or not we actually cache negative entries, only with whether we
120 	 * interpret negative cache responses correctly.
121 	 */
122 	struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
123 
124 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
125 	expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
126 	/* LOOKUP returns a negative cache entry for dst */
127 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
128 	.WillOnce(ReturnNegativeCache(&entry_valid));
129 
130 	EXPECT_CALL(*m_mock, process(
131 		ResultOf([=](auto in) {
132 			const char *src = (const char*)in.body.bytes +
133 				sizeof(fuse_rename_in);
134 			const char *dst = src + strlen(src) + 1;
135 			return (in.header.opcode == FUSE_RENAME &&
136 				in.body.rename.newdir == dst_dir_ino &&
137 				(0 == strcmp(RELDST, dst)) &&
138 				(0 == strcmp(RELSRC, src)));
139 		}, Eq(true)),
140 		_)
141 	).WillOnce(Invoke(ReturnErrno(0)));
142 
143 	ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
144 }
145 
146 /*
147  * Renaming a file should purge any negative namecache entries for the dst
148  */
149 TEST_F(Rename, entry_cache_negative_purge)
150 {
151 	const char FULLDST[] = "mountpoint/dst";
152 	const char RELDST[] = "dst";
153 	const char FULLSRC[] = "mountpoint/src";
154 	const char RELSRC[] = "src";
155 	uint64_t dst_dir_ino = FUSE_ROOT_ID;
156 	uint64_t ino = 42;
157 	struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
158 
159 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
160 	expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
161 	/* LOOKUP returns a negative cache entry for dst */
162 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
163 	.WillOnce(ReturnNegativeCache(&entry_valid))
164 	.RetiresOnSaturation();
165 
166 	EXPECT_CALL(*m_mock, process(
167 		ResultOf([=](auto in) {
168 			const char *src = (const char*)in.body.bytes +
169 				sizeof(fuse_rename_in);
170 			const char *dst = src + strlen(src) + 1;
171 			return (in.header.opcode == FUSE_RENAME &&
172 				in.body.rename.newdir == dst_dir_ino &&
173 				(0 == strcmp(RELDST, dst)) &&
174 				(0 == strcmp(RELSRC, src)));
175 		}, Eq(true)),
176 		_)
177 	).WillOnce(Invoke(ReturnErrno(0)));
178 
179 	ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
180 
181 	/* Finally, a subsequent lookup should query the daemon */
182 	expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1);
183 
184 	ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
185 }
186 
187 TEST_F(Rename, exdev)
188 {
189 	const char FULLB[] = "mountpoint/src";
190 	const char RELB[] = "src";
191 	// FUSE hardcodes the mountpoint to inode 1
192 	uint64_t b_ino = 42;
193 
194 	tmpfd = mkstemp(tmpfile);
195 	ASSERT_LE(0, tmpfd) << strerror(errno);
196 
197 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
198 	expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
199 
200 	ASSERT_NE(0, rename(tmpfile, FULLB));
201 	ASSERT_EQ(EXDEV, errno);
202 
203 	ASSERT_NE(0, rename(FULLB, tmpfile));
204 	ASSERT_EQ(EXDEV, errno);
205 }
206 
207 TEST_F(Rename, ok)
208 {
209 	const char FULLDST[] = "mountpoint/dst";
210 	const char RELDST[] = "dst";
211 	const char FULLSRC[] = "mountpoint/src";
212 	const char RELSRC[] = "src";
213 	uint64_t dst_dir_ino = FUSE_ROOT_ID;
214 	uint64_t ino = 42;
215 
216 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
217 	expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
218 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST)
219 	.WillOnce(Invoke(ReturnErrno(ENOENT)));
220 
221 	EXPECT_CALL(*m_mock, process(
222 		ResultOf([=](auto in) {
223 			const char *src = (const char*)in.body.bytes +
224 				sizeof(fuse_rename_in);
225 			const char *dst = src + strlen(src) + 1;
226 			return (in.header.opcode == FUSE_RENAME &&
227 				in.body.rename.newdir == dst_dir_ino &&
228 				(0 == strcmp(RELDST, dst)) &&
229 				(0 == strcmp(RELSRC, src)));
230 		}, Eq(true)),
231 		_)
232 	).WillOnce(Invoke(ReturnErrno(0)));
233 
234 	ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
235 }
236 
237 /* When moving a file to a new directory, update its parent */
238 TEST_F(Rename, parent)
239 {
240 	const char FULLDST[] = "mountpoint/dstdir/dst";
241 	const char RELDSTDIR[] = "dstdir";
242 	const char RELDST[] = "dst";
243 	const char FULLSRC[] = "mountpoint/src";
244 	const char RELSRC[] = "src";
245 	const char FULLDSTPARENT[] = "mountpoint/dstdir/dst/..";
246 	Sequence seq;
247 	uint64_t dst_dir_ino = 43;
248 	uint64_t ino = 42;
249 	struct stat sb;
250 
251 	expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1);
252 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
253 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR)
254 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
255 		SET_OUT_HEADER_LEN(out, entry);
256 		out.body.entry.nodeid = dst_dir_ino;
257 		out.body.entry.entry_valid = UINT64_MAX;
258 		out.body.entry.attr_valid = UINT64_MAX;
259 		out.body.entry.attr.mode = S_IFDIR | 0755;
260 		out.body.entry.attr.ino = dst_dir_ino;
261 	})));
262 	EXPECT_LOOKUP(dst_dir_ino, RELDST)
263 	.InSequence(seq)
264 	.WillOnce(Invoke(ReturnErrno(ENOENT)));
265 	EXPECT_CALL(*m_mock, process(
266 		ResultOf([=](auto in) {
267 			const char *src = (const char*)in.body.bytes +
268 				sizeof(fuse_rename_in);
269 			const char *dst = src + strlen(src) + 1;
270 			return (in.header.opcode == FUSE_RENAME &&
271 				in.body.rename.newdir == dst_dir_ino &&
272 				(0 == strcmp(RELDST, dst)) &&
273 				(0 == strcmp(RELSRC, src)));
274 		}, Eq(true)),
275 		_)
276 	).WillOnce(Invoke(ReturnErrno(0)));
277 	EXPECT_LOOKUP(dst_dir_ino, RELDST)
278 	.InSequence(seq)
279 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
280 		SET_OUT_HEADER_LEN(out, entry);
281 		out.body.entry.attr.mode = S_IFDIR | 0755;
282 		out.body.entry.nodeid = ino;
283 		out.body.entry.entry_valid = UINT64_MAX;
284 		out.body.entry.attr_valid = UINT64_MAX;
285 	})));
286 
287 	ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
288 	ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno);
289 	ASSERT_EQ(dst_dir_ino, sb.st_ino);
290 }
291 
292 // Rename overwrites an existing destination file
293 TEST_F(Rename, overwrite)
294 {
295 	const char FULLDST[] = "mountpoint/dst";
296 	const char RELDST[] = "dst";
297 	const char FULLSRC[] = "mountpoint/src";
298 	const char RELSRC[] = "src";
299 	// The inode of the already-existing destination file
300 	uint64_t dst_ino = 2;
301 	uint64_t dst_dir_ino = FUSE_ROOT_ID;
302 	uint64_t ino = 42;
303 
304 	expect_getattr(FUSE_ROOT_ID, S_IFDIR | 0755);
305 	expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
306 	expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1);
307 	EXPECT_CALL(*m_mock, process(
308 		ResultOf([=](auto in) {
309 			const char *src = (const char*)in.body.bytes +
310 				sizeof(fuse_rename_in);
311 			const char *dst = src + strlen(src) + 1;
312 			return (in.header.opcode == FUSE_RENAME &&
313 				in.body.rename.newdir == dst_dir_ino &&
314 				(0 == strcmp(RELDST, dst)) &&
315 				(0 == strcmp(RELSRC, src)));
316 		}, Eq(true)),
317 		_)
318 	).WillOnce(Invoke(ReturnErrno(0)));
319 
320 	ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
321 }
322