1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3 * 4 * Copyright (c) 2020 Alan Somers 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 * SUCH DAMAGE. 26 * 27 * $FreeBSD$ 28 */ 29 30 extern "C" { 31 #include <sys/param.h> 32 33 #include <fcntl.h> 34 } 35 36 #include "mockfs.hh" 37 #include "utils.hh" 38 39 using namespace testing; 40 41 class Lseek: public FuseTest {}; 42 class LseekPathconf: public Lseek {}; 43 class LseekPathconf_7_23: public LseekPathconf { 44 public: 45 virtual void SetUp() { 46 m_kernel_minor_version = 23; 47 FuseTest::SetUp(); 48 } 49 }; 50 class LseekSeekHole: public Lseek {}; 51 class LseekSeekData: public Lseek {}; 52 53 /* 54 * If a previous lseek operation has already returned enosys, then pathconf can 55 * return EINVAL immediately. 56 */ 57 TEST_F(LseekPathconf, already_enosys) 58 { 59 const char FULLPATH[] = "mountpoint/some_file.txt"; 60 const char RELPATH[] = "some_file.txt"; 61 const uint64_t ino = 42; 62 off_t fsize = 1 << 30; /* 1 GiB */ 63 off_t offset_in = 1 << 28; 64 int fd; 65 66 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 67 expect_open(ino, 0, 1); 68 EXPECT_CALL(*m_mock, process( 69 ResultOf([=](auto in) { 70 return (in.header.opcode == FUSE_LSEEK); 71 }, Eq(true)), 72 _) 73 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 74 75 fd = open(FULLPATH, O_RDONLY); 76 77 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 78 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 79 EXPECT_EQ(EINVAL, errno); 80 } 81 82 /* 83 * If a previous lseek operation has already returned successfully, then 84 * pathconf can return 1 immediately. 1 means "holes are reported, but size is 85 * not specified". 86 */ 87 TEST_F(LseekPathconf, already_seeked) 88 { 89 const char FULLPATH[] = "mountpoint/some_file.txt"; 90 const char RELPATH[] = "some_file.txt"; 91 const uint64_t ino = 42; 92 off_t fsize = 1 << 30; /* 1 GiB */ 93 off_t offset = 1 << 28; 94 int fd; 95 96 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 97 expect_open(ino, 0, 1); 98 EXPECT_CALL(*m_mock, process( 99 ResultOf([=](auto in) { 100 return (in.header.opcode == FUSE_LSEEK); 101 }, Eq(true)), 102 _) 103 ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) { 104 SET_OUT_HEADER_LEN(out, lseek); 105 out.body.lseek.offset = i.body.lseek.offset; 106 }))); 107 fd = open(FULLPATH, O_RDONLY); 108 EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA)); 109 110 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 111 } 112 113 /* 114 * If no FUSE_LSEEK operation has been attempted since mount, try once as soon 115 * as a pathconf request comes in. 116 */ 117 TEST_F(LseekPathconf, enosys_now) 118 { 119 const char FULLPATH[] = "mountpoint/some_file.txt"; 120 const char RELPATH[] = "some_file.txt"; 121 const uint64_t ino = 42; 122 off_t fsize = 1 << 30; /* 1 GiB */ 123 int fd; 124 125 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 126 expect_open(ino, 0, 1); 127 EXPECT_CALL(*m_mock, process( 128 ResultOf([=](auto in) { 129 return (in.header.opcode == FUSE_LSEEK); 130 }, Eq(true)), 131 _) 132 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 133 134 fd = open(FULLPATH, O_RDONLY); 135 136 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 137 EXPECT_EQ(EINVAL, errno); 138 } 139 140 /* 141 * If no FUSE_LSEEK operation has been attempted since mount, try one as soon 142 * as a pathconf request comes in. This is the typical pattern of bsdtar. It 143 * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. 144 */ 145 TEST_F(LseekPathconf, seek_now) 146 { 147 const char FULLPATH[] = "mountpoint/some_file.txt"; 148 const char RELPATH[] = "some_file.txt"; 149 const uint64_t ino = 42; 150 off_t fsize = 1 << 30; /* 1 GiB */ 151 off_t offset_initial = 1 << 27; 152 off_t offset_out = 1 << 29; 153 int fd; 154 155 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 156 expect_open(ino, 0, 1); 157 EXPECT_CALL(*m_mock, process( 158 ResultOf([=](auto in) { 159 return (in.header.opcode == FUSE_LSEEK); 160 }, Eq(true)), 161 _) 162 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 163 SET_OUT_HEADER_LEN(out, lseek); 164 out.body.lseek.offset = offset_out; 165 }))); 166 167 fd = open(FULLPATH, O_RDONLY); 168 EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); 169 EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 170 /* And check that the file pointer hasn't changed */ 171 EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR)); 172 } 173 174 /* 175 * For servers using older protocol versions, no FUSE_LSEEK should be attempted 176 */ 177 TEST_F(LseekPathconf_7_23, already_enosys) 178 { 179 const char FULLPATH[] = "mountpoint/some_file.txt"; 180 const char RELPATH[] = "some_file.txt"; 181 const uint64_t ino = 42; 182 off_t fsize = 1 << 30; /* 1 GiB */ 183 int fd; 184 185 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 186 expect_open(ino, 0, 1); 187 EXPECT_CALL(*m_mock, process( 188 ResultOf([=](auto in) { 189 return (in.header.opcode == FUSE_LSEEK); 190 }, Eq(true)), 191 _) 192 ).Times(0); 193 194 fd = open(FULLPATH, O_RDONLY); 195 EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); 196 EXPECT_EQ(EINVAL, errno); 197 } 198 199 TEST_F(LseekSeekData, ok) 200 { 201 const char FULLPATH[] = "mountpoint/some_file.txt"; 202 const char RELPATH[] = "some_file.txt"; 203 const uint64_t ino = 42; 204 off_t fsize = 1 << 30; /* 1 GiB */ 205 off_t offset_in = 1 << 28; 206 off_t offset_out = 1 << 29; 207 int fd; 208 209 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 210 expect_open(ino, 0, 1); 211 EXPECT_CALL(*m_mock, process( 212 ResultOf([=](auto in) { 213 return (in.header.opcode == FUSE_LSEEK && 214 in.header.nodeid == ino && 215 in.body.lseek.fh == FH && 216 (off_t)in.body.lseek.offset == offset_in && 217 in.body.lseek.whence == SEEK_DATA); 218 }, Eq(true)), 219 _) 220 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 221 SET_OUT_HEADER_LEN(out, lseek); 222 out.body.lseek.offset = offset_out; 223 }))); 224 fd = open(FULLPATH, O_RDONLY); 225 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA)); 226 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 227 } 228 229 /* 230 * If the server returns ENOSYS, fusefs should fall back to the default 231 * behavior, and never query the server again. 232 */ 233 TEST_F(LseekSeekData, enosys) 234 { 235 const char FULLPATH[] = "mountpoint/some_file.txt"; 236 const char RELPATH[] = "some_file.txt"; 237 const uint64_t ino = 42; 238 off_t fsize = 1 << 30; /* 1 GiB */ 239 off_t offset_in = 1 << 28; 240 int fd; 241 242 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 243 expect_open(ino, 0, 1); 244 EXPECT_CALL(*m_mock, process( 245 ResultOf([=](auto in) { 246 return (in.header.opcode == FUSE_LSEEK && 247 in.header.nodeid == ino && 248 in.body.lseek.fh == FH && 249 (off_t)in.body.lseek.offset == offset_in && 250 in.body.lseek.whence == SEEK_DATA); 251 }, Eq(true)), 252 _) 253 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 254 fd = open(FULLPATH, O_RDONLY); 255 256 /* 257 * Default behavior: ENXIO if offset is < 0 or >= fsize, offset 258 * otherwise. 259 */ 260 EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); 261 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 262 EXPECT_EQ(ENXIO, errno); 263 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 264 EXPECT_EQ(ENXIO, errno); 265 } 266 267 TEST_F(LseekSeekHole, ok) 268 { 269 const char FULLPATH[] = "mountpoint/some_file.txt"; 270 const char RELPATH[] = "some_file.txt"; 271 const uint64_t ino = 42; 272 off_t fsize = 1 << 30; /* 1 GiB */ 273 off_t offset_in = 1 << 28; 274 off_t offset_out = 1 << 29; 275 int fd; 276 277 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 278 expect_open(ino, 0, 1); 279 EXPECT_CALL(*m_mock, process( 280 ResultOf([=](auto in) { 281 return (in.header.opcode == FUSE_LSEEK && 282 in.header.nodeid == ino && 283 in.body.lseek.fh == FH && 284 (off_t)in.body.lseek.offset == offset_in && 285 in.body.lseek.whence == SEEK_HOLE); 286 }, Eq(true)), 287 _) 288 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { 289 SET_OUT_HEADER_LEN(out, lseek); 290 out.body.lseek.offset = offset_out; 291 }))); 292 fd = open(FULLPATH, O_RDONLY); 293 EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); 294 EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); 295 } 296 297 /* 298 * If the server returns ENOSYS, fusefs should fall back to the default 299 * behavior, and never query the server again. 300 */ 301 TEST_F(LseekSeekHole, enosys) 302 { 303 const char FULLPATH[] = "mountpoint/some_file.txt"; 304 const char RELPATH[] = "some_file.txt"; 305 const uint64_t ino = 42; 306 off_t fsize = 1 << 30; /* 1 GiB */ 307 off_t offset_in = 1 << 28; 308 int fd; 309 310 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 311 expect_open(ino, 0, 1); 312 EXPECT_CALL(*m_mock, process( 313 ResultOf([=](auto in) { 314 return (in.header.opcode == FUSE_LSEEK && 315 in.header.nodeid == ino && 316 in.body.lseek.fh == FH && 317 (off_t)in.body.lseek.offset == offset_in && 318 in.body.lseek.whence == SEEK_HOLE); 319 }, Eq(true)), 320 _) 321 ).WillOnce(Invoke(ReturnErrno(ENOSYS))); 322 fd = open(FULLPATH, O_RDONLY); 323 324 /* 325 * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize 326 * otherwise. 327 */ 328 EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE)); 329 EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE)); 330 EXPECT_EQ(ENXIO, errno); 331 EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE)); 332 EXPECT_EQ(ENXIO, errno); 333 } 334 335 /* lseek should return ENXIO when offset points to EOF */ 336 TEST_F(LseekSeekHole, enxio) 337 { 338 const char FULLPATH[] = "mountpoint/some_file.txt"; 339 const char RELPATH[] = "some_file.txt"; 340 const uint64_t ino = 42; 341 off_t fsize = 1 << 30; /* 1 GiB */ 342 off_t offset_in = fsize; 343 int fd; 344 345 expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); 346 expect_open(ino, 0, 1); 347 EXPECT_CALL(*m_mock, process( 348 ResultOf([=](auto in) { 349 return (in.header.opcode == FUSE_LSEEK && 350 in.header.nodeid == ino && 351 in.body.lseek.fh == FH && 352 (off_t)in.body.lseek.offset == offset_in && 353 in.body.lseek.whence == SEEK_HOLE); 354 }, Eq(true)), 355 _) 356 ).WillOnce(Invoke(ReturnErrno(ENXIO))); 357 fd = open(FULLPATH, O_RDONLY); 358 EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); 359 EXPECT_EQ(ENXIO, errno); 360 } 361