1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 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 56 // EINVAL, dst is subdir of src 57 TEST_F(Rename, einval) 58 { 59 const char FULLDST[] = "mountpoint/src/dst"; 60 const char RELDST[] = "dst"; 61 const char FULLSRC[] = "mountpoint/src"; 62 const char RELSRC[] = "src"; 63 uint64_t src_ino = 42; 64 65 expect_lookup(RELSRC, src_ino, S_IFDIR | 0755, 0, 2); 66 EXPECT_LOOKUP(src_ino, RELDST).WillOnce(Invoke(ReturnErrno(ENOENT))); 67 68 ASSERT_NE(0, rename(FULLSRC, FULLDST)); 69 ASSERT_EQ(EINVAL, errno); 70 } 71 72 // source does not exist 73 TEST_F(Rename, enoent) 74 { 75 const char FULLDST[] = "mountpoint/dst"; 76 const char FULLSRC[] = "mountpoint/src"; 77 const char RELSRC[] = "src"; 78 // FUSE hardcodes the mountpoint to inode 1 79 80 EXPECT_LOOKUP(FUSE_ROOT_ID, RELSRC) 81 .WillOnce(Invoke(ReturnErrno(ENOENT))); 82 83 ASSERT_NE(0, rename(FULLSRC, FULLDST)); 84 ASSERT_EQ(ENOENT, errno); 85 } 86 87 /* 88 * Renaming a file after FUSE_LOOKUP returned a negative cache entry for dst 89 */ 90 TEST_F(Rename, entry_cache_negative) 91 { 92 const char FULLDST[] = "mountpoint/dst"; 93 const char RELDST[] = "dst"; 94 const char FULLSRC[] = "mountpoint/src"; 95 const char RELSRC[] = "src"; 96 uint64_t dst_dir_ino = FUSE_ROOT_ID; 97 uint64_t ino = 42; 98 /* 99 * Set entry_valid = 0 because this test isn't concerned with whether 100 * or not we actually cache negative entries, only with whether we 101 * interpret negative cache responses correctly. 102 */ 103 struct timespec entry_valid = {.tv_sec = 0, .tv_nsec = 0}; 104 105 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); 106 /* LOOKUP returns a negative cache entry for dst */ 107 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 108 .WillOnce(ReturnNegativeCache(&entry_valid)); 109 110 EXPECT_CALL(*m_mock, process( 111 ResultOf([=](auto in) { 112 const char *src = (const char*)in.body.bytes + 113 sizeof(fuse_rename_in); 114 const char *dst = src + strlen(src) + 1; 115 return (in.header.opcode == FUSE_RENAME && 116 in.body.rename.newdir == dst_dir_ino && 117 (0 == strcmp(RELDST, dst)) && 118 (0 == strcmp(RELSRC, src))); 119 }, Eq(true)), 120 _) 121 ).WillOnce(Invoke(ReturnErrno(0))); 122 123 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 124 } 125 126 /* 127 * Renaming a file should purge any negative namecache entries for the dst 128 */ 129 TEST_F(Rename, entry_cache_negative_purge) 130 { 131 const char FULLDST[] = "mountpoint/dst"; 132 const char RELDST[] = "dst"; 133 const char FULLSRC[] = "mountpoint/src"; 134 const char RELSRC[] = "src"; 135 uint64_t dst_dir_ino = FUSE_ROOT_ID; 136 uint64_t ino = 42; 137 struct timespec entry_valid = {.tv_sec = TIME_T_MAX, .tv_nsec = 0}; 138 139 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); 140 /* LOOKUP returns a negative cache entry for dst */ 141 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 142 .WillOnce(ReturnNegativeCache(&entry_valid)) 143 .RetiresOnSaturation(); 144 145 EXPECT_CALL(*m_mock, process( 146 ResultOf([=](auto in) { 147 const char *src = (const char*)in.body.bytes + 148 sizeof(fuse_rename_in); 149 const char *dst = src + strlen(src) + 1; 150 return (in.header.opcode == FUSE_RENAME && 151 in.body.rename.newdir == dst_dir_ino && 152 (0 == strcmp(RELDST, dst)) && 153 (0 == strcmp(RELSRC, src))); 154 }, Eq(true)), 155 _) 156 ).WillOnce(Invoke(ReturnErrno(0))); 157 158 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 159 160 /* Finally, a subsequent lookup should query the daemon */ 161 expect_lookup(RELDST, ino, S_IFREG | 0644, 0, 1); 162 163 ASSERT_EQ(0, access(FULLDST, F_OK)) << strerror(errno); 164 } 165 166 TEST_F(Rename, exdev) 167 { 168 const char FULLB[] = "mountpoint/src"; 169 const char RELB[] = "src"; 170 // FUSE hardcodes the mountpoint to inode 1 171 uint64_t b_ino = 42; 172 173 tmpfd = mkstemp(tmpfile); 174 ASSERT_LE(0, tmpfd) << strerror(errno); 175 176 expect_lookup(RELB, b_ino, S_IFREG | 0644, 0, 2); 177 178 ASSERT_NE(0, rename(tmpfile, FULLB)); 179 ASSERT_EQ(EXDEV, errno); 180 181 ASSERT_NE(0, rename(FULLB, tmpfile)); 182 ASSERT_EQ(EXDEV, errno); 183 } 184 185 TEST_F(Rename, ok) 186 { 187 const char FULLDST[] = "mountpoint/dst"; 188 const char RELDST[] = "dst"; 189 const char FULLSRC[] = "mountpoint/src"; 190 const char RELSRC[] = "src"; 191 uint64_t dst_dir_ino = FUSE_ROOT_ID; 192 uint64_t ino = 42; 193 194 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); 195 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDST) 196 .WillOnce(Invoke(ReturnErrno(ENOENT))); 197 198 EXPECT_CALL(*m_mock, process( 199 ResultOf([=](auto in) { 200 const char *src = (const char*)in.body.bytes + 201 sizeof(fuse_rename_in); 202 const char *dst = src + strlen(src) + 1; 203 return (in.header.opcode == FUSE_RENAME && 204 in.body.rename.newdir == dst_dir_ino && 205 (0 == strcmp(RELDST, dst)) && 206 (0 == strcmp(RELSRC, src))); 207 }, Eq(true)), 208 _) 209 ).WillOnce(Invoke(ReturnErrno(0))); 210 211 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 212 } 213 214 /* When moving a file to a new directory, update its parent */ 215 TEST_F(Rename, parent) 216 { 217 const char FULLDST[] = "mountpoint/dstdir/dst"; 218 const char RELDSTDIR[] = "dstdir"; 219 const char RELDST[] = "dst"; 220 const char FULLSRC[] = "mountpoint/src"; 221 const char RELSRC[] = "src"; 222 const char FULLDSTPARENT[] = "mountpoint/dstdir"; 223 const char FULLDSTDOTDOT[] = "mountpoint/dstdir/dst/.."; 224 Sequence seq; 225 uint64_t dst_dir_ino = 43; 226 uint64_t ino = 42; 227 struct stat sb; 228 229 expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1); 230 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) 231 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 232 SET_OUT_HEADER_LEN(out, entry); 233 out.body.entry.nodeid = dst_dir_ino; 234 out.body.entry.entry_valid = UINT64_MAX; 235 out.body.entry.attr_valid = UINT64_MAX; 236 out.body.entry.attr.mode = S_IFDIR | 0755; 237 out.body.entry.attr.ino = dst_dir_ino; 238 out.body.entry.attr.nlink = 2; 239 }))); 240 EXPECT_LOOKUP(dst_dir_ino, RELDST) 241 .InSequence(seq) 242 .WillOnce(Invoke(ReturnErrno(ENOENT))); 243 EXPECT_CALL(*m_mock, process( 244 ResultOf([=](auto in) { 245 const char *src = (const char*)in.body.bytes + 246 sizeof(fuse_rename_in); 247 const char *dst = src + strlen(src) + 1; 248 return (in.header.opcode == FUSE_RENAME && 249 in.body.rename.newdir == dst_dir_ino && 250 (0 == strcmp(RELDST, dst)) && 251 (0 == strcmp(RELSRC, src))); 252 }, Eq(true)), 253 _) 254 ).WillOnce(Invoke(ReturnErrno(0))); 255 EXPECT_CALL(*m_mock, process( 256 ResultOf([](auto in) { 257 return (in.header.opcode == FUSE_GETATTR && 258 in.header.nodeid == 1); 259 }, Eq(true)), 260 _) 261 ).InSequence(seq) 262 .WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 263 SET_OUT_HEADER_LEN(out, attr); 264 out.body.attr.attr_valid = UINT64_MAX; 265 out.body.attr.attr.ino = 1; 266 out.body.attr.attr.mode = S_IFDIR | 0755; 267 out.body.attr.attr.nlink = 2; 268 }))); 269 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) 270 .InSequence(seq) 271 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 272 SET_OUT_HEADER_LEN(out, entry); 273 out.body.entry.nodeid = dst_dir_ino; 274 out.body.entry.entry_valid = UINT64_MAX; 275 out.body.entry.attr_valid = UINT64_MAX; 276 out.body.entry.attr.mode = S_IFDIR | 0755; 277 out.body.entry.attr.ino = dst_dir_ino; 278 out.body.entry.attr.nlink = 3; 279 }))); 280 EXPECT_LOOKUP(dst_dir_ino, RELDST) 281 .InSequence(seq) 282 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 283 SET_OUT_HEADER_LEN(out, entry); 284 out.body.entry.attr.mode = S_IFDIR | 0755; 285 out.body.entry.nodeid = ino; 286 out.body.entry.entry_valid = UINT64_MAX; 287 out.body.entry.attr_valid = UINT64_MAX; 288 }))); 289 290 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 291 292 ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno); 293 EXPECT_EQ(2ul, sb.st_nlink); 294 295 ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno); 296 EXPECT_EQ(3ul, sb.st_nlink); 297 298 ASSERT_EQ(0, stat(FULLDSTDOTDOT, &sb)) << strerror(errno); 299 ASSERT_EQ(dst_dir_ino, sb.st_ino); 300 } 301 302 // Rename overwrites an existing destination file 303 TEST_F(Rename, overwrite) 304 { 305 const char FULLDST[] = "mountpoint/dst"; 306 const char RELDST[] = "dst"; 307 const char FULLSRC[] = "mountpoint/src"; 308 const char RELSRC[] = "src"; 309 // The inode of the already-existing destination file 310 uint64_t dst_ino = 2; 311 uint64_t dst_dir_ino = FUSE_ROOT_ID; 312 uint64_t ino = 42; 313 314 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); 315 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1); 316 EXPECT_CALL(*m_mock, process( 317 ResultOf([=](auto in) { 318 const char *src = (const char*)in.body.bytes + 319 sizeof(fuse_rename_in); 320 const char *dst = src + strlen(src) + 1; 321 return (in.header.opcode == FUSE_RENAME && 322 in.body.rename.newdir == dst_dir_ino && 323 (0 == strcmp(RELDST, dst)) && 324 (0 == strcmp(RELSRC, src))); 325 }, Eq(true)), 326 _) 327 ).WillOnce(Invoke(ReturnErrno(0))); 328 329 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 330 } 331