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