root/tests/sys/fs/fusefs/fallocate.cc
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2021 Alan Somers
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

extern "C" {
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/resource.h>
#include <sys/time.h>

#include <fcntl.h>
#include <mntopts.h>    // for build_iovec
#include <signal.h>
#include <unistd.h>
}

#include "mockfs.hh"
#include "utils.hh"

using namespace testing;

/* Is buf all zero? */
static bool
is_zero(const char *buf, uint64_t size)
{
    return buf[0] == 0 && !memcmp(buf, buf + 1, size - 1);
}

class Fallocate: public FuseTest {
public:
/*
 * expect VOP_DEALLOCATE to be implemented by vop_stddeallocate.
 */
void expect_vop_stddeallocate(uint64_t ino, uint64_t off, uint64_t length)
{
        /* XXX read offset and size may depend on cache mode */
        EXPECT_CALL(*m_mock, process(
                ResultOf([=](auto in) {
                        return (in.header.opcode == FUSE_READ &&
                                in.header.nodeid == ino &&
                                in.body.read.offset <= off &&
                                in.body.read.offset + in.body.read.size >=
                                        off + length);
                }, Eq(true)),
                _)
        ).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
                assert(in.body.read.size <= sizeof(out.body.bytes));
                out.header.len = sizeof(struct fuse_out_header) +
                        in.body.read.size;
                memset(out.body.bytes, 'X', in.body.read.size);
        }))).RetiresOnSaturation();
        EXPECT_CALL(*m_mock, process(
                ResultOf([=](auto in) {
                        const char *buf = (const char*)in.body.bytes +
                                sizeof(struct fuse_write_in);

                        assert(length <= sizeof(in.body.bytes) -
                                sizeof(struct fuse_write_in));
                        return (in.header.opcode == FUSE_WRITE &&
                                in.header.nodeid == ino &&
                                in.body.write.offset == off  &&
                                in.body.write.size == length &&
                                is_zero(buf, length));
                }, Eq(true)),
                _)
        ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, write);
                out.body.write.size = length;
        })));
}
};

class Fspacectl: public Fallocate {};

class Fspacectl_7_18: public Fspacectl {
public:
virtual void SetUp() {
        m_kernel_minor_version = 18;
        Fspacectl::SetUp();
}
};

class FspacectlCache: public Fspacectl, public WithParamInterface<cache_mode> {
public:
bool m_direct_io;

FspacectlCache(): m_direct_io(false) {};

virtual void SetUp() {
        int cache_mode = GetParam();
        switch (cache_mode) {
                case Uncached:
                        m_direct_io = true;
                        break;
                case WritebackAsync:
                        m_async = true;
                        /* FALLTHROUGH */
                case Writeback:
                        m_init_flags |= FUSE_WRITEBACK_CACHE;
                        /* FALLTHROUGH */
                case Writethrough:
                        break;
                default:
                        FAIL() << "Unknown cache mode";
        }

        FuseTest::SetUp();
        if (IsSkipped())
                return;
}
};

class PosixFallocate: public Fallocate {
public:
static sig_atomic_t s_sigxfsz;

void SetUp() {
        s_sigxfsz = 0;
        FuseTest::SetUp();
}

void TearDown() {
        struct sigaction sa;

        bzero(&sa, sizeof(sa));
        sa.sa_handler = SIG_DFL;
        sigaction(SIGXFSZ, &sa, NULL);

        Fallocate::TearDown();
}

};

sig_atomic_t PosixFallocate::s_sigxfsz = 0;

void sigxfsz_handler(int __unused sig) {
        PosixFallocate::s_sigxfsz = 1;
}

class PosixFallocate_7_18: public PosixFallocate {
public:
virtual void SetUp() {
        m_kernel_minor_version = 18;
        PosixFallocate::SetUp();
}
};


/*
 * If the server returns ENOSYS, it indicates that the server does not support
 * FUSE_FALLOCATE.  This and future calls should fall back to vop_stddeallocate.
 */
TEST_F(Fspacectl, enosys)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        off_t fsize = 1 << 20;
        off_t off0 = 100;
        off_t len0 = 500;
        struct spacectl_range rqsr = { .r_offset = off0, .r_len = len0 };
        uint64_t ino = 42;
        uint64_t off1 = fsize;
        uint64_t len1 = 1000;
        off_t off2 = fsize / 2;
        off_t len2 = 500;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, off0, len0,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, ENOSYS);
        expect_vop_stddeallocate(ino, off0, len0);
        expect_vop_stddeallocate(ino, off2, len2);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        /* Subsequent calls shouldn't query the daemon either */
        rqsr.r_offset = off2;
        rqsr.r_len = len2;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        /* Neither should posix_fallocate query the daemon */
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, off1, len1));

        leak(fd);
}

/*
 * EOPNOTSUPP means "the file system does not support fallocate with the
 * supplied mode on this particular file".  So we should fallback, but not
 * assume anything about whether the operation will fail on a different file or
 * with a different mode.
 */
TEST_F(Fspacectl, eopnotsupp)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr;
        uint64_t ino = 42;
        uint64_t fsize = 1 << 20;
        uint64_t off0 = 500;
        uint64_t len = 1000;
        uint64_t off1 = fsize / 2;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, off0, len,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
                        EOPNOTSUPP);
        expect_vop_stddeallocate(ino, off0, len);
        expect_fallocate(ino, off1, len,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE,
                        EOPNOTSUPP);
        expect_vop_stddeallocate(ino, off1, len);
        expect_fallocate(ino, fsize, len, 0, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);

        /*
         * Though the FUSE daemon will reject the call, the kernel should fall
         * back to a read-modify-write approach.
         */
        rqsr.r_offset = off0;
        rqsr.r_len = len;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        /* Subsequent calls should still query the daemon */
        rqsr.r_offset = off1;
        rqsr.r_len = len;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        /* But subsequent posix_fallocate calls _should_ query the daemon */
        EXPECT_EQ(0, posix_fallocate(fd, fsize, len));

        leak(fd);
}

TEST_F(Fspacectl, erofs)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct statfs statbuf;
        uint64_t fsize = 2000;
        struct spacectl_range rqsr = { .r_offset = 0, .r_len = 1 };
        struct iovec *iov = NULL;
        int iovlen = 0;
        uint64_t ino = 42;
        int fd;
        int newflags;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        EXPECT_CALL(*m_mock, process(
                ResultOf([](auto in) {
                        return (in.header.opcode == FUSE_STATFS);
                }, Eq(true)),
                _)
        ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
        {
                /*
                 * All of the fields except f_flags are don't care, and f_flags
                 * is set by the VFS
                 */
                SET_OUT_HEADER_LEN(out, statfs);
        })));

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);

        /* Remount read-only */
        ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
        newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
        build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
        build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
        build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
        ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
        free_iovec(&iov, &iovlen);

        EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
        EXPECT_EQ(EROFS, errno);

        leak(fd);
}

/*
 * If FUSE_GETATTR fails when determining the size of the file, fspacectl
 * should fail gracefully.  This failure mode is easiest to trigger when
 * attribute caching is disabled.
 */
TEST_F(Fspacectl, getattr_fails)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        Sequence seq;
        struct spacectl_range rqsr;
        const uint64_t ino = 42;
        const uint64_t fsize = 2000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0);
        expect_open(ino, 0, 1);
        EXPECT_CALL(*m_mock, process(
                ResultOf([](auto in) {
                        return (in.header.opcode == FUSE_GETATTR &&
                                in.header.nodeid == ino);
                }, Eq(true)),
                _)
        ).Times(1)
        .InSequence(seq)
        .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, attr);
                out.body.attr.attr.ino = ino;
                out.body.attr.attr.mode = S_IFREG | 0644;
                out.body.attr.attr.size = fsize;
                out.body.attr.attr_valid = 0;
        })));
        EXPECT_CALL(*m_mock, process(
                ResultOf([](auto in) {
                        return (in.header.opcode == FUSE_GETATTR &&
                                in.header.nodeid == ino);
                }, Eq(true)),
                _)
        ).InSequence(seq)
        .WillOnce(ReturnErrno(EIO));

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        rqsr.r_offset = 500;
        rqsr.r_len = 1000;
        EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
        EXPECT_EQ(EIO, errno);

        leak(fd);
}

TEST_F(Fspacectl, ok)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr, rmsr;
        struct stat sb0, sb1;
        uint64_t ino = 42;
        uint64_t fsize = 2000;
        uint64_t offset = 500;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, offset, length,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
        rqsr.r_offset = offset;
        rqsr.r_len = length;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
        EXPECT_EQ(0, rmsr.r_len);
        EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);

        /*
         * The file's attributes should not have been invalidated, so this fstat
         * will not requery the daemon.
         */
        EXPECT_EQ(0, fstat(fd, &sb1));
        EXPECT_EQ(fsize, (uint64_t)sb1.st_size);

        /* mtime and ctime should be updated */
        EXPECT_EQ(sb0.st_atime, sb1.st_atime);
        EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
        EXPECT_NE(sb0.st_ctime, sb1.st_ctime);

        leak(fd);
}

/* The returned rqsr.r_off should be clipped at EoF */
TEST_F(Fspacectl, past_eof)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr, rmsr;
        uint64_t ino = 42;
        uint64_t fsize = 1000;
        uint64_t offset = 1500;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, offset, length,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        rqsr.r_offset = offset;
        rqsr.r_len = length;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
        EXPECT_EQ(0, rmsr.r_len);
        EXPECT_EQ((off_t)fsize, rmsr.r_offset);

        leak(fd);
}

/* The returned rqsr.r_off should be clipped at EoF */
TEST_F(Fspacectl, spans_eof)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr, rmsr;
        uint64_t ino = 42;
        uint64_t fsize = 1000;
        uint64_t offset = 500;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, offset, length,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        rqsr.r_offset = offset;
        rqsr.r_len = length;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
        EXPECT_EQ(0, rmsr.r_len);
        EXPECT_EQ((off_t)fsize, rmsr.r_offset);

        leak(fd);
}

/*
 * With older servers, no FUSE_FALLOCATE should be attempted.  The kernel
 * should fall back to vop_stddeallocate.
 */
TEST_F(Fspacectl_7_18, ok)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr, rmsr;
        char *buf;
        uint64_t ino = 42;
        uint64_t fsize = 2000;
        uint64_t offset = 500;
        uint64_t length = 1000;
        int fd;

        buf = new char[length];

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_vop_stddeallocate(ino, offset, length);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        rqsr.r_offset = offset;
        rqsr.r_len = length;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
        EXPECT_EQ(0, rmsr.r_len);
        EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);

        leak(fd);
        delete[] buf;
}

/*
 * A successful fspacectl should clear the zeroed data from the kernel cache.
 */
TEST_P(FspacectlCache, clears_cache)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        const char *CONTENTS = "abcdefghijklmnopqrstuvwxyz";
        struct spacectl_range rqsr, rmsr;
        uint64_t ino = 42;
        ssize_t bufsize = strlen(CONTENTS);
        uint64_t fsize = bufsize;
        uint8_t buf[bufsize];
        char zbuf[bufsize];
        uint64_t offset = 0;
        uint64_t length = bufsize;
        int fd;

        bzero(zbuf, bufsize);

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        /* NB: expectations are applied in LIFO order */
        expect_read(ino, 0, fsize, fsize, zbuf);
        expect_read(ino, 0, fsize, fsize, CONTENTS);
        expect_fallocate(ino, offset, length,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);

        /* Populate the cache */
        ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
                << strerror(errno);
        ASSERT_EQ(0, memcmp(buf, CONTENTS, fsize));

        /* Zero the file */
        rqsr.r_offset = offset;
        rqsr.r_len = length;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, &rmsr));
        EXPECT_EQ(0, rmsr.r_len);
        EXPECT_EQ((off_t)(offset + length), rmsr.r_offset);

        /* Read again.  This should query the daemon */
        ASSERT_EQ(fsize, (uint64_t)pread(fd, buf, bufsize, 0))
                << strerror(errno);
        ASSERT_EQ(0, memcmp(buf, zbuf, fsize));

        leak(fd);
}

INSTANTIATE_TEST_SUITE_P(FspacectlCache, FspacectlCache,
        Values(Uncached, Writethrough, Writeback)
);

/*
 * If the server returns ENOSYS, it indicates that the server does not support
 * FUSE_FALLOCATE.  This and future calls should return EOPNOTSUPP.
 */
TEST_F(PosixFallocate, enosys)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        uint64_t ino = 42;
        uint64_t off0 = 0;
        uint64_t len0 = 1000;
        off_t off1 = 100;
        off_t len1 = 200;
        uint64_t fsize = 500;
        struct spacectl_range rqsr = { .r_offset = off1, .r_len = len1 };
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, off0, len0, 0, ENOSYS);
        expect_vop_stddeallocate(ino, off1, len1);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, off0, len0));

        /* Subsequent calls shouldn't query the daemon*/
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, off0, len0));

        /* Neither should VOP_DEALLOCATE query the daemon */
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        leak(fd);
}

/*
 * EOPNOTSUPP means "the file system does not support fallocate with the
 * supplied mode on this particular file".  So we should fallback, but not
 * assume anything about whether the operation will fail on a different file or
 * with a different mode.
 */
TEST_F(PosixFallocate, eopnotsupp)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct spacectl_range rqsr;
        uint64_t ino = 42;
        uint64_t fsize = 2000;
        uint64_t offset = 0;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, fsize, length, 0, EOPNOTSUPP);
        expect_fallocate(ino, offset, length, 0, EOPNOTSUPP);
        expect_fallocate(ino, offset, length,
                FUSE_FALLOC_FL_KEEP_SIZE | FUSE_FALLOC_FL_PUNCH_HOLE, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, fsize, length));

        /* Subsequent calls should still query the daemon*/
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, offset, length));

        /* And subsequent VOP_DEALLOCATE calls should also query the daemon */
        rqsr.r_len = length;
        rqsr.r_offset = offset;
        EXPECT_EQ(0, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));

        leak(fd);
}

/* EIO is not a permanent error, and may be retried */
TEST_F(PosixFallocate, eio)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        uint64_t ino = 42;
        uint64_t offset = 0;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
        expect_open(ino, 0, 1);
        expect_fallocate(ino, offset, length, 0, EIO);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(EIO, posix_fallocate(fd, offset, length));

        expect_fallocate(ino, offset, length, 0, 0);

        EXPECT_EQ(0, posix_fallocate(fd, offset, length));

        leak(fd);
}

TEST_F(PosixFallocate, erofs)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct statfs statbuf;
        struct iovec *iov = NULL;
        int iovlen = 0;
        uint64_t ino = 42;
        uint64_t offset = 0;
        uint64_t length = 1000;
        int fd;
        int newflags;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
        expect_open(ino, 0, 1);
        EXPECT_CALL(*m_mock, process(
                ResultOf([](auto in) {
                        return (in.header.opcode == FUSE_STATFS);
                }, Eq(true)),
                _)
        ).WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
        {
                /*
                 * All of the fields except f_flags are don't care, and f_flags
                 * is set by the VFS
                 */
                SET_OUT_HEADER_LEN(out, statfs);
        })));

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);

        /* Remount read-only */
        ASSERT_EQ(0, statfs("mountpoint", &statbuf)) << strerror(errno);
        newflags = statbuf.f_flags | MNT_UPDATE | MNT_RDONLY;
        build_iovec(&iov, &iovlen, "fstype", (void*)statbuf.f_fstypename, -1);
        build_iovec(&iov, &iovlen, "fspath", (void*)statbuf.f_mntonname, -1);
        build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
        ASSERT_EQ(0, nmount(iov, iovlen, newflags)) << strerror(errno);
        free_iovec(&iov, &iovlen);

        EXPECT_EQ(EROFS, posix_fallocate(fd, offset, length));

        leak(fd);
}

TEST_F(PosixFallocate, ok)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct stat sb0, sb1;
        uint64_t ino = 42;
        uint64_t offset = 0;
        uint64_t length = 1000;
        int fd;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFREG | 0644;
                out.body.entry.nodeid = ino;
                out.body.entry.entry_valid = UINT64_MAX;
                out.body.entry.attr_valid = UINT64_MAX;
        })));
        expect_open(ino, 0, 1);
        expect_fallocate(ino, offset, length, 0, 0);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        ASSERT_EQ(0, fstat(fd, &sb0)) << strerror(errno);
        EXPECT_EQ(0, posix_fallocate(fd, offset, length));
        /*
         * Despite the originally cached file size of zero, stat should now
         * return either the new size or requery the daemon.
         */
        EXPECT_EQ(0, stat(FULLPATH, &sb1));
        EXPECT_EQ(length, (uint64_t)sb1.st_size);

        /* mtime and ctime should be updated */
        EXPECT_EQ(sb0.st_atime, sb1.st_atime);
        EXPECT_NE(sb0.st_mtime, sb1.st_mtime);
        EXPECT_NE(sb0.st_ctime, sb1.st_ctime);

        leak(fd);
}

/* fusefs should respect RLIMIT_FSIZE */
TEST_F(PosixFallocate, rlimit_fsize)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        struct rlimit rl;
        uint64_t ino = 42;
        uint64_t offset = 0;
        uint64_t length = 1'000'000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
        expect_open(ino, 0, 1);

        rl.rlim_cur = length / 2;
        rl.rlim_max = 10 * length;
        ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
        ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(EFBIG, posix_fallocate(fd, offset, length));
        EXPECT_EQ(1, s_sigxfsz);

        leak(fd);
}

/* With older servers, no FUSE_FALLOCATE should be attempted */
TEST_F(PosixFallocate_7_18, eopnotsupp)
{
        const char FULLPATH[] = "mountpoint/some_file.txt";
        const char RELPATH[] = "some_file.txt";
        uint64_t ino = 42;
        uint64_t offset = 0;
        uint64_t length = 1000;
        int fd;

        expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
        expect_open(ino, 0, 1);

        fd = open(FULLPATH, O_RDWR);
        ASSERT_LE(0, fd) << strerror(errno);
        EXPECT_EQ(EOPNOTSUPP, posix_fallocate(fd, offset, length));

        leak(fd);
}