// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fcntl.h>
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "nacl_io/fuse.h"
#include "nacl_io/fusefs/fuse_fs.h"
#include "nacl_io/kernel_handle.h"
#include "nacl_io/kernel_intercept.h"
#include "nacl_io/kernel_proxy.h"
#include "nacl_io/ostime.h"
using namespace nacl_io;
namespace {
class FuseFsForTesting : public FuseFs {
public:
explicit FuseFsForTesting(fuse_operations* fuse_ops) {
FsInitArgs args;
args.fuse_ops = fuse_ops;
EXPECT_EQ(0, Init(args));
}
};
// Implementation of a simple flat memory filesystem.
struct File {
File() : mode(0666) { memset(×, 0, sizeof(times)); }
std::string name;
std::vector<uint8_t> data;
mode_t mode;
timespec times[2];
};
typedef std::vector<File> Files;
Files g_files;
bool IsValidPath(const char* path) {
if (path == NULL)
return false;
if (strlen(path) <= 1)
return false;
if (path[0] != '/')
return false;
return true;
}
File* FindFile(const char* path) {
if (!IsValidPath(path))
return NULL;
for (Files::iterator iter = g_files.begin(); iter != g_files.end(); ++iter) {
if (iter->name == &path[1])
return &*iter;
}
return NULL;
}
int testfs_getattr(const char* path, struct stat* stbuf) {
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
return 0;
}
if (strcmp(path, "/foo") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
return 0;
}
File* file = FindFile(path);
if (file == NULL)
return -ENOENT;
stbuf->st_mode = S_IFREG | file->mode;
stbuf->st_size = file->data.size();
stbuf->st_atime = file->times[0].tv_sec;
stbuf->st_atimensec = file->times[0].tv_nsec;
stbuf->st_mtime = file->times[1].tv_sec;
stbuf->st_mtimensec = file->times[1].tv_nsec;
return 0;
}
int testfs_readdir(const char* path,
void* buf,
fuse_fill_dir_t filler,
off_t offset,
struct fuse_file_info*) {
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0);
filler(buf, "..", NULL, 0);
for (Files::iterator iter = g_files.begin(); iter != g_files.end(); ++iter) {
filler(buf, iter->name.c_str(), NULL, 0);
}
return 0;
}
int testfs_create(const char* path, mode_t mode, struct fuse_file_info* fi) {
if (!IsValidPath(path))
return -ENOENT;
File* file = FindFile(path);
if (file != NULL) {
if (fi->flags & O_EXCL)
return -EEXIST;
} else {
g_files.push_back(File());
file = &g_files.back();
file->name = &path[1]; // Skip initial /
}
file->mode = mode;
return 0;
}
int testfs_open(const char* path, struct fuse_file_info*) {
// open is only called to open an existing file, otherwise create is
// called. We don't need to do any additional work here, the path will be
// passed to any other operations.
return FindFile(path) != NULL;
}
int testfs_read(const char* path,
char* buf,
size_t size,
off_t offset,
struct fuse_file_info* fi) {
File* file = FindFile(path);
if (file == NULL)
return -ENOENT;
size_t filesize = file->data.size();
// Trying to read past the end of the file.
if (offset >= filesize)
return 0;
if (offset + size > filesize)
size = filesize - offset;
memcpy(buf, file->data.data() + offset, size);
return size;
}
int testfs_write(const char* path,
const char* buf,
size_t size,
off_t offset,
struct fuse_file_info*) {
File* file = FindFile(path);
if (file == NULL)
return -ENOENT;
size_t filesize = file->data.size();
if (offset + size > filesize)
file->data.resize(offset + size);
memcpy(file->data.data() + offset, buf, size);
return size;
}
int testfs_utimens(const char* path, const struct timespec times[2]) {
File* file = FindFile(path);
if (file == NULL)
return -ENOENT;
file->times[0] = times[0];
file->times[1] = times[1];
return 0;
}
int testfs_chmod(const char* path, mode_t mode) {
File* file = FindFile(path);
if (file == NULL)
return -ENOENT;
file->mode = mode;
return 0;
}
const char hello_world[] = "Hello, World!\n";
fuse_operations g_fuse_operations = {
0, // flag_nopath
0, // flag_reserved
testfs_getattr, // getattr
NULL, // readlink
NULL, // mknod
NULL, // mkdir
NULL, // unlink
NULL, // rmdir
NULL, // symlink
NULL, // rename
NULL, // link
testfs_chmod, // chmod
NULL, // chown
NULL, // truncate
testfs_open, // open
testfs_read, // read
testfs_write, // write
NULL, // statfs
NULL, // flush
NULL, // release
NULL, // fsync
NULL, // setxattr
NULL, // getxattr
NULL, // listxattr
NULL, // removexattr
NULL, // opendir
testfs_readdir, // readdir
NULL, // releasedir
NULL, // fsyncdir
NULL, // init
NULL, // destroy
NULL, // access
testfs_create, // create
NULL, // ftruncate
NULL, // fgetattr
NULL, // lock
testfs_utimens, // utimens
NULL, // bmap
NULL, // ioctl
NULL, // poll
NULL, // write_buf
NULL, // read_buf
NULL, // flock
NULL, // fallocate
};
class FuseFsTest : public ::testing::Test {
public:
FuseFsTest();
void SetUp();
protected:
FuseFsForTesting fs_;
};
FuseFsTest::FuseFsTest() : fs_(&g_fuse_operations) {}
void FuseFsTest::SetUp() {
// Reset the filesystem.
g_files.clear();
// Add a built-in file.
size_t hello_len = strlen(hello_world);
File hello;
hello.name = "hello";
hello.data.resize(hello_len);
memcpy(hello.data.data(), hello_world, hello_len);
g_files.push_back(hello);
}
} // namespace
TEST_F(FuseFsTest, OpenAndRead) {
ScopedNode node;
ASSERT_EQ(0, fs_.Open(Path("/hello"), O_RDONLY, &node));
char buffer[15] = {0};
int bytes_read = 0;
HandleAttr attr;
ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read));
ASSERT_EQ(strlen(hello_world), bytes_read);
ASSERT_STREQ(hello_world, buffer);
// Try to read past the end of the file.
attr.offs = strlen(hello_world) - 7;
ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read));
ASSERT_EQ(7, bytes_read);
ASSERT_STREQ("World!\n", buffer);
}
TEST_F(FuseFsTest, CreateWithMode) {
ScopedNode node;
struct stat statbuf;
ASSERT_EQ(0, fs_.OpenWithMode(Path("/hello"),
O_RDWR | O_CREAT, 0723, &node));
EXPECT_EQ(0, node->GetStat(&statbuf));
EXPECT_TRUE(S_ISREG(statbuf.st_mode));
EXPECT_EQ(0723, statbuf.st_mode & S_MODEBITS);
}
TEST_F(FuseFsTest, CreateAndWrite) {
ScopedNode node;
ASSERT_EQ(0, fs_.Open(Path("/foobar"), O_RDWR | O_CREAT, &node));
HandleAttr attr;
const char message[] = "Something interesting";
int bytes_written;
ASSERT_EQ(0, node->Write(attr, &message[0], strlen(message), &bytes_written));
ASSERT_EQ(bytes_written, strlen(message));
// Now try to read the data back.
char buffer[40] = {0};
int bytes_read = 0;
ASSERT_EQ(0, node->Read(attr, &buffer[0], sizeof(buffer), &bytes_read));
ASSERT_EQ(strlen(message), bytes_read);
ASSERT_STREQ(message, buffer);
}
TEST_F(FuseFsTest, GetStat) {
struct stat statbuf;
ScopedNode node;
ASSERT_EQ(0, fs_.Open(Path("/hello"), O_RDONLY, &node));
EXPECT_EQ(0, node->GetStat(&statbuf));
EXPECT_TRUE(S_ISREG(statbuf.st_mode));
EXPECT_EQ(0666, statbuf.st_mode & S_MODEBITS);
EXPECT_EQ(strlen(hello_world), statbuf.st_size);
ASSERT_EQ(0, fs_.Open(Path("/"), O_RDONLY, &node));
EXPECT_EQ(0, node->GetStat(&statbuf));
EXPECT_TRUE(S_ISDIR(statbuf.st_mode));
EXPECT_EQ(0755, statbuf.st_mode & S_MODEBITS);
// Create a file and stat.
ASSERT_EQ(0, fs_.Open(Path("/foobar"), O_RDWR | O_CREAT, &node));
EXPECT_EQ(0, node->GetStat(&statbuf));
EXPECT_TRUE(S_ISREG(statbuf.st_mode));
EXPECT_EQ(0666, statbuf.st_mode & S_MODEBITS);
EXPECT_EQ(0, statbuf.st_size);
}
TEST_F(FuseFsTest, GetDents) {
ScopedNode root;
ASSERT_EQ(0, fs_.Open(Path("/"), O_RDONLY, &root));
struct dirent entries[4];
int bytes_read;
// Try reading everything.
ASSERT_EQ(0, root->GetDents(0, &entries[0], sizeof(entries), &bytes_read));
ASSERT_EQ(3 * sizeof(dirent), bytes_read);
EXPECT_STREQ(".", entries[0].d_name);
EXPECT_STREQ("..", entries[1].d_name);
EXPECT_STREQ("hello", entries[2].d_name);
// Try reading from an offset.
memset(&entries, 0, sizeof(entries));
ASSERT_EQ(0, root->GetDents(sizeof(dirent), &entries[0], 2 * sizeof(dirent),
&bytes_read));
ASSERT_EQ(2 * sizeof(dirent), bytes_read);
EXPECT_STREQ("..", entries[0].d_name);
EXPECT_STREQ("hello", entries[1].d_name);
// Add a file and read again.
ScopedNode node;
ASSERT_EQ(0, fs_.Open(Path("/foobar"), O_RDWR | O_CREAT, &node));
ASSERT_EQ(0, root->GetDents(0, &entries[0], sizeof(entries), &bytes_read));
ASSERT_EQ(4 * sizeof(dirent), bytes_read);
EXPECT_STREQ(".", entries[0].d_name);
EXPECT_STREQ("..", entries[1].d_name);
EXPECT_STREQ("hello", entries[2].d_name);
EXPECT_STREQ("foobar", entries[3].d_name);
}
TEST_F(FuseFsTest, Utimens) {
struct stat statbuf;
ScopedNode node;
struct timespec times[2];
times[0].tv_sec = 1000;
times[0].tv_nsec = 2000;
times[1].tv_sec = 3000;
times[1].tv_nsec = 4000;
ASSERT_EQ(0, fs_.Open(Path("/hello"), O_RDONLY, &node));
EXPECT_EQ(0, node->Futimens(times));
EXPECT_EQ(0, node->GetStat(&statbuf));
EXPECT_EQ(times[0].tv_sec, statbuf.st_atime);
EXPECT_EQ(times[0].tv_nsec, statbuf.st_atimensec);
EXPECT_EQ(times[1].tv_sec, statbuf.st_mtime);
EXPECT_EQ(times[1].tv_nsec, statbuf.st_mtimensec);
}
TEST_F(FuseFsTest, Fchmod) {
struct stat statbuf;
ScopedNode node;
ASSERT_EQ(0, fs_.Open(Path("/hello"), O_RDONLY, &node));
ASSERT_EQ(0, node->GetStat(&statbuf));
EXPECT_EQ(0666, statbuf.st_mode & S_MODEBITS);
ASSERT_EQ(0, node->Fchmod(0777));
ASSERT_EQ(0, node->GetStat(&statbuf));
EXPECT_EQ(0777, statbuf.st_mode & S_MODEBITS);
}
namespace {
class KernelProxyFuseTest : public ::testing::Test {
public:
KernelProxyFuseTest() {}
void SetUp();
void TearDown();
private:
KernelProxy kp_;
};
void KernelProxyFuseTest::SetUp() {
ASSERT_EQ(0, ki_push_state_for_testing());
ASSERT_EQ(0, ki_init(&kp_));
// Register a fuse filesystem.
nacl_io_register_fs_type("flatfs", &g_fuse_operations);
// Unmount the passthrough FS and mount our fuse filesystem.
EXPECT_EQ(0, kp_.umount("/"));
EXPECT_EQ(0, kp_.mount("", "/", "flatfs", 0, NULL));
}
void KernelProxyFuseTest::TearDown() {
nacl_io_unregister_fs_type("flatfs");
ki_uninit();
}
} // namespace
TEST_F(KernelProxyFuseTest, Basic) {
// Write a file.
int fd = ki_open("/hello", O_WRONLY | O_CREAT, 0777);
ASSERT_GT(fd, -1);
ASSERT_EQ(sizeof(hello_world),
ki_write(fd, hello_world, sizeof(hello_world)));
EXPECT_EQ(0, ki_close(fd));
// Then read it back in.
fd = ki_open("/hello", O_RDONLY, 0);
ASSERT_GT(fd, -1);
char buffer[30];
memset(buffer, 0, sizeof(buffer));
ASSERT_EQ(sizeof(hello_world), ki_read(fd, buffer, sizeof(buffer)));
EXPECT_STREQ(hello_world, buffer);
EXPECT_EQ(0, ki_close(fd));
}
TEST_F(KernelProxyFuseTest, Chdir) {
ASSERT_EQ(0, ki_chdir("/foo"));
}