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