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 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/dst/.."; 225 Sequence seq; 226 uint64_t dst_dir_ino = 43; 227 uint64_t ino = 42; 228 struct stat sb; 229 230 expect_lookup(RELSRC, ino, S_IFDIR | 0755, 0, 1); 231 EXPECT_LOOKUP(FUSE_ROOT_ID, RELDSTDIR) 232 .WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 233 SET_OUT_HEADER_LEN(out, entry); 234 out.body.entry.nodeid = dst_dir_ino; 235 out.body.entry.entry_valid = UINT64_MAX; 236 out.body.entry.attr_valid = UINT64_MAX; 237 out.body.entry.attr.mode = S_IFDIR | 0755; 238 out.body.entry.attr.ino = dst_dir_ino; 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_LOOKUP(dst_dir_ino, RELDST) 256 .InSequence(seq) 257 .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { 258 SET_OUT_HEADER_LEN(out, entry); 259 out.body.entry.attr.mode = S_IFDIR | 0755; 260 out.body.entry.nodeid = ino; 261 out.body.entry.entry_valid = UINT64_MAX; 262 out.body.entry.attr_valid = UINT64_MAX; 263 }))); 264 265 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 266 ASSERT_EQ(0, stat(FULLDSTPARENT, &sb)) << strerror(errno); 267 ASSERT_EQ(dst_dir_ino, sb.st_ino); 268 } 269 270 // Rename overwrites an existing destination file 271 TEST_F(Rename, overwrite) 272 { 273 const char FULLDST[] = "mountpoint/dst"; 274 const char RELDST[] = "dst"; 275 const char FULLSRC[] = "mountpoint/src"; 276 const char RELSRC[] = "src"; 277 // The inode of the already-existing destination file 278 uint64_t dst_ino = 2; 279 uint64_t dst_dir_ino = FUSE_ROOT_ID; 280 uint64_t ino = 42; 281 282 expect_lookup(RELSRC, ino, S_IFREG | 0644, 0, 1); 283 expect_lookup(RELDST, dst_ino, S_IFREG | 0644, 0, 1); 284 EXPECT_CALL(*m_mock, process( 285 ResultOf([=](auto in) { 286 const char *src = (const char*)in.body.bytes + 287 sizeof(fuse_rename_in); 288 const char *dst = src + strlen(src) + 1; 289 return (in.header.opcode == FUSE_RENAME && 290 in.body.rename.newdir == dst_dir_ino && 291 (0 == strcmp(RELDST, dst)) && 292 (0 == strcmp(RELSRC, src))); 293 }, Eq(true)), 294 _) 295 ).WillOnce(Invoke(ReturnErrno(0))); 296 297 ASSERT_EQ(0, rename(FULLSRC, FULLDST)) << strerror(errno); 298 } 299