2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
4 * Copyright (c) 2019 The FreeBSD Foundation
6 * This software was developed by BFF Storage Systems, LLC under sponsorship
7 * from the FreeBSD Foundation.
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions
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.
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
39 using namespace testing;
41 class Rename: public FuseTest {
44 char tmpfile[80] = "/tmp/fuse.rename.XXXXXX";
46 virtual void TearDown() {
55 void expect_getattr(uint64_t ino, mode_t mode)
57 EXPECT_CALL(*m_mock, process(
58 ResultOf([=](auto in) {
59 return (in->header.opcode == FUSE_GETATTR &&
60 in->header.nodeid == ino);
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;
74 // EINVAL, dst is subdir of src
75 TEST_F(Rename, einval)
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;
83 expect_getattr(1, S_IFDIR | 0755);
84 expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2);
85 EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
87 ASSERT_NE(0, rename(FULLSRC, FULLDST));
88 ASSERT_EQ(EINVAL, errno);
91 // source does not exist
92 TEST_F(Rename, enoent)
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
99 EXPECT_LOOKUP(1, RELSRC).WillOnce(Invoke(ReturnErrno(ENOENT)));
101 ASSERT_NE(0, rename(FULLSRC, FULLDST));
102 ASSERT_EQ(ENOENT, errno);
106 * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst
108 TEST_F(Rename, entry_cache_negative)
110 const char FULLDST[] = "mountpoint/dst";
111 const char RELDST[] = "dst";
112 const char FULLSRC[] = "mountpoint/src";
113 const char RELSRC[] = "src";
114 // FUSE hardcodes the mountpoint to inode 1
115 uint64_t dst_dir_ino = 1;
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.
122 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0};
124 expect_getattr(1, 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(1, RELDST).WillOnce(ReturnNegativeCache(&entry_valid));
129 EXPECT_CALL(*m_mock, process(
130 ResultOf([=](auto in) {
131 const char *src = (const char*)in->body.bytes +
132 sizeof(fuse_rename_in);
133 const char *dst = src + strlen(src) + 1;
134 return (in->header.opcode == FUSE_RENAME &&
135 in->body.rename.newdir == dst_dir_ino &&
136 (0 == strcmp(RELDST, dst)) &&
137 (0 == strcmp(RELSRC, src)));
140 ).WillOnce(Invoke(ReturnErrno(0)));
142 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
146 * Renaming a file should purge any negative namecache entries for the dst
148 TEST_F(Rename, entry_cache_negative_purge)
150 const char FULLDST[] = "mountpoint/dst";
151 const char RELDST[] = "dst";
152 const char FULLSRC[] = "mountpoint/src";
153 const char RELSRC[] = "src";
154 // FUSE hardcodes the mountpoint to inode 1
155 uint64_t dst_dir_ino = 1;
157 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0};
159 expect_getattr(1, 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(1, RELDST).WillOnce(ReturnNegativeCache(&entry_valid))
163 .RetiresOnSaturation();
165 EXPECT_CALL(*m_mock, process(
166 ResultOf([=](auto in) {
167 const char *src = (const char*)in->body.bytes +
168 sizeof(fuse_rename_in);
169 const char *dst = src + strlen(src) + 1;
170 return (in->header.opcode == FUSE_RENAME &&
171 in->body.rename.newdir == dst_dir_ino &&
172 (0 == strcmp(RELDST, dst)) &&
173 (0 == strcmp(RELSRC, src)));
176 ).WillOnce(Invoke(ReturnErrno(0)));
178 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
180 /* Finally, a subsequent lookup should query the daemon */
181 expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1);
183 ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno);
186 TEST_F(Rename, exdev)
188 const char FULLB[] = "mountpoint/src";
189 const char RELB[] = "src";
190 // FUSE hardcodes the mountpoint to inode 1
193 tmpfd = mkstemp(tmpfile);
194 ASSERT_LE(0, tmpfd) << strerror(errno);
196 expect_getattr(1, S_IFDIR | 0755);
197 expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2);
199 ASSERT_NE(0, rename(tmpfile, FULLB));
200 ASSERT_EQ(EXDEV, errno);
202 ASSERT_NE(0, rename(FULLB, tmpfile));
203 ASSERT_EQ(EXDEV, errno);
208 const char FULLDST[] = "mountpoint/dst";
209 const char RELDST[] = "dst";
210 const char FULLSRC[] = "mountpoint/src";
211 const char RELSRC[] = "src";
212 // FUSE hardcodes the mountpoint to inode 1
213 uint64_t dst_dir_ino = 1;
216 expect_getattr(1, S_IFDIR | 0755);
217 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1);
218 EXPECT_LOOKUP(1, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT)));
220 EXPECT_CALL(*m_mock, process(
221 ResultOf([=](auto in) {
222 const char *src = (const char*)in->body.bytes +
223 sizeof(fuse_rename_in);
224 const char *dst = src + strlen(src) + 1;
225 return (in->header.opcode == FUSE_RENAME &&
226 in->body.rename.newdir == dst_dir_ino &&
227 (0 == strcmp(RELDST, dst)) &&
228 (0 == strcmp(RELSRC, src)));
231 ).WillOnce(Invoke(ReturnErrno(0)));
233 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
236 /* When moving a file to a new directory, update its parent */
237 TEST_F(Rename, parent)
239 const char FULLDST[] = "mountpoint/dstdir/dst";
240 const char RELDSTDIR[] = "dstdir";
241 const char RELDST[] = "dst";
242 const char FULLSRC[] = "mountpoint/src";
243 const char RELSRC[] = "src";
244 const char FULLDSTPARENT[] = "mountpoint/dstdir/dst/..";
246 uint64_t dst_dir_ino = 43;
250 expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1);
251 expect_getattr(1, S_IFDIR | 0755);
252 EXPECT_LOOKUP(1, RELDSTDIR)
253 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
254 SET_OUT_HEADER_LEN(out, entry);
255 out->body.entry.nodeid = dst_dir_ino;
256 out->body.entry.entry_valid = UINT64_MAX;
257 out->body.entry.attr_valid = UINT64_MAX;
258 out->body.entry.attr.mode = S_IFDIR | 0755;
259 out->body.entry.attr.ino = dst_dir_ino;
261 EXPECT_LOOKUP(dst_dir_ino, RELDST)
263 .WillOnce(Invoke(ReturnErrno(ENOENT)));
264 EXPECT_CALL(*m_mock, process(
265 ResultOf([=](auto in) {
266 const char *src = (const char*)in->body.bytes +
267 sizeof(fuse_rename_in);
268 const char *dst = src + strlen(src) + 1;
269 return (in->header.opcode == FUSE_RENAME &&
270 in->body.rename.newdir == dst_dir_ino &&
271 (0 == strcmp(RELDST, dst)) &&
272 (0 == strcmp(RELSRC, src)));
275 ).WillOnce(Invoke(ReturnErrno(0)));
276 EXPECT_LOOKUP(dst_dir_ino, RELDST)
278 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto out) {
279 SET_OUT_HEADER_LEN(out, entry);
280 out->body.entry.attr.mode = S_IFDIR | 0755;
281 out->body.entry.nodeid = ino;
282 out->body.entry.entry_valid = UINT64_MAX;
283 out->body.entry.attr_valid = UINT64_MAX;
286 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);
287 ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno);
288 ASSERT_EQ(dst_dir_ino, sb.st_ino);
291 // Rename overwrites an existing destination file
292 TEST_F(Rename, overwrite)
294 const char FULLDST[] = "mountpoint/dst";
295 const char RELDST[] = "dst";
296 const char FULLSRC[] = "mountpoint/src";
297 const char RELSRC[] = "src";
298 // The inode of the already-existing destination file
299 uint64_t dst_ino = 2;
300 // FUSE hardcodes the mountpoint to inode 1
301 uint64_t dst_dir_ino = 1;
304 expect_getattr(1, 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)));
318 ).WillOnce(Invoke(ReturnErrno(0)));
320 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno);