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