Implement FUSE_WRITE

This commit adds basic write(2) support for FUSE.
This commit is contained in:
Jinmou Li
2020-09-16 12:19:30 -07:00
committed by Andrei Vagin
parent 18f1e1c91b
commit 98faed55e6
11 changed files with 592 additions and 38 deletions
+41 -26
View File
@@ -124,32 +124,6 @@ type FUSEHeaderOut struct {
Unique FUSEOpID
}
// FUSEWriteIn is the header written by a daemon when it makes a
// write request to the FUSE filesystem.
//
// +marshal
type FUSEWriteIn struct {
// Fh specifies the file handle that is being written to.
Fh uint64
// Offset is the offset of the write.
Offset uint64
// Size is the size of data being written.
Size uint32
// WriteFlags is the flags used during the write.
WriteFlags uint32
// LockOwner is the ID of the lock owner.
LockOwner uint64
// Flags is the flags for the request.
Flags uint32
_ uint32
}
// FUSE_INIT flags, consistent with the ones in include/uapi/linux/fuse.h.
// Our taget version is 7.23 but we have few implemented in advance.
const (
@@ -427,6 +401,47 @@ type FUSEReadIn struct {
_ uint32
}
// FUSEWriteIn is the first part of the payload of the
// request sent by the kernel to the daemon
// for FUSE_WRITE (struct for FUSE version >= 7.9).
//
// The second part of the payload is the
// binary bytes of the data to be written.
//
// +marshal
type FUSEWriteIn struct {
// Fh is the file handle in userspace.
Fh uint64
// Offset is the write offset.
Offset uint64
// Size is the number of bytes to write.
Size uint32
// ReadFlags for this FUSE_WRITE request.
WriteFlags uint32
// LockOwner is the id of the lock owner if there is one.
LockOwner uint64
// Flags for the underlying file.
Flags uint32
_ uint32
}
// FUSEWriteOut is the payload of the reply sent by the daemon to the kernel
// for a FUSE_WRITE request.
//
// +marshal
type FUSEWriteOut struct {
// Size is the number of bytes written.
Size uint32
_ uint32
}
// FUSEReleaseIn is the request sent by the kernel to the daemon
// when there is no more reference to a file.
//
+4
View File
@@ -64,6 +64,10 @@ type Request struct {
id linux.FUSEOpID
hdr *linux.FUSEHeaderIn
data []byte
// payload for this request: extra bytes to write after
// the data slice. Used by FUSE_WRITE.
payload []byte
}
// Response represents an actual response from the server, including the
+12 -1
View File
@@ -152,7 +152,7 @@ func (fd *DeviceFD) readLocked(ctx context.Context, dst usermem.IOSequence, opts
for !fd.queue.Empty() {
req = fd.queue.Front()
if int64(req.hdr.Len) <= dst.NumBytes() {
if int64(req.hdr.Len)+int64(len(req.payload)) <= dst.NumBytes() {
break
}
@@ -191,6 +191,17 @@ func (fd *DeviceFD) readLocked(ctx context.Context, dst usermem.IOSequence, opts
return 0, syserror.EIO
}
if req.hdr.Opcode == linux.FUSE_WRITE {
written, err := dst.DropFirst(n).CopyOut(ctx, req.payload)
if err != nil {
return 0, err
}
if written != len(req.payload) {
return 0, syserror.EIO
}
n += int(written)
}
// Fully done with this req, remove it from the queue.
fd.queue.Remove(req)
if req.hdr.Opcode == linux.FUSE_RELEASE {
+8 -2
View File
@@ -18,6 +18,7 @@ package fuse
import (
"math"
"strconv"
"sync"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
@@ -228,13 +229,18 @@ type inode struct {
kernfs.InodeNotSymlink
kernfs.OrderedChildren
NodeID uint64
dentry kernfs.Dentry
locks vfs.FileLocks
// the owning filesystem. fs is immutable.
fs *filesystem
// metaDataMu protects the metadata of this inode.
metadataMu sync.Mutex
NodeID uint64
locks vfs.FileLocks
// size of the file.
size uint64
+90
View File
@@ -150,3 +150,93 @@ func (fs *filesystem) ReadCallback(ctx context.Context, fd *regularFileFD, off u
fs.conn.mu.Unlock()
}
}
// Write sends FUSE_WRITE requests and return the bytes
// written according to the response.
//
// Preconditions: len(data) == size.
func (fs *filesystem) Write(ctx context.Context, fd *regularFileFD, off uint64, size uint32, data []byte) (uint32, error) {
t := kernel.TaskFromContext(ctx)
if t == nil {
log.Warningf("fusefs.Read: couldn't get kernel task from context")
return 0, syserror.EINVAL
}
// One request cannnot exceed either maxWrite or maxPages.
maxWrite := uint32(fs.conn.maxPages) << usermem.PageShift
if maxWrite > fs.conn.maxWrite {
maxWrite = fs.conn.maxWrite
}
// Reuse the same struct for unmarshalling to avoid unnecessary memory allocation.
in := linux.FUSEWriteIn{
Fh: fd.Fh,
// TODO(gvisor.dev/issue/3245): file lock
LockOwner: 0,
// TODO(gvisor.dev/issue/3245): |= linux.FUSE_READ_LOCKOWNER
// TODO(gvisor.dev/issue/3237): |= linux.FUSE_WRITE_CACHE (not added yet)
WriteFlags: 0,
Flags: fd.statusFlags(),
}
var written uint32
// This loop is intended for fragmented write where the bytes to write is
// larger than either the maxWrite or maxPages or when bigWrites is false.
// Unless a small value for max_write is explicitly used, this loop
// is expected to execute only once for the majority of the writes.
for written < size {
toWrite := size - written
// Limit the write size to one page.
// Note that the bigWrites flag is obsolete,
// latest libfuse always sets it on.
if !fs.conn.bigWrites && toWrite > usermem.PageSize {
toWrite = usermem.PageSize
}
// Limit the write size to maxWrite.
if toWrite > maxWrite {
toWrite = maxWrite
}
in.Offset = off + uint64(written)
in.Size = toWrite
req, err := fs.conn.NewRequest(auth.CredentialsFromContext(ctx), uint32(t.ThreadID()), fd.inode().NodeID, linux.FUSE_WRITE, &in)
if err != nil {
return 0, err
}
req.payload = data[written : written+toWrite]
// TODO(gvisor.dev/issue/3247): support async write.
res, err := fs.conn.Call(t, req)
if err != nil {
return 0, err
}
if err := res.Error(); err != nil {
return 0, err
}
out := linux.FUSEWriteOut{}
if err := res.UnmarshalPayload(&out); err != nil {
return 0, err
}
// Write more than requested? EIO.
if out.Size > toWrite {
return 0, syserror.EIO
}
written += out.Size
// Break if short write. Not necessarily an error.
if out.Size != toWrite {
break
}
}
return written, nil
}
+105
View File
@@ -123,3 +123,108 @@ func (fd *regularFileFD) Read(ctx context.Context, dst usermem.IOSequence, opts
fd.offMu.Unlock()
return n, err
}
// PWrite implements vfs.FileDescriptionImpl.PWrite.
func (fd *regularFileFD) PWrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (int64, error) {
n, _, err := fd.pwrite(ctx, src, offset, opts)
return n, err
}
// Write implements vfs.FileDescriptionImpl.Write.
func (fd *regularFileFD) Write(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) {
fd.offMu.Lock()
n, off, err := fd.pwrite(ctx, src, fd.off, opts)
fd.off = off
fd.offMu.Unlock()
return n, err
}
// pwrite returns the number of bytes written, final offset and error. The
// final offset should be ignored by PWrite.
func (fd *regularFileFD) pwrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (written, finalOff int64, err error) {
if offset < 0 {
return 0, offset, syserror.EINVAL
}
// Check that flags are supported.
//
// TODO(gvisor.dev/issue/2601): Support select preadv2 flags.
if opts.Flags&^linux.RWF_HIPRI != 0 {
return 0, offset, syserror.EOPNOTSUPP
}
inode := fd.inode()
inode.metadataMu.Lock()
defer inode.metadataMu.Unlock()
// If the file is opened with O_APPEND, update offset to file size.
// Note: since our Open() implements the interface of kernfs,
// and kernfs currently does not support O_APPEND, this will never
// be true before we switch out from kernfs.
if fd.vfsfd.StatusFlags()&linux.O_APPEND != 0 {
// Locking inode.metadataMu is sufficient for reading size
offset = int64(inode.size)
}
srclen := src.NumBytes()
if srclen > math.MaxUint32 {
// FUSE only supports uint32 for size.
// Overflow.
return 0, offset, syserror.EINVAL
}
if end := offset + srclen; end < offset {
// Overflow.
return 0, offset, syserror.EINVAL
}
srclen, err = vfs.CheckLimit(ctx, offset, srclen)
if err != nil {
return 0, offset, err
}
if srclen == 0 {
// Return before causing any side effects.
return 0, offset, nil
}
src = src.TakeFirst64(srclen)
// TODO(gvisor.dev/issue/3237): Add cache support:
// buffer cache. Ideally we write from src to our buffer cache first.
// The slice passed to fs.Write() should be a slice from buffer cache.
data := make([]byte, srclen)
// Reason for making a copy here: connection.Call() blocks on kerneltask,
// which in turn acquires mm.activeMu lock. Functions like CopyInTo() will
// attemp to acquire the mm.activeMu lock as well -> deadlock.
// We must finish reading from the userspace memory before
// t.Block() deactivates it.
cp, err := src.CopyIn(ctx, data)
if err != nil {
return 0, offset, err
}
if int64(cp) != srclen {
return 0, offset, syserror.EIO
}
n, err := fd.inode().fs.Write(ctx, fd, uint64(offset), uint32(srclen), data)
if err != nil {
return 0, offset, err
}
if n == 0 {
// We have checked srclen != 0 previously.
// If err == nil, then it's a short write and we return EIO.
return 0, offset, syserror.EIO
}
written = int64(n)
finalOff = offset + written
if finalOff > int64(inode.size) {
atomic.StoreUint64(&inode.size, uint64(finalOff))
atomic.AddUint64(&inode.fs.conn.attributeVersion, 1)
}
return
}
+5
View File
@@ -42,6 +42,11 @@ syscall_test(
test = "//test/fuse/linux:read_test",
)
syscall_test(
fuse = "True",
test = "//test/fuse/linux:write_test",
)
syscall_test(
fuse = "True",
test = "//test/fuse/linux:rmdir_test",
+13
View File
@@ -154,6 +154,19 @@ cc_binary(
],
)
cc_binary(
name = "write_test",
testonly = 1,
srcs = ["write_test.cc"],
deps = [
gtest,
":fuse_base",
"//test/util:fuse_util",
"//test/util:test_main",
"//test/util:test_util",
],
)
cc_binary(
name = "create_test",
testonly = 1,
+6 -7
View File
@@ -164,7 +164,8 @@ void FuseTest::UnmountFuse() {
}
// Consumes the first FUSE request and returns the corresponding PosixError.
PosixError FuseTest::ServerConsumeFuseInit() {
PosixError FuseTest::ServerConsumeFuseInit(
const struct fuse_init_out* out_payload) {
std::vector<char> buf(FUSE_MIN_READ_BUFFER);
RETURN_ERROR_IF_SYSCALL_FAIL(
RetryEINTR(read)(dev_fd_, buf.data(), buf.size()));
@@ -176,10 +177,8 @@ PosixError FuseTest::ServerConsumeFuseInit() {
};
// Returns a fake fuse_init_out with 7.0 version to avoid ECONNREFUSED
// error in the initialization of FUSE connection.
struct fuse_init_out out_payload = {
.major = 7,
};
auto iov_out = FuseGenerateIovecs(out_header, out_payload);
auto iov_out = FuseGenerateIovecs(
out_header, *const_cast<struct fuse_init_out*>(out_payload));
RETURN_ERROR_IF_SYSCALL_FAIL(
RetryEINTR(writev)(dev_fd_, iov_out.data(), iov_out.size()));
@@ -244,7 +243,7 @@ void FuseTest::ServerFuseLoop() {
// becomes testing thread and the child thread becomes the FUSE server running
// in background. These 2 threads are connected via socketpair. sock_[0] is
// opened in testing thread and sock_[1] is opened in the FUSE server.
void FuseTest::SetUpFuseServer() {
void FuseTest::SetUpFuseServer(const struct fuse_init_out* payload) {
ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sock_), SyscallSucceeds());
switch (fork()) {
@@ -261,7 +260,7 @@ void FuseTest::SetUpFuseServer() {
// Begin child thread, i.e. the FUSE server.
ASSERT_THAT(close(sock_[0]), SyscallSucceeds());
ServerCompleteWith(ServerConsumeFuseInit().ok());
ServerCompleteWith(ServerConsumeFuseInit(payload).ok());
ServerFuseLoop();
_exit(0);
}
+5 -2
View File
@@ -33,6 +33,8 @@ namespace testing {
constexpr char kMountOpts[] = "rootmode=755,user_id=0,group_id=0";
constexpr struct fuse_init_out kDefaultFUSEInitOutPayload = {.major = 7};
// Internal commands used to communicate between testing thread and the FUSE
// server. See test/fuse/README.md for further detail.
enum class FuseTestCmd {
@@ -171,7 +173,8 @@ class FuseTest : public ::testing::Test {
void MountFuse(const char* mountOpts = kMountOpts);
// Creates a socketpair for communication and forks FUSE server.
void SetUpFuseServer();
void SetUpFuseServer(
const struct fuse_init_out* payload = &kDefaultFUSEInitOutPayload);
// Unmounts the mountpoint of the FUSE server.
void UnmountFuse();
@@ -194,7 +197,7 @@ class FuseTest : public ::testing::Test {
// Consumes the first FUSE request when mounting FUSE. Replies with a
// response with empty payload.
PosixError ServerConsumeFuseInit();
PosixError ServerConsumeFuseInit(const struct fuse_init_out* payload);
// A command switch that dispatch different FuseTestCmd to its handler.
void ServerHandleCommand();
+303
View File
@@ -0,0 +1,303 @@
// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <errno.h>
#include <fcntl.h>
#include <linux/fuse.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "test/fuse/linux/fuse_base.h"
#include "test/util/fuse_util.h"
#include "test/util/test_util.h"
namespace gvisor {
namespace testing {
namespace {
class WriteTest : public FuseTest {
void SetUp() override {
FuseTest::SetUp();
test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
}
// TearDown overrides the parent's function
// to skip checking the unconsumed release request at the end.
void TearDown() override { UnmountFuse(); }
protected:
const std::string test_file_ = "test_file";
const mode_t test_file_mode_ = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
const uint64_t test_fh_ = 1;
const uint32_t open_flag_ = O_RDWR;
std::string test_file_path_;
PosixErrorOr<FileDescriptor> OpenTestFile(const std::string &path,
uint64_t size = 512) {
SetServerInodeLookup(test_file_, test_file_mode_, size);
struct fuse_out_header out_header_open = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
};
struct fuse_open_out out_payload_open = {
.fh = test_fh_,
.open_flags = open_flag_,
};
auto iov_out_open = FuseGenerateIovecs(out_header_open, out_payload_open);
SetServerResponse(FUSE_OPEN, iov_out_open);
auto res = Open(path.c_str(), open_flag_);
if (res.ok()) {
SkipServerActualRequest();
}
return res;
}
};
class WriteTestSmallMaxWrite : public WriteTest {
void SetUp() override {
MountFuse();
SetUpFuseServer(&fuse_init_payload);
test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
}
protected:
const static uint32_t max_write_ = 4096;
constexpr static struct fuse_init_out fuse_init_payload = {
.major = 7,
.max_write = max_write_,
};
const uint32_t size_fragment = max_write_;
};
TEST_F(WriteTest, WriteNormal) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
// Prepare for the write.
const int n_write = 10;
struct fuse_out_header out_header_write = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
};
struct fuse_write_out out_payload_write = {
.size = n_write,
};
auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
SetServerResponse(FUSE_WRITE, iov_out_write);
// Issue the write.
std::vector<char> buf(n_write);
RandomizeBuffer(buf.data(), buf.size());
EXPECT_THAT(write(fd.get(), buf.data(), n_write),
SyscallSucceedsWithValue(n_write));
// Check the write request.
struct fuse_in_header in_header_write;
struct fuse_write_in in_payload_write;
std::vector<char> payload_buf(n_write);
auto iov_in_write =
FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
GetServerActualRequest(iov_in_write);
EXPECT_EQ(in_payload_write.fh, test_fh_);
EXPECT_EQ(in_header_write.len,
sizeof(in_header_write) + sizeof(in_payload_write));
EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
EXPECT_EQ(in_payload_write.offset, 0);
EXPECT_EQ(in_payload_write.size, n_write);
EXPECT_EQ(buf, payload_buf);
}
TEST_F(WriteTest, WriteShort) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
// Prepare for the write.
const int n_write = 10, n_written = 5;
struct fuse_out_header out_header_write = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
};
struct fuse_write_out out_payload_write = {
.size = n_written,
};
auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
SetServerResponse(FUSE_WRITE, iov_out_write);
// Issue the write.
std::vector<char> buf(n_write);
RandomizeBuffer(buf.data(), buf.size());
EXPECT_THAT(write(fd.get(), buf.data(), n_write),
SyscallSucceedsWithValue(n_written));
// Check the write request.
struct fuse_in_header in_header_write;
struct fuse_write_in in_payload_write;
std::vector<char> payload_buf(n_write);
auto iov_in_write =
FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
GetServerActualRequest(iov_in_write);
EXPECT_EQ(in_payload_write.fh, test_fh_);
EXPECT_EQ(in_header_write.len,
sizeof(in_header_write) + sizeof(in_payload_write));
EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
EXPECT_EQ(in_payload_write.offset, 0);
EXPECT_EQ(in_payload_write.size, n_write);
EXPECT_EQ(buf, payload_buf);
}
TEST_F(WriteTest, WriteShortZero) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
// Prepare for the write.
const int n_write = 10;
struct fuse_out_header out_header_write = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
};
struct fuse_write_out out_payload_write = {
.size = 0,
};
auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
SetServerResponse(FUSE_WRITE, iov_out_write);
// Issue the write.
std::vector<char> buf(n_write);
RandomizeBuffer(buf.data(), buf.size());
EXPECT_THAT(write(fd.get(), buf.data(), n_write), SyscallFailsWithErrno(EIO));
// Check the write request.
struct fuse_in_header in_header_write;
struct fuse_write_in in_payload_write;
std::vector<char> payload_buf(n_write);
auto iov_in_write =
FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
GetServerActualRequest(iov_in_write);
EXPECT_EQ(in_payload_write.fh, test_fh_);
EXPECT_EQ(in_header_write.len,
sizeof(in_header_write) + sizeof(in_payload_write));
EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
EXPECT_EQ(in_payload_write.offset, 0);
EXPECT_EQ(in_payload_write.size, n_write);
EXPECT_EQ(buf, payload_buf);
}
TEST_F(WriteTest, WriteZero) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
// Issue the write.
std::vector<char> buf(0);
EXPECT_THAT(write(fd.get(), buf.data(), 0), SyscallSucceedsWithValue(0));
}
TEST_F(WriteTest, PWrite) {
const int file_size = 512;
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, file_size));
// Prepare for the write.
const int n_write = 10;
struct fuse_out_header out_header_write = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
};
struct fuse_write_out out_payload_write = {
.size = n_write,
};
auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
SetServerResponse(FUSE_WRITE, iov_out_write);
// Issue the write.
std::vector<char> buf(n_write);
RandomizeBuffer(buf.data(), buf.size());
const int offset_write = file_size >> 1;
EXPECT_THAT(pwrite(fd.get(), buf.data(), n_write, offset_write),
SyscallSucceedsWithValue(n_write));
// Check the write request.
struct fuse_in_header in_header_write;
struct fuse_write_in in_payload_write;
std::vector<char> payload_buf(n_write);
auto iov_in_write =
FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
GetServerActualRequest(iov_in_write);
EXPECT_EQ(in_payload_write.fh, test_fh_);
EXPECT_EQ(in_header_write.len,
sizeof(in_header_write) + sizeof(in_payload_write));
EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
EXPECT_EQ(in_payload_write.offset, offset_write);
EXPECT_EQ(in_payload_write.size, n_write);
EXPECT_EQ(buf, payload_buf);
}
TEST_F(WriteTestSmallMaxWrite, WriteSmallMaxWrie) {
const int n_fragment = 10;
const int n_write = size_fragment * n_fragment;
auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, n_write));
// Prepare for the write.
struct fuse_out_header out_header_write = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
};
struct fuse_write_out out_payload_write = {
.size = size_fragment,
};
auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
for (int i = 0; i < n_fragment; ++i) {
SetServerResponse(FUSE_WRITE, iov_out_write);
}
// Issue the write.
std::vector<char> buf(n_write);
RandomizeBuffer(buf.data(), buf.size());
EXPECT_THAT(write(fd.get(), buf.data(), n_write),
SyscallSucceedsWithValue(n_write));
ASSERT_EQ(GetServerNumUnsentResponses(), 0);
ASSERT_EQ(GetServerNumUnconsumedRequests(), n_fragment);
// Check the write request.
struct fuse_in_header in_header_write;
struct fuse_write_in in_payload_write;
std::vector<char> payload_buf(size_fragment);
auto iov_in_write =
FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
for (int i = 0; i < n_fragment; ++i) {
GetServerActualRequest(iov_in_write);
EXPECT_EQ(in_payload_write.fh, test_fh_);
EXPECT_EQ(in_header_write.len,
sizeof(in_header_write) + sizeof(in_payload_write));
EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
EXPECT_EQ(in_payload_write.offset, i * size_fragment);
EXPECT_EQ(in_payload_write.size, size_fragment);
auto it = buf.begin() + i * size_fragment;
EXPECT_EQ(std::vector<char>(it, it + size_fragment), payload_buf);
}
}
} // namespace
} // namespace testing
} // namespace gvisor