Commit 79f7d506 authored by Kenton Varda's avatar Kenton Varda Committed by Kenton Varda

Implement filesystem API -> POSIX abstraction.

parent b99fcdd4
// Copyright (c) 2016 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include "filesystem.h"
#include "test.h"
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
namespace kj {
namespace {
static Own<File> newTempFile() {
char filename[] = "/var/tmp/kj-filesystem-test.XXXXXX";
int fd;
KJ_SYSCALL(fd = mkstemp(filename));
KJ_DEFER(KJ_SYSCALL(unlink(filename)));
return newDiskFile(AutoCloseFd(fd));
}
class TempDir {
public:
TempDir(): filename(heapString("/var/tmp/kj-filesystem-test.XXXXXX")) {
if (mkdtemp(filename.begin()) == nullptr) {
KJ_FAIL_SYSCALL("mkdtemp", errno, filename);
}
}
Own<Directory> get() {
int fd;
KJ_SYSCALL(fd = open(filename.cStr(), O_RDONLY));
return newDiskDirectory(AutoCloseFd(fd));
}
~TempDir() noexcept(false) {
recursiveDelete(filename);
}
private:
String filename;
static void recursiveDelete(StringPtr path) {
// Recursively delete the temp dir, verifying that no .kj-tmp. files were left over.
{
DIR* dir = opendir(path.cStr());
KJ_ASSERT(dir != nullptr);
KJ_DEFER(closedir(dir));
for (;;) {
auto entry = readdir(dir);
if (entry == nullptr) break;
StringPtr name = entry->d_name;
if (name == "." || name == "..") continue;
auto subPath = kj::str(path, '/', entry->d_name);
KJ_EXPECT(!name.startsWith(".kj-tmp."), "temp file not cleaned up", subPath);
struct stat stats;
KJ_SYSCALL(lstat(subPath.cStr(), &stats));
if (S_ISDIR(stats.st_mode)) {
recursiveDelete(subPath);
} else {
KJ_SYSCALL(unlink(subPath.cStr()));
}
}
}
KJ_SYSCALL(rmdir(path.cStr()));
}
};
KJ_TEST("DiskFile") {
auto file = newTempFile();
KJ_EXPECT(file->readAllText() == "");
file->writeAll("foo");
KJ_EXPECT(file->readAllText() == "foo");
file->write(3, StringPtr("bar").asBytes());
KJ_EXPECT(file->readAllText() == "foobar");
file->write(3, StringPtr("baz").asBytes());
KJ_EXPECT(file->readAllText() == "foobaz");
file->write(9, StringPtr("qux").asBytes());
KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0qux", 12));
file->truncate(6);
KJ_EXPECT(file->readAllText() == "foobaz");
file->truncate(18);
KJ_EXPECT(file->readAllText() == kj::StringPtr("foobaz\0\0\0\0\0\0\0\0\0\0\0\0", 18));
{
auto mapping = file->mmap(0, 18);
auto privateMapping = file->mmapPrivate(0, 18);
auto writableMapping = file->mmapWritable(0, 18);
KJ_EXPECT(mapping.size() == 18);
KJ_EXPECT(privateMapping.size() == 18);
KJ_EXPECT(writableMapping->get().size() == 18);
KJ_EXPECT(writableMapping->get().begin() != mapping.begin());
KJ_EXPECT(privateMapping.begin() != mapping.begin());
KJ_EXPECT(writableMapping->get().begin() != privateMapping.begin());
KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "foobaz");
KJ_EXPECT(kj::str(writableMapping->get().slice(0, 6).asChars()) == "foobaz");
KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "foobaz");
privateMapping[0] = 'F';
KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "foobaz");
KJ_EXPECT(kj::str(writableMapping->get().slice(0, 6).asChars()) == "foobaz");
KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "Foobaz");
writableMapping->get()[0] = 'D';
writableMapping->changed(writableMapping->get().slice(0, 1));
KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "Doobaz");
KJ_EXPECT(kj::str(writableMapping->get().slice(0, 6).asChars()) == "Doobaz");
KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "Foobaz");
file->write(0, StringPtr("qux").asBytes());
KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "quxbaz");
KJ_EXPECT(kj::str(writableMapping->get().slice(0, 6).asChars()) == "quxbaz");
KJ_EXPECT(kj::str(privateMapping.slice(0, 6).asChars()) == "Foobaz");
file->write(12, StringPtr("corge").asBytes());
KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == "corge");
// Can shrink.
file->truncate(6);
KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5));
// Can regrow.
file->truncate(18);
KJ_EXPECT(kj::str(mapping.slice(12, 17).asChars()) == kj::StringPtr("\0\0\0\0\0", 5));
// Can even regrow past previous capacity.
file->truncate(100);
}
file->truncate(6);
KJ_EXPECT(file->readAllText() == "quxbaz");
file->zero(3, 3);
KJ_EXPECT(file->readAllText() == StringPtr("qux\0\0\0", 6));
}
KJ_TEST("DiskFile::copy()") {
auto source = newTempFile();
source->writeAll("foobarbaz");
auto dest = newTempFile();
dest->writeAll("quxcorge");
KJ_EXPECT(dest->copy(3, *source, 6, kj::maxValue) == 3);
KJ_EXPECT(dest->readAllText() == "quxbazge");
KJ_EXPECT(dest->copy(0, *source, 3, 4) == 4);
KJ_EXPECT(dest->readAllText() == "barbazge");
KJ_EXPECT(dest->copy(0, *source, 128, kj::maxValue) == 0);
KJ_EXPECT(dest->copy(4, *source, 3, 0) == 0);
String bigString = strArray(repeat("foobar", 10000), "");
source->truncate(bigString.size() + 1000);
source->write(123, bigString.asBytes());
dest->copy(321, *source, 123, bigString.size());
KJ_EXPECT(dest->readAllText().slice(321) == bigString);
}
KJ_TEST("DiskDirectory") {
TempDir tempDir;
auto dir = tempDir.get();
KJ_EXPECT(dir->listNames() == nullptr);
KJ_EXPECT(dir->listEntries() == nullptr);
KJ_EXPECT(!dir->exists(Path("foo")));
KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
{
auto file = dir->openFile(Path("foo"), WriteMode::CREATE);
file->writeAll("foobar");
}
KJ_EXPECT(dir->exists(Path("foo")));
{
auto stats = dir->lstat(Path("foo"));
KJ_EXPECT(stats.type == FsNode::Type::FILE);
KJ_EXPECT(stats.size == 6);
}
{
auto list = dir->listNames();
KJ_ASSERT(list.size() == 1);
KJ_EXPECT(list[0] == "foo");
}
{
auto list = dir->listEntries();
KJ_ASSERT(list.size() == 1);
KJ_EXPECT(list[0].name == "foo");
KJ_EXPECT(list[0].type == FsNode::Type::FILE);
}
KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar");
KJ_EXPECT(dir->tryOpenFile(Path({"foo", "bar"}), WriteMode::MODIFY) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::MODIFY) == nullptr);
KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
dir->tryOpenFile(Path({"bar", "baz"}), WriteMode::CREATE));
{
auto file = dir->openFile(Path({"bar", "baz"}), WriteMode::CREATE | WriteMode::CREATE_PARENT);
file->writeAll("bazqux");
}
KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "bazqux");
{
auto stats = dir->lstat(Path("bar"));
KJ_EXPECT(stats.type == FsNode::Type::DIRECTORY);
}
{
auto list = dir->listNames();
KJ_ASSERT(list.size() == 2);
KJ_EXPECT(list[0] == "bar");
KJ_EXPECT(list[1] == "foo");
}
{
auto list = dir->listEntries();
KJ_ASSERT(list.size() == 2);
KJ_EXPECT(list[0].name == "bar");
KJ_EXPECT(list[0].type == FsNode::Type::DIRECTORY);
KJ_EXPECT(list[1].name == "foo");
KJ_EXPECT(list[1].type == FsNode::Type::FILE);
}
{
auto subdir = dir->openSubdir(Path("bar"));
KJ_EXPECT(subdir->openFile(Path("baz"))->readAllText() == "bazqux");
}
auto subdir = dir->openSubdir(Path("corge"), WriteMode::CREATE);
subdir->openFile(Path("grault"), WriteMode::CREATE)->writeAll("garply");
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "garply");
dir->openFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY)
->write(0, StringPtr("rag").asBytes());
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply");
KJ_EXPECT(dir->openSubdir(Path("corge"))->listNames().size() == 1);
{
auto replacer =
dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY);
replacer->get().writeAll("rag");
// temp file not in list
KJ_EXPECT(dir->openSubdir(Path("corge"))->listNames().size() == 1);
// Don't commit.
}
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragply");
{
auto replacer =
dir->replaceFile(Path({"corge", "grault"}), WriteMode::CREATE | WriteMode::MODIFY);
replacer->get().writeAll("rag");
// temp file not in list
KJ_EXPECT(dir->openSubdir(Path("corge"))->listNames().size() == 1);
replacer->commit();
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag");
}
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "rag");
{
auto appender = dir->appendFile(Path({"corge", "grault"}), WriteMode::MODIFY);
appender->write("waldo", 5);
appender->write("fred", 4);
}
KJ_EXPECT(dir->openFile(Path({"corge", "grault"}))->readAllText() == "ragwaldofred");
KJ_EXPECT(dir->exists(Path("foo")));
dir->remove(Path("foo"));
KJ_EXPECT(!dir->exists(Path("foo")));
KJ_EXPECT(!dir->tryRemove(Path("foo")));
KJ_EXPECT(dir->exists(Path({"bar", "baz"})));
dir->remove(Path({"bar", "baz"}));
KJ_EXPECT(!dir->exists(Path({"bar", "baz"})));
KJ_EXPECT(dir->exists(Path("bar")));
KJ_EXPECT(!dir->tryRemove(Path({"bar", "baz"})));
KJ_EXPECT(dir->exists(Path("corge")));
KJ_EXPECT(dir->exists(Path({"corge", "grault"})));
dir->remove(Path("corge"));
KJ_EXPECT(!dir->exists(Path("corge")));
KJ_EXPECT(!dir->exists(Path({"corge", "grault"})));
KJ_EXPECT(!dir->tryRemove(Path("corge")));
}
KJ_TEST("DiskDirectory symlinks") {
TempDir tempDir;
auto dir = tempDir.get();
dir->symlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE);
KJ_EXPECT(!dir->trySymlink(Path("foo"), "bar/qux/../baz", WriteMode::CREATE));
{
auto stats = dir->lstat(Path("foo"));
KJ_EXPECT(stats.type == FsNode::Type::SYMLINK);
}
KJ_EXPECT(dir->readlink(Path("foo")) == "bar/qux/../baz");
// Broken link into non-existing directory cannot be opened in any mode.
KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
dir->tryOpenFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY));
KJ_EXPECT_THROW_MESSAGE("parent is not a directory",
dir->tryOpenFile(Path("foo"),
WriteMode::CREATE | WriteMode::MODIFY | WriteMode::CREATE_PARENT));
// Create the directory.
auto subdir = dir->openSubdir(Path("bar"), WriteMode::CREATE);
subdir->openSubdir(Path("qux"), WriteMode::CREATE);
// Link still points to non-existing file so cannot be open in most modes.
KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::CREATE) == nullptr);
KJ_EXPECT(dir->tryOpenFile(Path("foo"), WriteMode::MODIFY) == nullptr);
// But... CREATE | MODIFY works.
dir->openFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY)
->writeAll("foobar");
KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar");
KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar");
KJ_EXPECT(dir->openFile(Path("foo"), WriteMode::MODIFY)->readAllText() == "foobar");
// operations that modify the symlink
dir->symlink(Path("foo"), "corge", WriteMode::MODIFY);
KJ_EXPECT(dir->openFile(Path({"bar", "baz"}))->readAllText() == "foobar");
KJ_EXPECT(dir->readlink(Path("foo")) == "corge");
KJ_EXPECT(!dir->exists(Path("foo")));
KJ_EXPECT(dir->lstat(Path("foo")).type == FsNode::Type::SYMLINK);
KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
dir->remove(Path("foo"));
KJ_EXPECT(!dir->exists(Path("foo")));
KJ_EXPECT(dir->tryOpenFile(Path("foo")) == nullptr);
}
KJ_TEST("DiskDirectory link") {
TempDir tempDirSrc;
TempDir tempDirDst;
auto src = tempDirSrc.get();
auto dst = tempDirDst.get();
src->openFile(Path("foo"), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("foobar");
dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::LINK);
KJ_EXPECT(dst->openFile(Path("link"))->readAllText() == "foobar");
// Writing the old location modifies the new.
src->openFile(Path("foo"), WriteMode::MODIFY)->writeAll("bazqux");
KJ_EXPECT(dst->openFile(Path("link"))->readAllText() == "bazqux");
// Replacing the old location doesn't modify the new.
{
auto replacer = src->replaceFile(Path("foo"), WriteMode::MODIFY);
replacer->get().writeAll("corge");
replacer->commit();
}
KJ_EXPECT(src->openFile(Path("foo"))->readAllText() == "corge");
KJ_EXPECT(dst->openFile(Path("link"))->readAllText() == "bazqux");
}
KJ_TEST("DiskDirectory copy") {
TempDir tempDirSrc;
TempDir tempDirDst;
auto src = tempDirSrc.get();
auto dst = tempDirDst.get();
src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("foobar");
src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("bazqux");
dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::COPY);
KJ_EXPECT(src->openFile(Path({"foo", "bar"}))->readAllText() == "foobar");
KJ_EXPECT(src->openFile(Path({"foo", "baz", "qux"}))->readAllText() == "bazqux");
KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux");
KJ_EXPECT(dst->exists(Path({"link", "bar"})));
src->remove(Path({"foo", "bar"}));
KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
}
KJ_TEST("DiskDirectory move") {
TempDir tempDirSrc;
TempDir tempDirDst;
auto src = tempDirSrc.get();
auto dst = tempDirDst.get();
src->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("foobar");
src->openFile(Path({"foo", "baz", "qux"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("bazqux");
dst->transfer(Path("link"), WriteMode::CREATE, *src, Path("foo"), TransferMode::MOVE);
KJ_EXPECT(!src->exists(Path({"foo"})));
KJ_EXPECT(dst->openFile(Path({"link", "bar"}))->readAllText() == "foobar");
KJ_EXPECT(dst->openFile(Path({"link", "baz", "qux"}))->readAllText() == "bazqux");
}
KJ_TEST("DiskDirectory createTemporary") {
TempDir tempDir;
auto dir = tempDir.get();
auto file = dir->createTemporary();
file->writeAll("foobar");
KJ_EXPECT(file->readAllText() == "foobar");
KJ_EXPECT(dir->listNames() == nullptr);
}
KJ_TEST("DiskDirectory replace directory with file") {
TempDir tempDir;
auto dir = tempDir.get();
dir->openFile(Path({"foo", "bar"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("foobar");
{
// CREATE fails -- already exists.
auto replacer = dir->replaceFile(Path("foo"), WriteMode::CREATE);
replacer->get().writeAll("bazqux");
KJ_EXPECT(!replacer->tryCommit());
}
// Still a directory.
KJ_EXPECT(dir->lstat(Path("foo")).type == FsNode::Type::DIRECTORY);
{
// MODIFY succeeds.
auto replacer = dir->replaceFile(Path("foo"), WriteMode::MODIFY);
replacer->get().writeAll("bazqux");
replacer->commit();
}
// Replaced with file.
KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "bazqux");
}
KJ_TEST("DiskDirectory replace file with directory") {
TempDir tempDir;
auto dir = tempDir.get();
dir->openFile(Path("foo"), WriteMode::CREATE)
->writeAll("foobar");
{
// CREATE fails -- already exists.
auto replacer = dir->replaceSubdir(Path("foo"), WriteMode::CREATE);
replacer->get().openFile(Path("bar"), WriteMode::CREATE)->writeAll("bazqux");
KJ_EXPECT(dir->listNames().size() == 1 && dir->listNames()[0] == "foo");
KJ_EXPECT(!replacer->tryCommit());
}
// Still a file.
KJ_EXPECT(dir->openFile(Path("foo"))->readAllText() == "foobar");
{
// MODIFY succeeds.
auto replacer = dir->replaceSubdir(Path("foo"), WriteMode::MODIFY);
replacer->get().openFile(Path("bar"), WriteMode::CREATE)->writeAll("bazqux");
KJ_EXPECT(dir->listNames().size() == 1 && dir->listNames()[0] == "foo");
replacer->commit();
}
// Replaced with directory.
KJ_EXPECT(dir->openFile(Path({"foo", "bar"}))->readAllText() == "bazqux");
}
#ifndef HOLES_NOT_SUPPORTED
KJ_TEST("DiskFile holes") {
TempDir tempDir;
auto dir = tempDir.get();
auto file = dir->openFile(Path("holes"), WriteMode::CREATE);
file->writeAll("foobar");
file->write(1 << 20, StringPtr("foobar").asBytes());
// Allow for block sizes as low as 512 bytes and as high as 64k.
auto meta = file->stat();
KJ_EXPECT(meta.spaceUsed >= 2 * 512);
KJ_EXPECT(meta.spaceUsed <= 2 * 65536);
byte buf[7];
{
// Copy doesn't fill in holes.
dir->transfer(Path("copy"), WriteMode::CREATE, Path("holes"), TransferMode::COPY);
auto copy = dir->openFile(Path("copy"));
KJ_EXPECT(copy->stat().spaceUsed == meta.spaceUsed);
KJ_EXPECT(copy->read(0, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
KJ_EXPECT(copy->read(1 << 20, buf) == 6);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
KJ_EXPECT(copy->read(1 << 19, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == StringPtr("\0\0\0\0\0\0", 6));
}
file->truncate(1 << 21);
KJ_EXPECT(file->stat().spaceUsed == meta.spaceUsed);
KJ_EXPECT(file->read(1 << 20, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
{
dir->transfer(Path("copy"), WriteMode::MODIFY, Path("holes"), TransferMode::COPY);
auto copy = dir->openFile(Path("copy"));
KJ_EXPECT(copy->stat().spaceUsed == meta.spaceUsed);
KJ_EXPECT(copy->read(0, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
KJ_EXPECT(copy->read(1 << 20, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
KJ_EXPECT(copy->read(1 << 19, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == StringPtr("\0\0\0\0\0\0", 6));
}
}
#endif
} // namespace
} // namespace kj
// Copyright (c) 2015 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include "filesystem.h"
#include "debug.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <dirent.h>
#include <syscall.h>
#include "vector.h"
#include "miniposix.h"
#if __linux__
#include <linux/fs.h>
#include <sys/sendfile.h>
#endif
namespace kj {
namespace {
#define HIDDEN_PREFIX ".kj-tmp."
// Prefix for temp files which should be hidden when listing a directory.
//
// If you change this, make sure to update the unit test.
#ifdef O_CLOEXEC
#define MAYBE_O_CLOEXEC O_CLOEXEC
#else
#define MAYBE_O_CLOEXEC 0
#endif
#ifdef O_DIRECTORY
#define MAYBE_O_DIRECTORY O_DIRECTORY
#else
#define MAYBE_O_DIRECTORY 0
#endif
static void setCloexec(int fd) KJ_UNUSED;
static void setCloexec(int fd) {
// Set the O_CLOEXEC flag on the given fd.
//
// We try to avoid the need to call this by taking advantage of syscall flags that set it
// atomically on new file descriptors. Unfortunately some platforms do not support such syscalls.
#ifdef FIOCLEX
// Yay, we can set the flag in one call.
KJ_SYSCALL_HANDLE_ERRORS(ioctl(fd, FIOCLEX)) {
case EINVAL:
case EOPNOTSUPP:
break;
default:
KJ_FAIL_SYSCALL("ioctl(fd, FIOCLEX)", error) { break; }
break;
} else {
// success
return;
}
#endif
// Sadness, we must resort to read/modify/write.
//
// (On many platforms, FD_CLOEXEC is the only flag modifiable via F_SETFD and therefore we could
// skip the read... but it seems dangerous to assume that's true of all platforms, and anyway
// most platforms support FIOCLEX.)
int flags;
KJ_SYSCALL(flags = fcntl(fd, F_GETFD));
if (!(flags & FD_CLOEXEC)) {
KJ_SYSCALL(fcntl(fd, F_SETFD, flags | FD_CLOEXEC));
}
}
static Date toKjDate(struct timespec tv) {
return tv.tv_sec * SECONDS + tv.tv_nsec * NANOSECONDS + UNIX_EPOCH;
}
static FsNode::Type modeToType(mode_t mode) {
switch (mode & S_IFMT) {
case S_IFREG : return FsNode::Type::FILE;
case S_IFDIR : return FsNode::Type::DIRECTORY;
case S_IFLNK : return FsNode::Type::SYMLINK;
case S_IFBLK : return FsNode::Type::BLOCK_DEVICE;
case S_IFCHR : return FsNode::Type::CHARACTER_DEVICE;
case S_IFIFO : return FsNode::Type::NAMED_PIPE;
case S_IFSOCK: return FsNode::Type::SOCKET;
default: return FsNode::Type::OTHER;
}
}
static FsNode::Metadata statToMetadata(struct stat& stats) {
return FsNode::Metadata {
modeToType(stats.st_mode),
implicitCast<uint64_t>(stats.st_size),
implicitCast<uint64_t>(stats.st_blocks * 512u),
toKjDate(stats.st_mtim),
implicitCast<uint>(stats.st_nlink)
};
}
static bool rmrf(int fd, StringPtr path);
static void rmrfChildrenAndClose(int fd) {
// Assumes fd is seeked to beginning.
DIR* dir = fdopendir(fd);
if (dir == nullptr) {
close(fd);
KJ_FAIL_SYSCALL("fdopendir", errno);
};
KJ_DEFER(closedir(dir));
for (;;) {
errno = 0;
struct dirent* entry = readdir(dir);
if (entry == nullptr) {
int error = errno;
if (error == 0) {
break;
} else {
KJ_FAIL_SYSCALL("readdir", error);
}
}
if (entry->d_name[0] == '.' &&
(entry->d_name[1] == '\0' ||
(entry->d_name[1] == '.' &&
entry->d_name[2] == '\0'))) {
// ignore . and ..
} else {
#ifdef DT_UNKNOWN // d_type is not available on all platforms.
if (entry->d_type == DT_DIR) {
int subdirFd;
KJ_SYSCALL(subdirFd = openat(
fd, entry->d_name, O_RDONLY | MAYBE_O_DIRECTORY | MAYBE_O_CLOEXEC));
rmrfChildrenAndClose(subdirFd);
KJ_SYSCALL(unlinkat(fd, entry->d_name, AT_REMOVEDIR));
} else if (entry->d_type != DT_UNKNOWN) {
KJ_SYSCALL(unlinkat(fd, entry->d_name, 0));
} else {
#endif
KJ_ASSERT(rmrf(fd, entry->d_name));
#ifdef DT_UNKNOWN
}
#endif
}
}
}
static bool rmrf(int fd, StringPtr path) {
struct stat stats;
KJ_SYSCALL_HANDLE_ERRORS(fstatat(fd, path.cStr(), &stats, AT_SYMLINK_NOFOLLOW)) {
case ENOENT:
case ENOTDIR:
// Doesn't exist.
return false;
default:
KJ_FAIL_SYSCALL("lstat(path)", error, path) { return false; }
}
if (S_ISDIR(stats.st_mode)) {
int subdirFd;
KJ_SYSCALL(subdirFd = openat(
fd, path.cStr(), O_RDONLY | MAYBE_O_DIRECTORY | MAYBE_O_CLOEXEC)) { return false; }
rmrfChildrenAndClose(subdirFd);
KJ_SYSCALL(unlinkat(fd, path.cStr(), AT_REMOVEDIR)) { return false; }
} else {
KJ_SYSCALL(unlinkat(fd, path.cStr(), 0)) { return false; }
}
return true;
}
struct MmapRange {
uint64_t offset;
uint64_t size;
};
static MmapRange getMmapRange(uint64_t offset, uint64_t size) {
// Rounds the given byte range up to page boundaries.
#ifndef _SC_PAGESIZE
#define _SC_PAGESIZE _SC_PAGE_SIZE
#endif
static const uint64_t pageSize = sysconf(_SC_PAGESIZE);
uint64_t pageMask = pageSize - 1;
uint64_t realOffset = offset & ~pageMask;
uint64_t end = offset + size;
uint64_t realEnd = (end + pageMask) & ~pageMask;
return { realOffset, realEnd - realOffset };
}
class MmapDisposer: public ArrayDisposer {
protected:
void disposeImpl(void* firstElement, size_t elementSize, size_t elementCount,
size_t capacity, void (*destroyElement)(void*)) const {
auto range = getMmapRange(reinterpret_cast<uintptr_t>(firstElement),
elementSize * elementCount);
#if _WIN32
KJ_ASSERT(UnmapViewOfFile(reinterpret_cast<byte*>(range.offset))) { break; }
#else
KJ_SYSCALL(munmap(reinterpret_cast<byte*>(range.offset), range.size)) { break; }
#endif
}
};
constexpr MmapDisposer mmapDisposer = MmapDisposer();
class DiskHandle {
// We need to implement each of ReadableFile, AppendableFile, File, ReadableDirectory, and
// Directory for disk handles. There is a lot of implementation overlap between these, especially
// stat(), sync(), etc. We can't have everything inherit from a common DiskFsNode that implements
// these because then we get diamond inheritance which means we need to make all our inheritance
// virtual which means downcasting requires RTTI which violates our goal of supporting compiling
// with no RTTI. So instead we have the DiskHandle class which implements all the methods without
// inheriting anything, and then we have DiskFile, DiskDirectory, etc. hold this and delegate to
// it. Ugly, but works.
public:
DiskHandle(AutoCloseFd&& fd): fd(kj::mv(fd)) {}
// OsHandle ------------------------------------------------------------------
AutoCloseFd clone() {
int fd2;
#ifdef F_DUPFD_CLOEXEC
KJ_SYSCALL_HANDLE_ERRORS(fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 3)) {
case EINVAL:
case EOPNOTSUPP:
// fall back
break;
default:
KJ_FAIL_SYSCALL("fnctl(fd, F_DUPFD_CLOEXEC, 3)", error) { break; }
break;
} else {
return AutoCloseFd(fd2);
}
#endif
KJ_SYSCALL(fd2 = ::dup(fd));
AutoCloseFd result(fd2);
setCloexec(result);
return result;
}
int getFd() {
return fd.get();
}
// FsNode --------------------------------------------------------------------
FsNode::Metadata stat() {
struct stat stats;
KJ_SYSCALL(::fstat(fd, &stats));
return statToMetadata(stats);
}
void sync() { KJ_SYSCALL(fsync(fd)); }
void datasync() { KJ_SYSCALL(fdatasync(fd)); }
// ReadableFile --------------------------------------------------------------
size_t read(uint64_t offset, ArrayPtr<byte> buffer) {
// pread() probably never returns short reads unless it hits EOF. Unfortunately, though, per
// spec we are not allowed to assume this.
size_t total = 0;
while (buffer.size() > 0) {
ssize_t n;
KJ_SYSCALL(n = pread(fd, buffer.begin(), buffer.size(), offset));
if (n == 0) break;
total += n;
offset += n;
buffer = buffer.slice(n, buffer.size());
}
return total;
}
Array<const byte> mmap(uint64_t offset, uint64_t size) {
auto range = getMmapRange(offset, size);
const void* mapping = ::mmap(NULL, range.size, PROT_READ, MAP_SHARED, fd, range.offset);
if (mapping == MAP_FAILED) {
KJ_FAIL_SYSCALL("mmap", errno);
}
return Array<const byte>(reinterpret_cast<const byte*>(mapping) + (offset - range.offset),
size, mmapDisposer);
}
Array<byte> mmapPrivate(uint64_t offset, uint64_t size) {
auto range = getMmapRange(offset, size);
void* mapping = ::mmap(NULL, range.size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, range.offset);
if (mapping == MAP_FAILED) {
KJ_FAIL_SYSCALL("mmap", errno);
}
return Array<byte>(reinterpret_cast<byte*>(mapping) + (offset - range.offset),
size, mmapDisposer);
}
// File ----------------------------------------------------------------------
void write(uint64_t offset, ArrayPtr<const byte> data) {
// pwrite() probably never returns short writes unless there's no space left on disk.
// Unfortunately, though, per spec we are not allowed to assume this.
while (data.size() > 0) {
ssize_t n;
KJ_SYSCALL(n = pwrite(fd, data.begin(), data.size(), offset));
KJ_ASSERT(n > 0, "pwrite() returned zero?");
offset += n;
data = data.slice(n, data.size());
}
}
void zero(uint64_t offset, uint64_t size) {
#ifdef FALLOC_FL_PUNCH_HOLE
KJ_SYSCALL_HANDLE_ERRORS(
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, size)) {
case EOPNOTSUPP:
// fall back to below
break;
default:
KJ_FAIL_SYSCALL("fallocate(FALLOC_FL_PUNCH_HOLE)", error) { return; }
} else {
return;
}
#endif
// Use a 4k buffer of zeros amplified by iov to write zeros with as few syscalls as possible.
byte buffer[4096];
memset(buffer, 0, sizeof(buffer));
size_t count = (size + sizeof(buffer) - 1) / sizeof(buffer);
const size_t iovmax = miniposix::iovMax(count);
KJ_STACK_ARRAY(struct iovec, iov, kj::min(iovmax, count), 16, 256);
for (auto& item: iov) {
item.iov_base = buffer;
item.iov_len = sizeof(buffer);
}
while (size > 0) {
size_t iovCount;
if (size >= iov.size() * sizeof(buffer)) {
iovCount = iov.size();
} else {
iovCount = size / sizeof(buffer);
size_t rem = size % sizeof(buffer);
if (rem > 0) {
iov[iovCount++].iov_len = rem;
}
}
ssize_t n;
KJ_SYSCALL(n = pwritev(fd, iov.begin(), count, offset));
KJ_ASSERT(n > 0, "pwrite() returned zero?");
offset += n;
size -= n;
}
}
void truncate(uint64_t size) {
KJ_SYSCALL(ftruncate(fd, size));
}
class WritableFileMappingImpl: public WritableFileMapping {
public:
WritableFileMappingImpl(Array<byte> bytes): bytes(kj::mv(bytes)) {}
ArrayPtr<byte> get() override {
return bytes;
}
void changed(ArrayPtr<byte> slice) override {
KJ_REQUIRE(slice.begin() >= bytes.begin() && slice.end() <= bytes.end(),
"byte range is not part of this mapping");
KJ_SYSCALL(msync(slice.begin(), slice.size(), MS_ASYNC));
}
void sync(ArrayPtr<byte> slice) override {
KJ_REQUIRE(slice.begin() >= bytes.begin() && slice.end() <= bytes.end(),
"byte range is not part of this mapping");
KJ_SYSCALL(msync(slice.begin(), slice.size(), MS_SYNC));
}
private:
Array<byte> bytes;
};
Own<WritableFileMapping> mmapWritable(uint64_t offset, uint64_t size) {
auto range = getMmapRange(offset, size);
void* mapping = ::mmap(NULL, range.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, range.offset);
if (mapping == MAP_FAILED) {
KJ_FAIL_SYSCALL("mmap", errno);
}
auto array = Array<byte>(reinterpret_cast<byte*>(mapping) + (offset - range.offset),
size, mmapDisposer);
return heap<WritableFileMappingImpl>(kj::mv(array));
}
size_t copyChunk(uint64_t offset, int fromFd, uint64_t fromOffset, uint64_t size) {
// Copies a range of bytes from `fromFd` to this file in the most efficient way possible for
// the OS. Only returns less than `size` if EOF. Does not account for holes.
#if __linux__
{
KJ_SYSCALL(lseek(fd, offset, SEEK_SET));
off_t fromPos = fromOffset;
off_t end = fromOffset + size;
while (fromPos < end) {
ssize_t n;
KJ_SYSCALL_HANDLE_ERRORS(n = sendfile(fd, fromFd, &fromPos, end - fromPos)) {
case EINVAL:
case ENOSYS:
goto sendfileNotAvailable;
default:
KJ_FAIL_SYSCALL("sendfile", error) { return fromPos - fromOffset; }
}
}
return fromPos - fromOffset;
}
sendfileNotAvailable:
#endif
uint64_t total = 0;
while (size > 0) {
byte buffer[4096];
ssize_t n;
KJ_SYSCALL(n = pread(fromFd, buffer, kj::min(sizeof(buffer), size), fromOffset));
if (n == 0) break;
write(offset, arrayPtr(buffer, n));
fromOffset += n;
offset += n;
total += n;
size -= n;
}
return total;
}
kj::Maybe<size_t> copy(uint64_t offset, ReadableFile& from, uint64_t fromOffset, uint64_t size) {
KJ_IF_MAYBE(otherFd, from.getFd()) {
#ifdef FICLONE
if (offset == 0 && fromOffset == 0 && size == kj::maxValue && stat().size == 0) {
if (ioctl(fd, FICLONE, *otherFd) >= 0) {
return stat().size;
}
} else if (size > 0) { // src_length = 0 has special meaning for the syscall, so avoid.
struct file_clone_range range;
memset(&range, 0, sizeof(range));
range.src_fd = *otherFd;
range.dest_offset = offset;
range.src_offset = fromOffset;
range.src_length = size == kj::maxValue ? 0 : size;
if (ioctl(fd, FICLONERANGE, &range) >= 0) {
// TODO(someday): What does FICLONERANGE actually do if the range goes past EOF? The docs
// don't say. Maybe it only copies the parts that exist. Maybe it punches holes for the
// rest. Where does the destination file's EOF marker end up? Who knows?
return kj::min(from.stat().size - fromOffset, size);
}
} else {
// size == 0
return size_t(0);
}
// ioctl failed. Almost all failures documented for these are of the form "the operation is
// not supported for the filesystem(s) specified", so fall back to other approaches.
#endif
off_t toPos = offset;
off_t fromPos = fromOffset;
off_t end = size == kj::maxValue ? off_t(kj::maxValue) : off_t(fromOffset + size);
for (;;) {
// Handle data.
{
// Find out how much data there is before the next hole.
off_t nextHole;
#ifdef SEEK_HOLE
KJ_SYSCALL_HANDLE_ERRORS(nextHole = lseek(*otherFd, fromPos, SEEK_HOLE)) {
case EINVAL:
// SEEK_HOLE probably not supported. Assume no holes.
nextHole = end;
break;
case ENXIO:
// Past EOF. Stop here.
return fromPos - fromOffset;
default:
KJ_FAIL_SYSCALL("lseek(fd, pos, SEEK_HOLE)", error) { return fromPos - fromOffset; }
}
#else
// SEEK_HOLE not supported. Assume no holes.
nextHole = end;
#endif
// Copy the next chunk of data.
off_t copyTo = kj::min(end, nextHole);
size_t amount = copyTo - fromPos;
if (amount > 0) {
size_t n = copyChunk(toPos, *otherFd, fromPos, amount);
fromPos += n;
toPos += n;
if (n < amount) {
return fromPos - fromOffset;
}
}
if (fromPos == end) {
return fromPos - fromOffset;
}
}
#ifdef SEEK_HOLE
// Handle hole.
{
// Find out how much hole there is before the next data.
off_t nextData;
KJ_SYSCALL_HANDLE_ERRORS(nextData = lseek(*otherFd, fromPos, SEEK_DATA)) {
case EINVAL:
// SEEK_DATA probably not supported. But we should only have gotten here if we
// were expecting a hole.
KJ_FAIL_ASSERT("can't determine hole size; SEEK_DATA not supported");
break;
case ENXIO:
// No more data. Set to EOF.
KJ_SYSCALL(nextData = lseek(*otherFd, 0, SEEK_END));
if (nextData > end) {
end = nextData;
}
break;
default:
KJ_FAIL_SYSCALL("lseek(fd, pos, SEEK_HOLE)", error) { return fromPos - fromOffset; }
}
// Write zeros.
off_t zeroTo = kj::min(end, nextData);
off_t amount = zeroTo - fromPos;
if (amount > 0) {
zero(toPos, amount);
toPos += amount;
fromPos = zeroTo;
}
if (fromPos == end) {
return fromPos - fromOffset;
}
}
#endif
}
}
// Indicates caller should call File::copy() default implementation.
return nullptr;
}
// ReadableDirectory ---------------------------------------------------------
template <typename Func>
auto list(bool needTypes, Func&& func)
-> Array<Decay<decltype(func(instance<StringPtr>(), instance<FsNode::Type>()))>> {
// Seek to start of directory.
KJ_SYSCALL(lseek(fd, 0, SEEK_SET));
// Unfortunately, fdopendir() takes ownership of the file descriptor. Therefore we need to
// make a duplicate.
int duped;
KJ_SYSCALL(duped = dup(fd));
DIR* dir = fdopendir(duped);
if (dir == nullptr) {
close(duped);
KJ_FAIL_SYSCALL("fdopendir", errno);
}
KJ_DEFER(closedir(dir));
kj::Vector<Decay<decltype(func(instance<StringPtr>(), instance<FsNode::Type>()))>> entries;
for (;;) {
errno = 0;
struct dirent* entry = readdir(dir);
if (entry == nullptr) {
int error = errno;
if (error == 0) {
break;
} else {
KJ_FAIL_SYSCALL("readdir", error);
}
}
kj::StringPtr name = entry->d_name;
if (name != "." && name != ".." && !name.startsWith(HIDDEN_PREFIX)) {
#ifdef DT_UNKNOWN // d_type is not available on all platforms.
if (entry->d_type != DT_UNKNOWN) {
entries.add(func(name, modeToType(DTTOIF(entry->d_type))));
} else {
#endif
if (needTypes) {
// Unknown type. Fall back to stat.
struct stat stats;
KJ_SYSCALL(fstatat(fd, name.cStr(), &stats, AT_SYMLINK_NOFOLLOW));
entries.add(func(name, modeToType(stats.st_mode)));
} else {
entries.add(func(name, FsNode::Type::OTHER));
}
#ifdef DT_UNKNOWN
}
#endif
}
}
return entries.releaseAsArray();
}
Array<String> listNames() {
return list(false, [](StringPtr name, FsNode::Type type) { return heapString(name); });
}
Array<ReadableDirectory::Entry> listEntries() {
return list(true, [](StringPtr name, FsNode::Type type) {
return ReadableDirectory::Entry { type, heapString(name), };
});
}
bool exists(PathPtr path) {
KJ_SYSCALL_HANDLE_ERRORS(faccessat(fd, path.toString().cStr(), F_OK, 0)) {
case ENOENT:
case ENOTDIR:
return false;
default:
KJ_FAIL_SYSCALL("faccessat(fd, path)", error, path) { return false; }
}
return true;
}
Maybe<FsNode::Metadata> tryLstat(PathPtr path) {
struct stat stats;
KJ_SYSCALL_HANDLE_ERRORS(fstatat(fd, path.toString().cStr(), &stats, AT_SYMLINK_NOFOLLOW)) {
case ENOENT:
case ENOTDIR:
return nullptr;
default:
KJ_FAIL_SYSCALL("faccessat(fd, path)", error, path) { return nullptr; }
}
return statToMetadata(stats);
}
Maybe<Own<ReadableFile>> tryOpenFile(PathPtr path) {
int newFd;
KJ_SYSCALL_HANDLE_ERRORS(newFd = openat(
fd, path.toString().cStr(), O_RDONLY | MAYBE_O_CLOEXEC)) {
case ENOENT:
case ENOTDIR:
return nullptr;
default:
KJ_FAIL_SYSCALL("openat(fd, path, O_RDONLY)", error, path) { return nullptr; }
}
kj::AutoCloseFd result(newFd);
#ifndef O_CLOEXEC
setCloexec(result);
#endif
return newDiskReadableFile(kj::mv(result));
}
Maybe<AutoCloseFd> tryOpenSubdirInternal(PathPtr path) {
int newFd;
KJ_SYSCALL_HANDLE_ERRORS(newFd = openat(
fd, path.toString().cStr(), O_RDONLY | MAYBE_O_CLOEXEC | MAYBE_O_DIRECTORY)) {
case ENOENT:
return nullptr;
case ENOTDIR:
// Could mean that a parent is not a directory, which we treat as "doesn't exist".
// Could also mean that the specified file is not a directory, which should throw.
// Check using exists().
if (!exists(path)) {
return nullptr;
}
// fallthrough
default:
KJ_FAIL_SYSCALL("openat(fd, path, O_DIRECTORY)", error, path) { return nullptr; }
}
kj::AutoCloseFd result(newFd);
#ifndef O_CLOEXEC
setCloexec(result);
#endif
return kj::mv(result);
}
Maybe<Own<ReadableDirectory>> tryOpenSubdir(PathPtr path) {
return tryOpenSubdirInternal(path).map(newDiskReadableDirectory);
}
Maybe<String> tryReadlink(PathPtr path) {
size_t trySize = 256;
for (;;) {
KJ_STACK_ARRAY(char, buf, trySize, 256, 4096);
ssize_t n = readlinkat(fd, path.toString().cStr(), buf.begin(), buf.size());
if (n < 0) {
int error = errno;
switch (error) {
case EINTR:
continue;
case ENOENT:
case ENOTDIR:
case EINVAL: // not a link
return nullptr;
default:
KJ_FAIL_SYSCALL("readlinkat(fd, path)", error, path) { return nullptr; }
}
}
if (n >= buf.size()) {
// Didn't give it enough space. Better retry with a bigger buffer.
trySize *= 2;
continue;
}
return heapString(buf.begin(), n);
}
}
// Directory -----------------------------------------------------------------
bool tryMkdir(PathPtr path, WriteMode mode, bool noThrow) {
// Internal function to make a directory.
auto filename = path.toString();
mode_t acl = has(mode, WriteMode::PRIVATE) ? 0700 : 0777;
KJ_SYSCALL_HANDLE_ERRORS(mkdirat(fd, filename.cStr(), acl)) {
case EEXIST: {
// Apparently this path exists.
if (!has(mode, WriteMode::MODIFY)) {
// Require exclusive create.
return false;
}
// MODIFY is allowed, so we just need to check whether the existing entry is a directory.
struct stat stats;
KJ_SYSCALL_HANDLE_ERRORS(fstatat(fd, filename.cStr(), &stats, 0)) {
default:
// mkdir() says EEXIST but we can't stat it. Maybe it's a dangling link, or maybe
// we can't access it for some reason. Assume failure.
//
// TODO(someday): Maybe we should be creating the directory at the target of the
// link?
goto failed;
}
return (stats.st_mode & S_IFMT) == S_IFDIR;
}
case ENOENT:
if (has(mode, WriteMode::CREATE_PARENT) && path.size() > 0 &&
tryMkdir(path.parent(), WriteMode::CREATE | WriteMode::MODIFY |
WriteMode::CREATE_PARENT, true)) {
// Retry, but make sure we don't try to create the parent again.
return tryMkdir(path, mode - WriteMode::CREATE_PARENT, noThrow);
} else {
goto failed;
}
default:
failed:
if (noThrow) {
// Caller requested no throwing.
return false;
} else {
KJ_FAIL_SYSCALL("mkdirat(fd, path)", error, path);
}
}
return true;
}
kj::Maybe<String> createNamedTemporary(
PathPtr finalName, WriteMode mode, Function<int(StringPtr)> tryCreate) {
// Create a temporary file which will eventually replace `finalName`.
//
// Calls `tryCreate` to actually create the temporary, passing in the desired path. tryCreate()
// is expected to behave like a syscall, returning a negative value and setting `errno` on
// error. tryCreate() MUST fail with EEXIST if the path exists -- this is not checked in
// advance, since it needs to be checked atomically. In the case of EEXIST, tryCreate() will
// be called again with a new path.
//
// Returns the temporary path that succeeded. Only returns nullptr if there was an exception
// but we're complied with -fno-exceptions.
if (finalName.size() == 0) {
KJ_FAIL_REQUIRE("can't replace self") { break; }
return nullptr;
}
static uint counter = 0;
static const pid_t pid = getpid();
String pathPrefix;
if (finalName.size() > 1) {
pathPrefix = kj::str(finalName.parent(), '/');
}
auto path = kj::str(pathPrefix, HIDDEN_PREFIX, pid, '.', counter++, '.',
finalName.basename()[0], ".partial");
KJ_SYSCALL_HANDLE_ERRORS(tryCreate(path)) {
case EEXIST:
return createNamedTemporary(finalName, mode, kj::mv(tryCreate));
case ENOENT:
if (has(mode, WriteMode::CREATE_PARENT) && finalName.size() > 1 &&
tryMkdir(finalName.parent(), WriteMode::CREATE | WriteMode::MODIFY |
WriteMode::CREATE_PARENT, true)) {
// Retry, but make sure we don't try to create the parent again.
mode = mode - WriteMode::CREATE_PARENT;
return createNamedTemporary(finalName, mode, kj::mv(tryCreate));
}
// fallthrough
default:
KJ_FAIL_SYSCALL("create(path)", error, path) { break; }
return nullptr;
}
return kj::mv(path);
}
bool tryReplaceNode(PathPtr path, WriteMode mode, Function<int(StringPtr)> tryCreate) {
// Replaces the given path with an object created by calling tryCreate().
//
// tryCreate() must behave like a syscall which creates the node at the path passed to it,
// returning a negative value on error. If the path passed to tryCreate already exists, it
// MUST fail with EEXIST.
//
// When `mode` includes MODIFY, replaceNode() reacts to EEXIST by creating the node in a
// temporary location and then rename()ing it into place.
if (path.size() == 0) {
KJ_FAIL_REQUIRE("can't replace self") { return false; }
}
auto filename = path.toString();
if (has(mode, WriteMode::CREATE)) {
// First try just cerating the node in-place.
KJ_SYSCALL_HANDLE_ERRORS(tryCreate(filename)) {
case EEXIST:
// Target exists.
if (has(mode, WriteMode::MODIFY)) {
// Fall back to MODIFY path, below.
break;
} else {
return false;
}
case ENOENT:
if (has(mode, WriteMode::CREATE_PARENT) && path.size() > 0 &&
tryMkdir(path.parent(), WriteMode::CREATE | WriteMode::MODIFY |
WriteMode::CREATE_PARENT, true)) {
// Retry, but make sure we don't try to create the parent again.
return tryReplaceNode(path, mode - WriteMode::CREATE_PARENT, kj::mv(tryCreate));
}
default:
KJ_FAIL_SYSCALL("create(path)", error, path) { return false; }
} else {
// Success.
return true;
}
}
// Either we don't have CREATE mode or the target already exists. We need to perform a
// replacement instead.
KJ_IF_MAYBE(tempPath, createNamedTemporary(path, mode, kj::mv(tryCreate))) {
if (tryCommitReplacement(filename, fd, *tempPath, mode)) {
return true;
} else {
KJ_SYSCALL_HANDLE_ERRORS(unlinkat(fd, tempPath->cStr(), 0)) {
case ENOENT:
// meh
break;
default:
KJ_FAIL_SYSCALL("unlinkat(fd, tempPath, 0)", error, *tempPath);
}
return false;
}
} else {
// threw, but exceptions are disabled
return false;
}
}
Maybe<AutoCloseFd> tryOpenFileInternal(PathPtr path, WriteMode mode, bool append) {
uint flags = O_RDWR | MAYBE_O_CLOEXEC;
mode_t acl = 0666;
if (has(mode, WriteMode::CREATE)) {
flags |= O_CREAT;
}
if (!has(mode, WriteMode::MODIFY)) {
if (!has(mode, WriteMode::CREATE)) {
// Neither CREATE nor MODIFY -- impossible to satisfy preconditions.
return nullptr;
}
flags |= O_EXCL;
}
if (append) {
flags |= O_APPEND;
}
if (has(mode, WriteMode::EXECUTABLE)) {
acl = 0777;
}
if (has(mode, WriteMode::PRIVATE)) {
acl &= 0700;
}
auto filename = path.toString();
int newFd;
KJ_SYSCALL_HANDLE_ERRORS(newFd = openat(fd, filename.cStr(), flags, acl)) {
case ENOENT:
if (has(mode, WriteMode::CREATE)) {
// Either:
// - The file is a broken symlink.
// - A parent directory didn't exist.
if (has(mode, WriteMode::CREATE_PARENT) && path.size() > 0 &&
tryMkdir(path.parent(), WriteMode::CREATE | WriteMode::MODIFY |
WriteMode::CREATE_PARENT, true)) {
// Retry, but make sure we don't try to create the parent again.
return tryOpenFileInternal(path, mode - WriteMode::CREATE_PARENT, append);
}
// Check for broken link.
if (!has(mode, WriteMode::MODIFY) &&
faccessat(fd, path.toString().cStr(), F_OK, AT_SYMLINK_NOFOLLOW) >= 0) {
// Yep. We treat this as already-exists, which means in CREATE-only mode this is a
// simple failure.
return nullptr;
}
KJ_FAIL_REQUIRE("parent is not a directory", path) { return nullptr; }
} else {
// MODIFY-only mode. ENOENT = doesn't exist = return null.
return nullptr;
}
case ENOTDIR:
if (!has(mode, WriteMode::CREATE)) {
// MODIFY-only mode. ENOTDIR = parent not a directory = doesn't exist = return null.
return nullptr;
}
goto failed;
case EEXIST:
if (!has(mode, WriteMode::MODIFY)) {
// CREATE-only mode. EEXIST = already exists = return null.
return nullptr;
}
goto failed;
default:
failed:
KJ_FAIL_SYSCALL("openat(fd, path, O_RDWR | ...)", error, path) { return nullptr; }
}
kj::AutoCloseFd result(newFd);
#ifndef O_CLOEXEC
setCloexec(result);
#endif
return kj::mv(result);
}
bool tryCommitReplacement(StringPtr toPath, int fromDirFd, StringPtr fromPath, WriteMode mode,
int* errorReason = nullptr) {
if (has(mode, WriteMode::CREATE) && has(mode, WriteMode::MODIFY)) {
// Always clobber. Try it.
KJ_SYSCALL_HANDLE_ERRORS(renameat(fromDirFd, fromPath.cStr(), fd.get(), toPath.cStr())) {
case EISDIR:
case ENOTDIR:
case ENOTEMPTY:
case EEXIST:
// Failed because target exists and due to the various weird quirks of rename(), it
// can't remove it for us. On Linux we can try an exchange instead. On others we have
// to move the target out of the way.
break;
default:
if (errorReason == nullptr) {
KJ_FAIL_SYSCALL("rename(fromPath, toPath)", error, fromPath, toPath) { return false; }
} else {
*errorReason = error;
return false;
}
} else {
return true;
}
}
#ifdef RENAME_EXCHANGE
// Try to use Linux's renameat2() to atomically check preconditions and apply.
if (has(mode, WriteMode::MODIFY)) {
// Use an exchange to implement modification.
//
// We reach this branch when performing a MODIFY-only, or when performing a CREATE | MODIFY
// in which we determined above that there's a node of a different type blocking the
// exchange.
KJ_SYSCALL_HANDLE_ERRORS(syscall(SYS_renameat2,
fromDirFd, fromPath.cStr(), fd.get(), toPath.cStr(), RENAME_EXCHANGE)) {
case ENOSYS:
break; // fall back to traditional means
case ENOENT:
// Presumably because the target path doesn't exist.
if (has(mode, WriteMode::CREATE)) {
KJ_FAIL_ASSERT("rename(tmp, path) claimed path exists but "
"renameat2(fromPath, toPath, EXCAHNGE) said it doest; concurrent modification?",
fromPath, toPath) { return false; }
} else {
// Assume target doesn't exist.
return false;
}
default:
if (errorReason == nullptr) {
KJ_FAIL_SYSCALL("renameat2(fromPath, toPath, EXCHANGE)", error, fromPath, toPath) {
return false;
}
} else {
*errorReason = error;
return false;
}
} else {
// Successful swap! Delete swapped-out content.
rmrf(fromDirFd, fromPath);
return true;
}
} else if (has(mode, WriteMode::CREATE)) {
KJ_SYSCALL_HANDLE_ERRORS(syscall(SYS_renameat2,
fromDirFd, fromPath.cStr(), fd.get(), toPath.cStr(), RENAME_NOREPLACE)) {
case ENOSYS:
break; // fall back to traditional means
case EEXIST:
return false;
default:
if (errorReason == nullptr) {
KJ_FAIL_SYSCALL("renameat2(fromPath, toPath, NOREPLACE)", error, fromPath, toPath) {
return false;
}
} else {
*errorReason = error;
return false;
}
} else {
return true;
}
}
#endif
// We're unable to do what we wanted atomically. :(
if (has(mode, WriteMode::CREATE) && has(mode, WriteMode::MODIFY)) {
// We failed to atomically delete the target previously. So now we need to do two calls in
// rapid succession to move the old file away then move the new one into place.
// Find out what kind of file exists at the target path.
struct stat stats;
KJ_SYSCALL(fstatat(fd, toPath.cStr(), &stats, AT_SYMLINK_NOFOLLOW)) { return false; }
// Create a temporary location to move the existing object to. Note that rename() allows a
// non-directory to replace a non-directory, and allows a directory to replace an empty
// directory. So we have to create the right type.
Path toPathParsed = Path::parse(toPath);
String away;
KJ_IF_MAYBE(awayPath, createNamedTemporary(toPathParsed, WriteMode::CREATE,
[&](StringPtr candidatePath) {
if (S_ISDIR(stats.st_mode)) {
return mkdirat(fd, candidatePath.cStr(), 0700);
} else {
return mknodat(fd, candidatePath.cStr(), S_IFREG | 0600, dev_t());
}
})) {
away = kj::mv(*awayPath);
} else {
// Already threw.
return false;
}
// OK, now move the target object to replace the thing we just created.
KJ_SYSCALL(renameat(fd, toPath.cStr(), fd, away.cStr())) {
// Something went wrong. Remove the thing we just created.
unlinkat(fd, away.cStr(), S_ISDIR(stats.st_mode) ? AT_REMOVEDIR : 0);
return false;
}
// Now move the source object to the target location.
KJ_SYSCALL_HANDLE_ERRORS(renameat(fromDirFd, fromPath.cStr(), fd, toPath.cStr())) {
default:
// Try to put things back where they were. If this fails, though, then we have little
// choice but to leave things broken.
KJ_SYSCALL_HANDLE_ERRORS(renameat(fd, away.cStr(), fd, toPath.cStr())) {
default: break;
}
if (errorReason == nullptr) {
KJ_FAIL_SYSCALL("rename(fromPath, toPath)", error, fromPath, toPath) {
return false;
}
} else {
*errorReason = error;
return false;
}
}
// OK, success. Delete the old content.
rmrf(fd, away);
return true;
} else {
// Only one of CREATE or MODIFY is specified, so we need to verify non-atomically that the
// corresponding precondition (must-not-exist or must-exist, respectively) is held.
if (has(mode, WriteMode::CREATE)) {
struct stat stats;
KJ_SYSCALL_HANDLE_ERRORS(fstatat(fd.get(), toPath.cStr(), &stats, AT_SYMLINK_NOFOLLOW)) {
case ENOENT:
case ENOTDIR:
break; // doesn't exist; continue
default:
KJ_FAIL_SYSCALL("fstatat(fd, toPath)", error, toPath) { return false; }
} else {
return false; // already exists; fail
}
} else if (has(mode, WriteMode::MODIFY)) {
struct stat stats;
KJ_SYSCALL_HANDLE_ERRORS(fstatat(fd.get(), toPath.cStr(), &stats, AT_SYMLINK_NOFOLLOW)) {
case ENOENT:
case ENOTDIR:
return false; // doesn't exist; fail
default:
KJ_FAIL_SYSCALL("fstatat(fd, toPath)", error, toPath) { return false; }
} else {
// already exists; continue
}
} else {
// Neither CREATE nor MODIFY.
return false;
}
// Start over in create-and-modify mode.
return tryCommitReplacement(toPath, fromDirFd, fromPath,
WriteMode::CREATE | WriteMode::MODIFY,
errorReason);
}
}
template <typename T>
class ReplacerImpl: public Directory::Replacer<T> {
public:
ReplacerImpl(Own<T>&& object, DiskHandle& handle,
String&& tempPath, String&& path, WriteMode mode)
: Directory::Replacer<T>(mode),
object(kj::mv(object)), handle(handle),
tempPath(kj::mv(tempPath)), path(kj::mv(path)) {}
~ReplacerImpl() noexcept(false) {
if (!committed) {
rmrf(handle.fd, tempPath);
}
}
T& get() override {
return *object;
}
bool tryCommit() override {
KJ_ASSERT(!committed, "already committed") { return false; }
return committed = handle.tryCommitReplacement(path, handle.fd, tempPath,
Directory::Replacer<T>::mode);
}
private:
Own<T> object;
DiskHandle& handle;
String tempPath;
String path;
bool committed = false; // true if *successfully* committed (in which case tempPath is gone)
};
template <typename T>
class BrokenReplacer: public Directory::Replacer<T> {
// For recovery path when exceptions are disabled.
public:
BrokenReplacer(Own<T> inner)
: Directory::Replacer<T>(WriteMode::CREATE | WriteMode::MODIFY),
inner(kj::mv(inner)) {}
T& get() override { return *inner; }
bool tryCommit() override { return false; }
private:
Own<T> inner;
};
Maybe<Own<File>> tryOpenFile(PathPtr path, WriteMode mode) {
return tryOpenFileInternal(path, mode, false).map(newDiskFile);
}
Own<Directory::Replacer<File>> replaceFile(PathPtr path, WriteMode mode) {
mode_t acl = 0666;
if (has(mode, WriteMode::EXECUTABLE)) {
acl = 0777;
}
if (has(mode, WriteMode::PRIVATE)) {
acl &= 0700;
}
int newFd_;
KJ_IF_MAYBE(temp, createNamedTemporary(path, mode,
[&](StringPtr candidatePath) {
return newFd_ = openat(fd, candidatePath.cStr(),
O_RDWR | O_CREAT | O_EXCL | MAYBE_O_CLOEXEC, acl);
})) {
AutoCloseFd newFd(newFd_);
#ifndef O_CLOEXEC
setCloexec(newFd);
#endif
return heap<ReplacerImpl<File>>(newDiskFile(kj::mv(newFd)), *this, kj::mv(*temp),
path.toString(), mode);
} else {
// threw, but exceptions are disabled
return heap<BrokenReplacer<File>>(newInMemoryFile(nullClock()));
}
}
Own<File> createTemporary() {
int newFd_;
#ifdef O_TMPFILE
// Use syscall() to work around glibc bug with O_TMPFILE:
// https://sourceware.org/bugzilla/show_bug.cgi?id=17523
KJ_SYSCALL_HANDLE_ERRORS(newFd_ = syscall(
SYS_openat, fd.get(), ".", O_RDWR | O_TMPFILE, 0700)) {
case EOPNOTSUPP:
case EINVAL:
// Maybe not supported by this kernel / filesystem. Fall back to below.
break;
default:
KJ_FAIL_SYSCALL("open(O_TMPFILE)", error) { break; }
break;
} else {
AutoCloseFd newFd(newFd_);
#ifndef O_CLOEXEC
setCloexec(newFd);
#endif
return newDiskFile(kj::mv(newFd));
}
#endif
KJ_IF_MAYBE(temp, createNamedTemporary(Path("unnamed"), WriteMode::CREATE,
[&](StringPtr path) {
return newFd_ = openat(fd, path.cStr(), O_RDWR | O_CREAT | O_EXCL | MAYBE_O_CLOEXEC, 0600);
})) {
AutoCloseFd newFd(newFd_);
#ifndef O_CLOEXEC
setCloexec(newFd);
#endif
auto result = newDiskFile(kj::mv(newFd));
KJ_SYSCALL(unlinkat(fd, temp->cStr(), 0)) { break; }
return kj::mv(result);
} else {
// threw, but exceptions are disabled
return newInMemoryFile(nullClock());
}
}
Maybe<Own<AppendableFile>> tryAppendFile(PathPtr path, WriteMode mode) {
return tryOpenFileInternal(path, mode, true).map(newDiskAppendableFile);
}
Maybe<Own<Directory>> tryOpenSubdir(PathPtr path, WriteMode mode) {
// Must create before open.
if (has(mode, WriteMode::CREATE)) {
if (!tryMkdir(path, mode, false)) return nullptr;
}
return tryOpenSubdirInternal(path).map(newDiskDirectory);
}
Own<Directory::Replacer<Directory>> replaceSubdir(PathPtr path, WriteMode mode) {
mode_t acl = has(mode, WriteMode::PRIVATE) ? 0700 : 0777;
KJ_IF_MAYBE(temp, createNamedTemporary(path, mode,
[&](StringPtr candidatePath) {
return mkdirat(fd, candidatePath.cStr(), acl);
})) {
int subdirFd_;
KJ_SYSCALL_HANDLE_ERRORS(subdirFd_ = openat(
fd, temp->cStr(), O_RDONLY | MAYBE_O_CLOEXEC | MAYBE_O_DIRECTORY)) {
default:
KJ_FAIL_SYSCALL("open(just-created-temporary)", error);
return heap<BrokenReplacer<Directory>>(newInMemoryDirectory(nullClock()));
}
AutoCloseFd subdirFd(subdirFd_);
#ifndef O_CLOEXEC
setCloexec(subdirFd);
#endif
return heap<ReplacerImpl<Directory>>(
newDiskDirectory(kj::mv(subdirFd)), *this, kj::mv(*temp), path.toString(), mode);
} else {
// threw, but exceptions are disabled
return heap<BrokenReplacer<Directory>>(newInMemoryDirectory(nullClock()));
}
}
bool trySymlink(PathPtr linkpath, StringPtr content, WriteMode mode) {
return tryReplaceNode(linkpath, mode, [&](StringPtr candidatePath) {
return symlinkat(content.cStr(), fd, candidatePath.cStr());
});
}
bool tryTransfer(PathPtr toPath, WriteMode toMode,
Directory& fromDirectory, PathPtr fromPath,
TransferMode mode, Directory& self) {
KJ_REQUIRE(toPath.size() > 0, "can't replace self") { return false; }
if (mode == TransferMode::LINK) {
KJ_IF_MAYBE(fromFd, fromDirectory.getFd()) {
// Other is a disk directory, so we can hopefully do an efficient move/link.
return tryReplaceNode(toPath, toMode, [&](StringPtr candidatePath) {
return linkat(*fromFd, fromPath.toString().cStr(), fd, candidatePath.cStr(), 0);
});
};
} else if (mode == TransferMode::MOVE) {
KJ_IF_MAYBE(fromFd, fromDirectory.getFd()) {
KJ_ASSERT(mode == TransferMode::MOVE);
int error = 0;
if (tryCommitReplacement(toPath.toString(), *fromFd, fromPath.toString(), toMode,
&error)) {
return true;
} else switch (error) {
case 0:
// Plain old WriteMode precondition failure.
return false;
case EXDEV:
// Can't move between devices. Fall back to default implementation, which does
// copy/delete.
break;
case ENOENT:
// Either the destination directory doesn't exist or the source path doesn't exist.
// Unfortunately we don't really know. If CREATE_PARENT was provided, try creating
// the parent directory. Otherwise, we don't actually need to distinguish between
// these two errors; just return false.
if (has(toMode, WriteMode::CREATE) && has(toMode, WriteMode::CREATE_PARENT) &&
toPath.size() > 0 && tryMkdir(toPath.parent(),
WriteMode::CREATE | WriteMode::MODIFY | WriteMode::CREATE_PARENT, true)) {
// Retry, but make sure we don't try to create the parent again.
return tryTransfer(toPath, toMode - WriteMode::CREATE_PARENT,
fromDirectory, fromPath, mode, self);
}
return false;
default:
KJ_FAIL_SYSCALL("rename(fromPath, toPath)", error, fromPath, toPath) {
return false;
}
}
}
}
// OK, we can't do anything efficient using the OS. Fall back to default implementation.
return self.Directory::tryTransfer(toPath, toMode, fromDirectory, fromPath, mode);
}
bool tryRemove(PathPtr path) {
return rmrf(fd, path.toString());
}
protected:
AutoCloseFd fd;
};
#define FSNODE_METHODS(classname) \
Maybe<int> getFd() override { return DiskHandle::getFd(); } \
\
Own<OsHandle> cloneOsHandle() override { \
return heap<classname>(DiskHandle::clone()); \
} \
\
Metadata stat() override { return DiskHandle::stat(); } \
void sync() override { DiskHandle::sync(); } \
void datasync() override { DiskHandle::datasync(); }
class DiskReadableFile final: public ReadableFile, public DiskHandle {
public:
DiskReadableFile(AutoCloseFd&& fd): DiskHandle(kj::mv(fd)) {}
FSNODE_METHODS(DiskReadableFile);
size_t read(uint64_t offset, ArrayPtr<byte> buffer) override {
return DiskHandle::read(offset, buffer);
}
Array<const byte> mmap(uint64_t offset, uint64_t size) override {
return DiskHandle::mmap(offset, size);
}
Array<byte> mmapPrivate(uint64_t offset, uint64_t size) override {
return DiskHandle::mmapPrivate(offset, size);
}
};
class DiskAppendableFile final: public AppendableFile, public DiskHandle, public FdOutputStream {
public:
DiskAppendableFile(AutoCloseFd&& fd)
: DiskHandle(kj::mv(fd)),
FdOutputStream(DiskHandle::fd.get()) {}
FSNODE_METHODS(DiskAppendableFile);
void write(const void* buffer, size_t size) override { FdOutputStream::write(buffer, size); }
void write(ArrayPtr<const ArrayPtr<const byte>> pieces) override {
FdOutputStream::write(pieces);
}
};
class DiskFile final: public File, public DiskHandle {
public:
DiskFile(AutoCloseFd&& fd): DiskHandle(kj::mv(fd)) {}
FSNODE_METHODS(DiskFile);
size_t read(uint64_t offset, ArrayPtr<byte> buffer) override {
return DiskHandle::read(offset, buffer);
}
Array<const byte> mmap(uint64_t offset, uint64_t size) override {
return DiskHandle::mmap(offset, size);
}
Array<byte> mmapPrivate(uint64_t offset, uint64_t size) override {
return DiskHandle::mmapPrivate(offset, size);
}
void write(uint64_t offset, ArrayPtr<const byte> data) override {
DiskHandle::write(offset, data);
}
void zero(uint64_t offset, uint64_t size) override {
DiskHandle::zero(offset, size);
}
void truncate(uint64_t size) override {
DiskHandle::truncate(size);
}
Own<WritableFileMapping> mmapWritable(uint64_t offset, uint64_t size) override {
return DiskHandle::mmapWritable(offset, size);
}
size_t copy(uint64_t offset, ReadableFile& from, uint64_t fromOffset, uint64_t size) override {
KJ_IF_MAYBE(result, DiskHandle::copy(offset, from, fromOffset, size)) {
return *result;
} else {
return File::copy(offset, from, fromOffset, size);
}
}
};
class DiskReadableDirectory final: public ReadableDirectory, public DiskHandle {
public:
DiskReadableDirectory(AutoCloseFd&& fd): DiskHandle(kj::mv(fd)) {}
FSNODE_METHODS(DiskReadableDirectory);
Array<String> listNames() override { return DiskHandle::listNames(); }
Array<Entry> listEntries() override { return DiskHandle::listEntries(); }
bool exists(PathPtr path) override { return DiskHandle::exists(path); }
Maybe<FsNode::Metadata> tryLstat(PathPtr path) override { return DiskHandle::tryLstat(path); }
Maybe<Own<ReadableFile>> tryOpenFile(PathPtr path) override {
return DiskHandle::tryOpenFile(path);
}
Maybe<Own<ReadableDirectory>> tryOpenSubdir(PathPtr path) override {
return DiskHandle::tryOpenSubdir(path);
}
Maybe<String> tryReadlink(PathPtr path) override { return DiskHandle::tryReadlink(path); }
};
class DiskDirectory final: public Directory, public DiskHandle {
public:
DiskDirectory(AutoCloseFd&& fd): DiskHandle(kj::mv(fd)) {}
FSNODE_METHODS(DiskDirectory);
Array<String> listNames() override { return DiskHandle::listNames(); }
Array<Entry> listEntries() override { return DiskHandle::listEntries(); }
bool exists(PathPtr path) override { return DiskHandle::exists(path); }
Maybe<FsNode::Metadata> tryLstat(PathPtr path) override { return DiskHandle::tryLstat(path); }
Maybe<Own<ReadableFile>> tryOpenFile(PathPtr path) override {
return DiskHandle::tryOpenFile(path);
}
Maybe<Own<ReadableDirectory>> tryOpenSubdir(PathPtr path) override {
return DiskHandle::tryOpenSubdir(path);
}
Maybe<String> tryReadlink(PathPtr path) override { return DiskHandle::tryReadlink(path); }
Maybe<Own<File>> tryOpenFile(PathPtr path, WriteMode mode) override {
return DiskHandle::tryOpenFile(path, mode);
}
Own<Replacer<File>> replaceFile(PathPtr path, WriteMode mode) override {
return DiskHandle::replaceFile(path, mode);
}
Own<File> createTemporary() override {
return DiskHandle::createTemporary();
}
Maybe<Own<AppendableFile>> tryAppendFile(PathPtr path, WriteMode mode) override {
return DiskHandle::tryAppendFile(path, mode);
}
Maybe<Own<Directory>> tryOpenSubdir(PathPtr path, WriteMode mode) override {
return DiskHandle::tryOpenSubdir(path, mode);
}
Own<Replacer<Directory>> replaceSubdir(PathPtr path, WriteMode mode) override {
return DiskHandle::replaceSubdir(path, mode);
}
bool trySymlink(PathPtr linkpath, StringPtr content, WriteMode mode) override {
return DiskHandle::trySymlink(linkpath, content, mode);
}
bool tryTransfer(PathPtr toPath, WriteMode toMode,
Directory& fromDirectory, PathPtr fromPath,
TransferMode mode) override {
return DiskHandle::tryTransfer(toPath, toMode, fromDirectory, fromPath, mode, *this);
}
// tryTransferTo() not implemented because we have nothing special we can do.
bool tryRemove(PathPtr path) override {
return DiskHandle::tryRemove(path);
}
};
} // namespace
Own<ReadableFile> newDiskReadableFile(kj::AutoCloseFd fd) {
return heap<DiskReadableFile>(kj::mv(fd));
}
Own<AppendableFile> newDiskAppendableFile(kj::AutoCloseFd fd) {
return heap<DiskAppendableFile>(kj::mv(fd));
}
Own<File> newDiskFile(kj::AutoCloseFd fd) {
return heap<DiskFile>(kj::mv(fd));
}
Own<ReadableDirectory> newDiskReadableDirectory(kj::AutoCloseFd fd) {
return heap<DiskReadableDirectory>(kj::mv(fd));
}
Own<Directory> newDiskDirectory(kj::AutoCloseFd fd) {
return heap<DiskDirectory>(kj::mv(fd));
}
} // namespace kj
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment