/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2019 The FreeBSD Foundation
 *
 * This software was developed by BFF Storage Systems, LLC under sponsorship
 * from the FreeBSD Foundation.
 *
 * 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/types.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/time.h>

#include <fcntl.h>
#include <semaphore.h>
#include <signal.h>
}

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

using namespace testing;

class Setattr : public FuseTest {
public:
static sig_atomic_t s_sigxfsz;
};

class RofsSetattr: public Setattr {
public:
virtual void SetUp() {
	s_sigxfsz = 0;
	m_ro = true;
	Setattr::SetUp();
}
};

class Setattr_7_8: public Setattr {
public:
virtual void SetUp() {
	m_kernel_minor_version = 8;
	Setattr::SetUp();
}
};


sig_atomic_t Setattr::s_sigxfsz = 0;

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

/*
 * If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
 * should use the cached attributes, rather than query the daemon
 */
TEST_F(Setattr, attr_cache)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	struct stat sb;
	const mode_t newmode = 0644;

	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
	.WillRepeatedly(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;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | newmode;
		out.body.attr.attr_valid = UINT64_MAX;
	})));
	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			return (in.header.opcode == FUSE_GETATTR);
		}, Eq(true)),
		_)
	).Times(0);

	/* Set an attribute with SETATTR */
	ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);

	/* The stat(2) should use cached attributes */
	ASSERT_EQ(0, stat(FULLPATH, &sb));
	EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
}

/* Change the mode of a file */
TEST_F(Setattr, chmod)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const mode_t oldmode = 0755;
	const mode_t newmode = 0644;

	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 | oldmode;
		out.body.entry.nodeid = ino;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			uint32_t valid = FATTR_MODE;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.mode == newmode);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | newmode;
	})));
	EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}

/* 
 * Chmod a multiply-linked file with cached attributes.  Check that both files'
 * attributes have changed.
 */
TEST_F(Setattr, chmod_multiply_linked)
{
	const char FULLPATH0[] = "mountpoint/some_file.txt";
	const char RELPATH0[] = "some_file.txt";
	const char FULLPATH1[] = "mountpoint/other_file.txt";
	const char RELPATH1[] = "other_file.txt";
	struct stat sb;
	const uint64_t ino = 42;
	const mode_t oldmode = 0777;
	const mode_t newmode = 0666;

	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, entry);
		out.body.entry.attr.mode = S_IFREG | oldmode;
		out.body.entry.nodeid = ino;
		out.body.entry.attr.nlink = 2;
		out.body.entry.attr_valid = UINT64_MAX;
		out.body.entry.entry_valid = UINT64_MAX;
	})));

	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, entry);
		out.body.entry.attr.mode = S_IFREG | oldmode;
		out.body.entry.nodeid = ino;
		out.body.entry.attr.nlink = 2;
		out.body.entry.attr_valid = UINT64_MAX;
		out.body.entry.entry_valid = UINT64_MAX;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			uint32_t valid = FATTR_MODE;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.mode == newmode);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;
		out.body.attr.attr.mode = S_IFREG | newmode;
		out.body.attr.attr.nlink = 2;
		out.body.attr.attr_valid = UINT64_MAX;
	})));

	/* For a lookup of the 2nd file to get it into the cache*/
	ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
	EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);

	ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno);
	ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno);
	EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
	ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
	EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
}


/* Change the owner and group of a file */
TEST_F(Setattr, chown)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const gid_t oldgroup = 66;
	const gid_t newgroup = 99;
	const uid_t olduser = 33;
	const uid_t newuser = 44;

	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.attr.gid = oldgroup;
		out.body.entry.attr.uid = olduser;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			uint32_t valid = FATTR_GID | FATTR_UID;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.uid == newuser &&
				in.body.setattr.gid == newgroup);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0644;
		out.body.attr.attr.uid = newuser;
		out.body.attr.attr.gid = newgroup;
	})));
	EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno);
}



/* 
 * FUSE daemons are allowed to check permissions however they like.  If the
 * daemon returns EPERM, even if the file permissions "should" grant access,
 * then fuse(4) should return EPERM too.
 */
TEST_F(Setattr, eperm)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;

	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
	.WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
		SET_OUT_HEADER_LEN(out, entry);
		out.body.entry.attr.mode = S_IFREG | 0777;
		out.body.entry.nodeid = ino;
		out.body.entry.attr.uid = in.header.uid;
		out.body.entry.attr.gid = in.header.gid;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnErrno(EPERM)));
	EXPECT_NE(0, truncate(FULLPATH, 10));
	EXPECT_EQ(EPERM, errno);
}

/* Change the mode of an open file, by its file descriptor */
TEST_F(Setattr, fchmod)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	uint64_t ino = 42;
	int fd;
	const mode_t oldmode = 0755;
	const mode_t newmode = 0644;

	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 | oldmode;
		out.body.entry.nodeid = ino;
		out.body.entry.attr_valid = UINT64_MAX;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_OPEN &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		out.header.len = sizeof(out.header);
		SET_OUT_HEADER_LEN(out, open);
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			uint32_t valid = FATTR_MODE;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.mode == newmode);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | newmode;
	})));

	fd = open(FULLPATH, O_RDONLY);
	ASSERT_LE(0, fd) << strerror(errno);
	ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
	leak(fd);
}

/* Change the size of an open file, by its file descriptor */
TEST_F(Setattr, ftruncate)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	uint64_t ino = 42;
	int fd;
	uint64_t fh = 0xdeadbeef1a7ebabe;
	const off_t oldsize = 99;
	const off_t newsize = 12345;

	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 | 0755;
		out.body.entry.nodeid = ino;
		out.body.entry.attr_valid = UINT64_MAX;
		out.body.entry.attr.size = oldsize;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_OPEN &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		out.header.len = sizeof(out.header);
		SET_OUT_HEADER_LEN(out, open);
		out.body.open.fh = fh;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			uint32_t valid = FATTR_SIZE | FATTR_FH;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.fh == fh);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0755;
		out.body.attr.attr.size = newsize;
	})));

	fd = open(FULLPATH, O_RDWR);
	ASSERT_LE(0, fd) << strerror(errno);
	ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno);
	leak(fd);
}

/* Change the size of the file */
TEST_F(Setattr, truncate) {
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const uint64_t oldsize = 100'000'000;
	const uint64_t newsize = 20'000'000;

	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.attr.size = oldsize;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			uint32_t valid = FATTR_SIZE;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.size == newsize);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0644;
		out.body.attr.attr.size = newsize;
	})));
	EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno);
}

/*
 * Truncating a file should discard cached data past the truncation point.
 * This is a regression test for bug 233783.
 *
 * There are two distinct failure modes.  The first one is a failure to zero
 * the portion of the file's final buffer past EOF.  It can be reproduced by
 * fsx -WR -P /tmp -S10 fsx.bin
 *
 * The second is a failure to drop buffers beyond that.  It can be reproduced by
 * fsx -WR -P /tmp -S18 -n fsx.bin
 * Also reproducible in sh with:
 * $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt
 * $> cd /tmp/mnt/tmp
 * $> dd if=/dev/random of=randfile bs=1k count=192
 * $> truncate -s 1k randfile && truncate -s 192k randfile
 * $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000
 */
TEST_F(Setattr, truncate_discards_cached_data) {
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	char *w0buf, *r0buf, *r1buf, *expected;
	off_t w0_offset = 0;
	size_t w0_size = 0x30000;
	off_t r0_offset = 0;
	off_t r0_size = w0_size;
	size_t trunc0_size = 0x400;
	size_t trunc1_size = w0_size;
	off_t r1_offset = trunc0_size;
	off_t r1_size = w0_size - trunc0_size;
	size_t cur_size = 0;
	const uint64_t ino = 42;
	mode_t mode = S_IFREG | 0644;
	int fd, r;
	bool should_have_data = false;

	w0buf = new char[w0_size];
	memset(w0buf, 'X', w0_size);

	r0buf = new char[r0_size];
	r1buf = new char[r1_size];

	expected = new char[r1_size]();

	expect_lookup(RELPATH, ino, mode, 0, 1);
	expect_open(ino, O_RDWR, 1);
	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_GETATTR &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;
		out.body.attr.attr.mode = mode;
		out.body.attr.attr.size = cur_size;
	})));
	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_WRITE);
		}, Eq(true)),
		_)
	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
		SET_OUT_HEADER_LEN(out, write);
		out.body.attr.attr.ino = ino;
		out.body.write.size = in.body.write.size;
		cur_size = std::max(static_cast<uint64_t>(cur_size),
			in.body.write.size + in.body.write.offset);
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				(in.body.setattr.valid & FATTR_SIZE));
		}, Eq(true)),
		_)
	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
		auto trunc_size = in.body.setattr.size;
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;
		out.body.attr.attr.mode = mode;
		out.body.attr.attr.size = trunc_size;
		cur_size = trunc_size;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			return (in.header.opcode == FUSE_READ);
		}, Eq(true)),
		_)
	).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
		auto osize = std::min(
			static_cast<uint64_t>(cur_size) - in.body.read.offset,
			static_cast<uint64_t>(in.body.read.size));
		assert(osize <= sizeof(out.body.bytes));
		out.header.len = sizeof(struct fuse_out_header) + osize;
		if (should_have_data)
			memset(out.body.bytes, 'X', osize);
		else
			bzero(out.body.bytes, osize);
	})));

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

	/* Fill the file with Xs */
	ASSERT_EQ(static_cast<ssize_t>(w0_size),
		pwrite(fd, w0buf, w0_size, w0_offset));
	should_have_data = true;
	/* Fill the cache */
	ASSERT_EQ(static_cast<ssize_t>(r0_size),
		pread(fd, r0buf, r0_size, r0_offset));
	/* 1st truncate should discard cached data */
	EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno);
	should_have_data = false;
	/* 2nd truncate extends file into previously cached data */
	EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno);
	/* Read should return all zeros */
	ASSERT_EQ(static_cast<ssize_t>(r1_size),
		pread(fd, r1buf, r1_size, r1_offset));

	r = memcmp(expected, r1buf, r1_size);
	ASSERT_EQ(0, r);

	delete[] expected;
	delete[] r1buf;
	delete[] r0buf;
	delete[] w0buf;

	leak(fd);
}

/* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */
TEST_F(Setattr, truncate_rlimit_rsize)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	struct rlimit rl;
	const uint64_t ino = 42;
	const uint64_t oldsize = 0;
	const uint64_t newsize = 100'000'000;

	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.attr.size = oldsize;
	})));

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

	EXPECT_EQ(-1, truncate(FULLPATH, newsize));
	EXPECT_EQ(EFBIG, errno);
	EXPECT_EQ(1, s_sigxfsz);
}

/* Change a file's timestamps */
TEST_F(Setattr, utimensat) {
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const timespec oldtimes[2] = {
		{.tv_sec = 1, .tv_nsec = 2},
		{.tv_sec = 3, .tv_nsec = 4},
	};
	const timespec newtimes[2] = {
		{.tv_sec = 5, .tv_nsec = 6},
		{.tv_sec = 7, .tv_nsec = 8},
	};

	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.attr_valid = UINT64_MAX;
		out.body.entry.attr.atime = oldtimes[0].tv_sec;
		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
		out.body.entry.attr.mtime = oldtimes[1].tv_sec;
		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			uint32_t valid = FATTR_ATIME | FATTR_MTIME;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				(time_t)in.body.setattr.atime ==
					newtimes[0].tv_sec &&
				(long)in.body.setattr.atimensec ==
					newtimes[0].tv_nsec &&
				(time_t)in.body.setattr.mtime ==
					newtimes[1].tv_sec &&
				(long)in.body.setattr.mtimensec ==
					newtimes[1].tv_nsec);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0644;
		out.body.attr.attr.atime = newtimes[0].tv_sec;
		out.body.attr.attr.atimensec = newtimes[0].tv_nsec;
		out.body.attr.attr.mtime = newtimes[1].tv_sec;
		out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
	})));
	EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
		<< strerror(errno);
}

/* Change a file mtime but not its atime */
TEST_F(Setattr, utimensat_mtime_only) {
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const timespec oldtimes[2] = {
		{.tv_sec = 1, .tv_nsec = 2},
		{.tv_sec = 3, .tv_nsec = 4},
	};
	const timespec newtimes[2] = {
		{.tv_sec = 5, .tv_nsec = UTIME_OMIT},
		{.tv_sec = 7, .tv_nsec = 8},
	};

	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.attr_valid = UINT64_MAX;
		out.body.entry.attr.atime = oldtimes[0].tv_sec;
		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
		out.body.entry.attr.mtime = oldtimes[1].tv_sec;
		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			uint32_t valid = FATTR_MTIME;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				(time_t)in.body.setattr.mtime ==
					newtimes[1].tv_sec &&
				(long)in.body.setattr.mtimensec ==
					newtimes[1].tv_nsec);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0644;
		out.body.attr.attr.atime = oldtimes[0].tv_sec;
		out.body.attr.attr.atimensec = oldtimes[0].tv_nsec;
		out.body.attr.attr.mtime = newtimes[1].tv_sec;
		out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
	})));
	EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
		<< strerror(errno);
}

/*
 * Set a file's mtime and atime to now
 *
 * The design of FreeBSD's VFS does not allow fusefs to set just one of atime
 * or mtime to UTIME_NOW; it's both or neither.
 */
TEST_F(Setattr, utimensat_utime_now) {
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const timespec oldtimes[2] = {
		{.tv_sec = 1, .tv_nsec = 2},
		{.tv_sec = 3, .tv_nsec = 4},
	};
	const timespec newtimes[2] = {
		{.tv_sec = 0, .tv_nsec = UTIME_NOW},
		{.tv_sec = 0, .tv_nsec = UTIME_NOW},
	};
	/* "now" is whatever the server says it is */
	const timespec now[2] = {
		{.tv_sec = 5, .tv_nsec = 7},
		{.tv_sec = 6, .tv_nsec = 8},
	};
	struct stat sb;

	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.attr_valid = UINT64_MAX;
		out.body.entry.entry_valid = UINT64_MAX;
		out.body.entry.attr.atime = oldtimes[0].tv_sec;
		out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
		out.body.entry.attr.mtime = oldtimes[1].tv_sec;
		out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([=](auto in) {
			uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW |
				FATTR_MTIME | FATTR_MTIME_NOW;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | 0644;
		out.body.attr.attr.atime = now[0].tv_sec;
		out.body.attr.attr.atimensec = now[0].tv_nsec;
		out.body.attr.attr.mtime = now[1].tv_sec;
		out.body.attr.attr.mtimensec = now[1].tv_nsec;
		out.body.attr.attr_valid = UINT64_MAX;
	})));
	ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
		<< strerror(errno);
	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
	EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec);
	EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec);
	EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec);
	EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec);
}

/*
 * FUSE_SETATTR returns a different file type, even though the entry cache
 * hasn't expired.  This is a server bug!  It probably means that the server
 * removed the file and recreated it with the same inode but a different vtyp.
 * The best thing fusefs can do is return ENOENT to the caller.  After all, the
 * entry must not have existed recently.
 */
TEST_F(Setattr, vtyp_conflict)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	uid_t newuser = 12345;
	sem_t sem;

	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);

	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 | 0777;
		out.body.entry.nodeid = ino;
		out.body.entry.entry_valid = UINT64_MAX;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr);
		out.body.attr.attr.ino = ino;
		out.body.attr.attr.mode = S_IFDIR | 0777;	// Changed!
		out.body.attr.attr.uid = newuser;
	})));
	// We should reclaim stale vnodes
	expect_forget(ino, 1, &sem);

	EXPECT_NE(0, chown(FULLPATH, newuser, -1));
	EXPECT_EQ(ENOENT, errno);

	sem_wait(&sem);
	sem_destroy(&sem);
}

/* On a read-only mount, no attributes may be changed */
TEST_F(RofsSetattr, erofs)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const mode_t oldmode = 0755;
	const mode_t newmode = 0644;

	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 | oldmode;
		out.body.entry.nodeid = ino;
	})));

	ASSERT_EQ(-1, chmod(FULLPATH, newmode));
	ASSERT_EQ(EROFS, errno);
}

/* Change the mode of a file */
TEST_F(Setattr_7_8, chmod)
{
	const char FULLPATH[] = "mountpoint/some_file.txt";
	const char RELPATH[] = "some_file.txt";
	const uint64_t ino = 42;
	const mode_t oldmode = 0755;
	const mode_t newmode = 0644;

	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, entry_7_8);
		out.body.entry.attr.mode = S_IFREG | oldmode;
		out.body.entry.nodeid = ino;
	})));

	EXPECT_CALL(*m_mock, process(
		ResultOf([](auto in) {
			uint32_t valid = FATTR_MODE;
			return (in.header.opcode == FUSE_SETATTR &&
				in.header.nodeid == ino &&
				in.body.setattr.valid == valid &&
				in.body.setattr.mode == newmode);
		}, Eq(true)),
		_)
	).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
		SET_OUT_HEADER_LEN(out, attr_7_8);
		out.body.attr.attr.ino = ino;	// Must match nodeid
		out.body.attr.attr.mode = S_IFREG | newmode;
	})));
	EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
}
