blob: 3820a00a54aca4711208b581c3a03c503a451d6f [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fusebox/file_system_fake.h"
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include <base/check.h>
#include <base/check_op.h>
#include <base/logging.h>
#include <base/no_destructor.h>
#include <base/numerics/safe_conversions.h>
#include <base/strings/string_piece.h>
#include "fusebox/fuse_file_handles.h"
#include "fusebox/fuse_path_inodes.h"
#include "fusebox/fuse_request.h"
#include "fusebox/make_stat.h"
#include "fusebox/util.h"
namespace fusebox {
static auto& GetInodeTable() {
static base::NoDestructor<InodeTable> inode_table;
return *inode_table;
}
class FakeFileEntry {
public:
// Creates FakeFileEntry for InodeTable |node| with |mode|.
static std::pair<ino_t, FakeFileEntry> Create(Node* node, mode_t mode) {
CHECK(node && node->ino) << __func__ << " invalid node";
return {node->ino, FakeFileEntry(node, mode)};
}
// Returns entry node.
Node* node() const { return node_; }
// Returns entry data size: 0 unless the entry is S_ISREG.
size_t GetSize() const { return data_.size(); }
// Sets an S_ISREG entry data to |size|. Returns the size.
size_t SetSize(size_t size) {
CheckEntryIsReg(__func__);
data_.resize(size);
return data_.size();
}
// Returns an S_ISREG entry data at |off| of length |size|.
base::StringPiece GetDataSlice(off_t off, size_t size) const {
CheckEntryIsReg(__func__);
size_t length = data_.size();
if (off < 0 || off >= length)
return {}; // Out-of-bounds: returns empty slice.
length -= size_t(off);
return {data_.data() + off, std::min(length, size)};
}
// Sets an S_ISREG entry data at |off| to |buffer| of |size|. Returns
// number of bytes written (count).
size_t SetData(const char* buffer, size_t size, off_t off) {
CheckEntryIsReg(__func__);
if (!buffer || !size)
return 0;
CHECK_GE(off, 0);
CHECK_EQ(off, size_t(off));
size_t length = base::saturated_cast<size_t>(size_t(off) + size);
if (length > data_.size())
data_.resize(length);
size_t count = std::min(length - size_t(off), size);
std::memcpy(data_.data() + size_t(off), buffer, count);
return count;
}
private:
// Constructs an entry for InodeTable |node| with |mode|.
FakeFileEntry(Node* node, mode_t mode) : node_(node), mode_(mode) {
CHECK(IsAllowedStatMode(mode_)) << "invalid mode " << mode;
}
// Disallow access to entry that are not S_ISREG |mode_|.
void CheckEntryIsReg(const char* method) const {
LOG_IF(FATAL, !S_ISREG(mode_)) << method << " entry not S_IFREG";
}
// Node in the InodeTable: not owned.
Node* node_ = nullptr;
// Node's associated stat(2).st_mode.
mode_t mode_ = 0;
// File data: always empty, except for an S_ISREG entry.
std::vector<char> data_;
};
FileSystemFake::FileSystemFake() = default;
FileSystemFake::~FileSystemFake() = default;
void FileSystemFake::Init(void* userdata, struct fuse_conn_info*) {
LOG(INFO) << "Init";
const auto time_now = std::time(nullptr);
Node* root = GetInodeTable().Lookup(1);
struct stat root_stat = MakeTimeStat(S_IFDIR | 0777, time_now);
root_stat = MakeStat(root->ino, root_stat, read_only_);
GetInodeTable().SetStat(root->ino, root_stat);
ShowStat(root_stat, root->name);
files_.insert(FakeFileEntry::Create(root, root_stat.st_mode));
CHECK_EQ(0, files_.find(root->ino)->second.GetSize());
const char* file_data = "hello world\r\n";
const auto data_size = strlen(file_data);
Node* hello = GetInodeTable().Create(1, "hello");
struct stat hello_stat = MakeTimeStat(S_IFREG | 0777, time_now);
hello_stat.st_size = data_size;
hello_stat = MakeStat(hello->ino, hello_stat, read_only_);
GetInodeTable().SetStat(hello->ino, hello_stat);
ShowStat(hello_stat, hello->name);
files_.insert(FakeFileEntry::Create(hello, hello_stat.st_mode));
files_.find(hello->ino)->second.SetData(file_data, data_size, 0);
CHECK_EQ(data_size, files_.find(hello->ino)->second.GetSize());
CHECK(userdata) << "FileSystem (userdata) is required";
}
void FileSystemFake::Lookup(std::unique_ptr<EntryRequest> request,
ino_t parent,
const char* name) {
LOG(INFO) << "Lookup parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(parent, name);
if (!node) {
PLOG(ERROR) << " lookup error";
request->ReplyError(errno);
return;
}
auto it = files_.find(node->ino);
if (it == files_.end()) {
LOG(ERROR) << " lookup files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(node->ino, &stat));
CHECK_EQ(stat.st_ino, node->ino);
stat.st_size = it->second.GetSize();
GetInodeTable().SetStat(node->ino, stat);
const double kEntryTimeoutSeconds = 5.0;
fuse_entry_param entry = {0};
entry.ino = static_cast<fuse_ino_t>(node->ino);
entry.attr = stat;
entry.attr_timeout = kEntryTimeoutSeconds;
entry.entry_timeout = kEntryTimeoutSeconds;
LOG(INFO) << " found ino " << node->ino;
request->ReplyEntry(entry);
}
void FileSystemFake::GetAttr(std::unique_ptr<AttrRequest> request, ino_t ino) {
LOG(INFO) << "GetAttr ino " << ino;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(ino);
if (!node) {
PLOG(ERROR) << " getattr error";
request->ReplyError(errno);
return;
}
auto it = files_.find(node->ino);
if (it == files_.end()) {
LOG(ERROR) << " getattr files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(node->ino, &stat));
CHECK_EQ(stat.st_ino, node->ino);
stat.st_size = it->second.GetSize();
GetInodeTable().SetStat(node->ino, stat);
const double kStatTimeoutSeconds = 5.0;
request->ReplyAttr(stat, kStatTimeoutSeconds);
}
void FileSystemFake::SetAttr(std::unique_ptr<AttrRequest> request,
ino_t ino,
struct stat* attr,
int to_set) {
LOG(INFO) << "SetAttr ino " << ino << " fh " << request->fh();
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(ino);
if (!node) {
PLOG(ERROR) << " setattr error";
request->ReplyError(errno);
return;
}
auto it = files_.find(ino);
if (it == files_.end()) {
LOG(ERROR) << " setattr files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
// Allow setting file size ftruncate(2), and file times utime(2).
const int kAllowedToSet =
FUSE_SET_ATTR_SIZE | FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME;
constexpr auto allowed_to_set = [](int to_set) {
// ATTR_XTIME_NOW are optional ATTR_XTIME modifiers: mask them.
to_set &= ~(FUSE_SET_ATTR_ATIME_NOW | FUSE_SET_ATTR_MTIME_NOW);
if (to_set & ~kAllowedToSet)
return ENOTSUP;
if (!to_set) // Nothing to_set? error EINVAL.
return EINVAL;
return 0;
};
LOG(INFO) << " to_set " << ToSetFlagsToString(to_set);
if (errno = allowed_to_set(to_set); errno) {
PLOG(ERROR) << " setattr to_set";
request->ReplyError(errno);
return;
}
struct stat stat = {0};
CHECK(GetInodeTable().GetStat(node->ino, &stat));
CHECK_EQ(stat.st_ino, node->ino);
// Set file st_size.
if (to_set & FUSE_SET_ATTR_SIZE) {
const auto size = it->second.GetSize();
stat.st_size = it->second.SetSize(attr->st_size);
LOG(INFO) << " set size " << size << " to " << stat.st_size;
}
// Set st_atime || st_mtime.
if (to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) {
if (to_set & FUSE_SET_ATTR_ATIME_NOW) {
stat.st_atime = std::time(nullptr);
LOG(INFO) << " set atime now " << stat.st_atime;
} else if (to_set & FUSE_SET_ATTR_ATIME) {
stat.st_atime = attr->st_atime;
LOG(INFO) << " set atime " << stat.st_atime;
}
if (to_set & FUSE_SET_ATTR_MTIME_NOW) {
stat.st_mtime = std::time(nullptr);
LOG(INFO) << " set mtime now " << stat.st_mtime;
} else if (to_set & FUSE_SET_ATTR_MTIME) {
stat.st_mtime = attr->st_mtime;
LOG(INFO) << " set mtime " << stat.st_mtime;
}
}
stat.st_size = it->second.GetSize();
GetInodeTable().SetStat(node->ino, stat);
const double kStatTimeoutSeconds = 5.0;
request->ReplyAttr(stat, kStatTimeoutSeconds);
}
void FileSystemFake::MkDir(std::unique_ptr<EntryRequest> request,
ino_t parent,
const char* name,
mode_t mode) {
LOG(INFO) << "MkDir parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
if (read_only_) {
LOG(ERROR) << " mkdir read-only: EACCES";
request->ReplyError(EACCES);
return;
}
Node* node = GetInodeTable().Create(parent, name);
if (!node) {
PLOG(ERROR) << " mkdir error";
request->ReplyError(errno);
return;
}
struct stat stat = MakeTimeStat(S_IFDIR | 0777);
stat = MakeStat(node->ino, stat, read_only_);
GetInodeTable().SetStat(node->ino, stat);
files_.insert(FakeFileEntry::Create(node, stat.st_mode));
const double kEntryTimeoutSeconds = 5.0;
fuse_entry_param entry = {0};
entry.ino = static_cast<fuse_ino_t>(node->ino);
entry.attr = stat;
entry.attr_timeout = kEntryTimeoutSeconds;
entry.entry_timeout = kEntryTimeoutSeconds;
LOG(INFO) << " mkdir ino " << node->ino;
request->ReplyEntry(entry);
}
void FileSystemFake::Unlink(std::unique_ptr<OkRequest> request,
ino_t parent,
const char* name) {
LOG(INFO) << "Unlink parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(parent, name);
if (!node || node->ino <= FUSE_ROOT_ID) {
errno = !node ? errno : EBUSY;
PLOG(ERROR) << " unlink error";
request->ReplyError(errno);
return;
}
auto it = files_.find(node->ino);
if (it == files_.end()) {
LOG(ERROR) << " unlink files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
if (read_only_) {
LOG(ERROR) << " unlink read-only: EACCES";
request->ReplyError(EACCES);
return;
}
bool removed = GetInodeTable().Forget(node->ino, 1);
LOG_IF(FATAL, !removed) << " unlink failed";
files_.erase(it);
request->ReplyOk();
}
void FileSystemFake::RmDir(std::unique_ptr<OkRequest> request,
ino_t parent,
const char* name) {
LOG(INFO) << "RmDir parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(parent, name);
if (!node || node->ino <= FUSE_ROOT_ID) {
errno = !node ? errno : EBUSY;
PLOG(ERROR) << " rmdir error";
request->ReplyError(errno);
return;
}
auto it = files_.find(node->ino);
if (it == files_.end()) {
LOG(ERROR) << " read files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
if (read_only_) {
LOG(ERROR) << " rmdir read-only: EACCES";
request->ReplyError(EACCES);
return;
}
for (const auto& it : files_) {
Node* child = it.second.node();
if (!child || child->parent != node->ino)
continue; // skip: not a child of the node |ino|.
LOG(ERROR) << " rmdir error: ENOTEMPTY";
request->ReplyError(ENOTEMPTY);
return;
}
bool removed = GetInodeTable().Forget(node->ino, 1);
LOG_IF(FATAL, !removed) << " rmdir failed";
files_.erase(it);
request->ReplyOk();
}
void FileSystemFake::Rename(std::unique_ptr<OkRequest> request,
ino_t parent,
const char* name,
ino_t new_parent,
const char* new_name) {
LOG(INFO) << "Rename parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
LOG(ERROR) << " rename not implemented: ENOTSUP";
request->ReplyError(ENOTSUP);
}
void FileSystemFake::OpenDir(std::unique_ptr<OpenRequest> request, ino_t ino) {
LOG(INFO) << "OpenDir ino " << ino;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(ino);
if (!node) {
PLOG(ERROR) << " opendir error";
request->ReplyError(errno);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(node->ino, &stat));
CHECK_EQ(stat.st_ino, node->ino);
if (!S_ISDIR(stat.st_mode)) {
LOG(ERROR) << " opendir error: ENOTDIR";
request->ReplyError(ENOTDIR);
return;
}
LOG(INFO) << " flags " << OpenFlagsToString(request->flags());
if ((request->flags() & O_ACCMODE) != O_RDONLY) {
LOG(ERROR) << " opendir error: EACCES";
request->ReplyError(EACCES);
return;
}
uint64_t handle = fusebox::OpenFile();
readdir_[handle].reset(new DirEntryResponse(node->ino, handle));
LOG(INFO) << " opendir fh " << handle;
request->ReplyOpen(handle);
}
void FileSystemFake::ReadDir(std::unique_ptr<DirEntryRequest> request,
ino_t ino,
off_t off) {
LOG(INFO) << "ReadDir ino " << ino << " off " << off;
if (request->IsInterrupted())
return;
auto it = readdir_.find(request->fh());
if (it == readdir_.end()) {
LOG(ERROR) << " readdir error: EBADF " << request->fh();
request->ReplyError(EBADF);
return;
}
DirEntryResponse* response = it->second.get();
constexpr auto dir_entry = [](ino_t ino, const std::string& name) {
struct stat stat = {0};
CHECK(GetInodeTable().GetStat(ino, &stat));
if (ino > FUSE_ROOT_ID && name == "..") {
Node* node = GetInodeTable().Lookup(ino);
CHECK(node && GetInodeTable().GetStat(node->parent, &stat));
}
return DirEntry{ino, name, stat.st_mode};
};
if (off == 0) {
LOG(INFO) << " readdir fh " << request->fh();
std::vector<DirEntry> entries;
entries.push_back(dir_entry(ino, "."));
entries.push_back(dir_entry(ino, ".."));
for (const auto& it : files_) {
Node* child = it.second.node();
if (!child || child->parent != ino)
continue; // skip: not a child of the node |ino|.
const std::string name = child->name.substr(1);
entries.push_back(dir_entry(child->ino, name));
}
for (const auto& entry : entries)
LOG(INFO) << " entry [" << entry.name << "]";
response->Append(entries, true);
}
response->Append(std::move(request));
}
void FileSystemFake::ReleaseDir(std::unique_ptr<OkRequest> request, ino_t ino) {
LOG(INFO) << "ReleaseDir ino " << ino;
if (request->IsInterrupted())
return;
if (!fusebox::GetFile(request->fh())) {
LOG(ERROR) << " releasedir error: EBADF " << request->fh();
request->ReplyError(EBADF);
return;
}
LOG(INFO) << " releasedir fh " << request->fh();
fusebox::CloseFile(request->fh());
readdir_.erase(request->fh());
request->ReplyOk();
}
void FileSystemFake::Open(std::unique_ptr<OpenRequest> request, ino_t ino) {
LOG(INFO) << "Open ino " << ino;
if (request->IsInterrupted())
return;
Node* node = GetInodeTable().Lookup(ino);
if (!node) {
PLOG(ERROR) << " open error";
request->ReplyError(errno);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(node->ino, &stat));
CHECK_EQ(stat.st_ino, node->ino);
if (S_ISDIR(stat.st_mode)) {
LOG(ERROR) << " open error: EISDIR";
request->ReplyError(EISDIR);
return;
}
LOG(INFO) << " flags " << OpenFlagsToString(request->flags());
if (read_only_ && (request->flags() & O_ACCMODE) != O_RDONLY) {
LOG(ERROR) << " open error: EACCES";
request->ReplyError(EACCES);
return;
}
uint64_t handle = fusebox::OpenFile();
LOG(INFO) << " opened fh " << handle;
request->ReplyOpen(handle);
}
void FileSystemFake::Read(std::unique_ptr<BufferRequest> request,
ino_t ino,
size_t size,
off_t off) {
LOG(INFO) << "Read ino " << ino << " off " << off << " size " << size;
if (request->IsInterrupted())
return;
if (!fusebox::GetFile(request->fh())) {
LOG(ERROR) << " read error: EBADF " << request->fh();
request->ReplyError(EBADF);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(ino, &stat));
CHECK_EQ(stat.st_ino, ino);
if (S_ISDIR(stat.st_mode)) {
LOG(ERROR) << " read error: EISDIR";
request->ReplyError(EISDIR);
return;
}
auto it = files_.find(ino);
if (it == files_.end()) {
LOG(ERROR) << " read files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
LOG(INFO) << " read fh " << request->fh();
auto slice = it->second.GetDataSlice(off, size);
request->ReplyBuffer(slice.data(), slice.size());
}
void FileSystemFake::Write(std::unique_ptr<WriteRequest> request,
ino_t ino,
const char* buf,
size_t size,
off_t off) {
LOG(INFO) << "Write ino " << ino << " off " << off << " size " << size;
if (request->IsInterrupted())
return;
if (!fusebox::GetFile(request->fh())) {
LOG(ERROR) << " write error: EBADF " << request->fh();
request->ReplyError(EBADF);
return;
}
struct stat stat;
CHECK(GetInodeTable().GetStat(ino, &stat));
CHECK_EQ(stat.st_ino, ino);
if (S_ISDIR(stat.st_mode)) {
LOG(ERROR) << " write error: EISDIR";
request->ReplyError(EISDIR);
return;
}
auto it = files_.find(ino);
if (it == files_.end()) {
LOG(ERROR) << " write files map: ENOENT";
request->ReplyError(ENOENT);
return;
}
LOG(INFO) << " write fh " << request->fh();
auto count = it->second.SetData(buf, size, off);
request->ReplyWrite(count);
}
void FileSystemFake::Release(std::unique_ptr<OkRequest> request, ino_t ino) {
LOG(INFO) << "Release ino " << ino;
if (request->IsInterrupted())
return;
if (!fusebox::GetFile(request->fh())) {
LOG(ERROR) << " release error: EBADF " << request->fh();
request->ReplyError(EBADF);
return;
}
LOG(INFO) << " release fh " << request->fh();
fusebox::CloseFile(request->fh());
request->ReplyOk();
}
void FileSystemFake::Create(std::unique_ptr<CreateRequest> request,
ino_t parent,
const char* name,
mode_t mode) {
LOG(INFO) << "Create parent " << parent << " name " << name;
if (request->IsInterrupted())
return;
if (!S_ISREG(mode)) {
LOG(ERROR) << " create mode: EINVAL";
request->ReplyError(EINVAL);
return;
}
if (read_only_) {
LOG(ERROR) << " create error: EACCES";
request->ReplyError(EACCES);
return;
}
Node* node = GetInodeTable().Create(parent, name);
if (!node) {
PLOG(ERROR) << " create error";
request->ReplyError(errno);
return;
}
struct stat stat = MakeTimeStat(S_IFREG | 0777);
stat = MakeStat(node->ino, stat, read_only_);
GetInodeTable().SetStat(node->ino, stat);
files_.insert(FakeFileEntry::Create(node, stat.st_mode));
const double kEntryTimeoutSeconds = 5.0;
fuse_entry_param entry = {0};
entry.ino = static_cast<fuse_ino_t>(node->ino);
entry.attr = stat;
entry.attr_timeout = kEntryTimeoutSeconds;
entry.entry_timeout = kEntryTimeoutSeconds;
LOG(INFO) << " flags " << OpenFlagsToString(request->flags());
uint64_t handle = fusebox::OpenFile();
LOG(INFO) << " create ino " << node->ino << " fh " << handle;
request->ReplyCreate(entry, handle);
}
} // namespace fusebox