blob: d1d9fa2c98bc57e51b333302bb9c065e585f499b [file] [log] [blame]
// 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 "encoding.h"
#include <stdlib.h>
#if _WIN32
#include <windows.h>
#include "windows-sanity.h"
#else
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#endif
namespace kj {
namespace {
bool isWine() KJ_UNUSED;
#if _WIN32
bool detectWine() {
HMODULE hntdll = GetModuleHandle("ntdll.dll");
if(hntdll == NULL) return false;
return GetProcAddress(hntdll, "wine_get_version") != nullptr;
}
bool isWine() {
static bool result = detectWine();
return result;
}
template <typename Func>
static auto newTemp(Func&& create)
-> Decay<decltype(*kj::_::readMaybe(create(Array<wchar_t>())))> {
wchar_t wtmpdir[MAX_PATH + 1];
DWORD len = GetTempPathW(kj::size(wtmpdir), wtmpdir);
KJ_ASSERT(len < kj::size(wtmpdir));
auto tmpdir = decodeWideString(arrayPtr(wtmpdir, len));
static uint counter = 0;
for (;;) {
auto path = kj::str(tmpdir, "kj-filesystem-test.", GetCurrentProcessId(), ".", counter++);
KJ_IF_MAYBE(result, create(encodeWideString(path, true))) {
return kj::mv(*result);
}
}
}
static Own<File> newTempFile() {
return newTemp([](Array<wchar_t> candidatePath) -> Maybe<Own<File>> {
HANDLE handle;
KJ_WIN32_HANDLE_ERRORS(handle = CreateFileW(
candidatePath.begin(),
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
0,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,
NULL)) {
case ERROR_ALREADY_EXISTS:
case ERROR_FILE_EXISTS:
return nullptr;
default:
KJ_FAIL_WIN32("CreateFileW", error);
}
return newDiskFile(AutoCloseHandle(handle));
});
}
static Array<wchar_t> join16(ArrayPtr<const wchar_t> path, const wchar_t* file) {
// Assumes `path` ends with a NUL terminator (and `file` is of course NUL terminated as well).
size_t len = wcslen(file) + 1;
auto result = kj::heapArray<wchar_t>(path.size() + len);
memcpy(result.begin(), path.begin(), path.asBytes().size() - sizeof(wchar_t));
result[path.size() - 1] = '\\';
memcpy(result.begin() + path.size(), file, len * sizeof(wchar_t));
return result;
}
class TempDir {
public:
TempDir(): filename(newTemp([](Array<wchar_t> candidatePath) -> Maybe<Array<wchar_t>> {
KJ_WIN32_HANDLE_ERRORS(CreateDirectoryW(candidatePath.begin(), NULL)) {
case ERROR_ALREADY_EXISTS:
case ERROR_FILE_EXISTS:
return nullptr;
default:
KJ_FAIL_WIN32("CreateDirectoryW", error);
}
return kj::mv(candidatePath);
})) {}
Own<Directory> get() {
HANDLE handle;
KJ_WIN32(handle = CreateFileW(
filename.begin(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, // apparently, this flag is required for directories
NULL));
return newDiskDirectory(AutoCloseHandle(handle));
}
~TempDir() noexcept(false) {
recursiveDelete(filename);
}
private:
Array<wchar_t> filename;
static void recursiveDelete(ArrayPtr<const wchar_t> path) {
// Recursively delete the temp dir, verifying that no .kj-tmp. files were left over.
//
// Mostly copied from rmrfChildren() in filesystem-win32.c++.
auto glob = join16(path, L"\\*");
WIN32_FIND_DATAW data;
HANDLE handle = FindFirstFileW(glob.begin(), &data);
if (handle == INVALID_HANDLE_VALUE) {
auto error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND) return;
KJ_FAIL_WIN32("FindFirstFile", error, path) { return; }
}
KJ_DEFER(KJ_WIN32(FindClose(handle)) { break; });
do {
// Ignore "." and "..", ugh.
if (data.cFileName[0] == L'.') {
if (data.cFileName[1] == L'\0' ||
(data.cFileName[1] == L'.' && data.cFileName[2] == L'\0')) {
continue;
}
}
String utf8Name = decodeWideString(arrayPtr(data.cFileName, wcslen(data.cFileName)));
KJ_EXPECT(!utf8Name.startsWith(".kj-tmp."), "temp file not cleaned up", utf8Name);
auto child = join16(path, data.cFileName);
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
!(data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
recursiveDelete(child);
} else {
KJ_WIN32(DeleteFileW(child.begin()));
}
} while (FindNextFileW(handle, &data));
auto error = GetLastError();
if (error != ERROR_NO_MORE_FILES) {
KJ_FAIL_WIN32("FindNextFile", error, path) { return; }
}
uint retryCount = 0;
retry:
KJ_WIN32_HANDLE_ERRORS(RemoveDirectoryW(path.begin())) {
case ERROR_DIR_NOT_EMPTY:
if (retryCount++ < 10) {
Sleep(10);
goto retry;
}
KJ_FALLTHROUGH;
default:
KJ_FAIL_WIN32("RemoveDirectory", error) { break; }
}
}
};
#else
bool isWine() { return false; }
#if __APPLE__ || __CYGWIN__
#define HOLES_NOT_SUPPORTED 1
#endif
#if __ANDROID__
#define VAR_TMP "/data/local/tmp"
#else
#define VAR_TMP "/var/tmp"
#endif
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()));
}
};
#endif // _WIN32, else
KJ_TEST("DiskFile") {
auto file = newTempFile();
KJ_EXPECT(file->readAllText() == "");
// mmaping empty file should work
KJ_EXPECT(file->mmap(0, 0).size() == 0);
KJ_EXPECT(file->mmapPrivate(0, 0).size() == 0);
KJ_EXPECT(file->mmapWritable(0, 0)->get().size() == 0);
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));
// empty mappings work, even if useless
KJ_EXPECT(file->mmap(0, 0).size() == 0);
KJ_EXPECT(file->mmapPrivate(0, 0).size() == 0);
KJ_EXPECT(file->mmapWritable(0, 0)->get().size() == 0);
KJ_EXPECT(file->mmap(2, 0).size() == 0);
KJ_EXPECT(file->mmapPrivate(2, 0).size() == 0);
KJ_EXPECT(file->mmapWritable(2, 0)->get().size() == 0);
{
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()[1] = 'D';
writableMapping->changed(writableMapping->get().slice(1, 2));
KJ_EXPECT(kj::str(mapping.slice(0, 6).asChars()) == "fDobaz");
KJ_EXPECT(kj::str(writableMapping->get().slice(0, 6).asChars()) == "fDobaz");
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");
#if !_WIN32 && !__CYGWIN__ // Windows doesn't allow the file size to change while mapped.
// 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);
#endif
}
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_RECOVERABLE_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"})));
#if _WIN32
// On Windows, we can't delete a directory while we still have it open.
subdir = nullptr;
#endif
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")));
}
#if !_WIN32 // Creating symlinks on Win32 requires admin privileges prior to Windows 10.
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_RECOVERABLE_MESSAGE("parent is not a directory",
dir->tryOpenFile(Path("foo"), WriteMode::CREATE | WriteMode::MODIFY));
KJ_EXPECT_THROW_RECOVERABLE_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);
}
#endif
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 copy-replace") {
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->openFile(Path({"link", "corge"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("abcd");
// CREATE fails.
KJ_EXPECT(!dst->tryTransfer(Path("link"), WriteMode::CREATE,
*src, Path("foo"), TransferMode::COPY));
// Verify nothing changed.
KJ_EXPECT(dst->openFile(Path({"link", "corge"}))->readAllText() == "abcd");
KJ_EXPECT(!dst->exists(Path({"foo", "bar"})));
// Now try MODIFY.
dst->transfer(Path("link"), WriteMode::MODIFY, *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", "corge"})));
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 move-replace") {
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->openFile(Path({"link", "corge"}), WriteMode::CREATE | WriteMode::CREATE_PARENT)
->writeAll("abcd");
// CREATE fails.
KJ_EXPECT(!dst->tryTransfer(Path("link"), WriteMode::CREATE,
*src, Path("foo"), TransferMode::MOVE));
// Verify nothing changed.
KJ_EXPECT(dst->openFile(Path({"link", "corge"}))->readAllText() == "abcd");
KJ_EXPECT(!dst->exists(Path({"foo", "bar"})));
KJ_EXPECT(src->exists(Path({"foo"})));
// Now try MODIFY.
dst->transfer(Path("link"), WriteMode::MODIFY, *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);
}
#if !__CYGWIN__ // TODO(someday): Figure out why this doesn't work on Cygwin.
KJ_TEST("DiskDirectory replaceSubdir()") {
TempDir tempDir;
auto dir = tempDir.get();
{
auto replacer = dir->replaceSubdir(Path("foo"), WriteMode::CREATE);
replacer->get().openFile(Path("bar"), WriteMode::CREATE)->writeAll("original");
KJ_EXPECT(replacer->get().openFile(Path("bar"))->readAllText() == "original");
KJ_EXPECT(!dir->exists(Path({"foo", "bar"})));
replacer->commit();
KJ_EXPECT(replacer->get().openFile(Path("bar"))->readAllText() == "original");
KJ_EXPECT(dir->openFile(Path({"foo", "bar"}))->readAllText() == "original");
}
{
// CREATE fails -- already exists.
auto replacer = dir->replaceSubdir(Path("foo"), WriteMode::CREATE);
replacer->get().openFile(Path("corge"), WriteMode::CREATE)->writeAll("bazqux");
KJ_EXPECT(dir->listNames().size() == 1 && dir->listNames()[0] == "foo");
KJ_EXPECT(!replacer->tryCommit());
}
// Unchanged.
KJ_EXPECT(dir->openFile(Path({"foo", "bar"}))->readAllText() == "original");
KJ_EXPECT(!dir->exists(Path({"foo", "corge"})));
{
// MODIFY succeeds.
auto replacer = dir->replaceSubdir(Path("foo"), WriteMode::MODIFY);
replacer->get().openFile(Path("corge"), WriteMode::CREATE)->writeAll("bazqux");
KJ_EXPECT(dir->listNames().size() == 1 && dir->listNames()[0] == "foo");
replacer->commit();
}
// Replaced with new contents.
KJ_EXPECT(!dir->exists(Path({"foo", "bar"})));
KJ_EXPECT(dir->openFile(Path({"foo", "corge"}))->readAllText() == "bazqux");
}
#endif // !__CYGWIN__
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");
}
#if !defined(HOLES_NOT_SUPPORTED) && (CAPNP_DEBUG_TYPES || CAPNP_EXPENSIVE_TESTS)
// Not all filesystems support sparse files, and if they do, they don't necessarily support
// copying them in a way that preserves holes. We don't want the capnp test suite to fail just
// because it was run on the wrong filesystem. We could design the test to check first if the
// filesystem supports holes, but the code to do that would be almost the same as the code being
// tested... Instead, we've marked this test so it only runs when building this library using
// defines that only the Cap'n Proto maintainers use. So, we run the test ourselves but we don't
// make other people run it.
KJ_TEST("DiskFile holes") {
if (isWine()) {
// WINE doesn't support sparse files.
return;
}
TempDir tempDir;
auto dir = tempDir.get();
auto file = dir->openFile(Path("holes"), WriteMode::CREATE);
#if _WIN32
FILE_SET_SPARSE_BUFFER sparseInfo;
memset(&sparseInfo, 0, sizeof(sparseInfo));
sparseInfo.SetSparse = TRUE;
DWORD dummy;
KJ_WIN32(DeviceIoControl(
KJ_ASSERT_NONNULL(file->getWin32Handle()),
FSCTL_SET_SPARSE, &sparseInfo, sizeof(sparseInfo),
NULL, 0, &dummy, NULL));
#endif
file->writeAll("foobar");
file->write(1 << 20, StringPtr("foobar").asBytes());
// Some filesystems, like BTRFS, report zero `spaceUsed` until synced.
file->datasync();
// Allow for block sizes as low as 512 bytes and as high as 64k.
auto meta = file->stat();
KJ_EXPECT(meta.spaceUsed >= 2 * 512, meta.spaceUsed);
KJ_EXPECT(meta.spaceUsed <= 2 * 65536);
byte buf[7];
#if !_WIN32 // Win32 CopyFile() does NOT preserve sparseness.
{
// 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));
}
#endif
file->truncate(1 << 21);
file->datasync();
KJ_EXPECT(file->stat().spaceUsed == meta.spaceUsed);
KJ_EXPECT(file->read(1 << 20, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == "foobar");
#if !_WIN32 // Win32 CopyFile() does NOT preserve sparseness.
{
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
// Try punching a hole with zero().
#if _WIN32
uint64_t blockSize = 4096; // TODO(someday): Actually ask the OS.
#else
struct stat stats;
KJ_SYSCALL(fstat(KJ_ASSERT_NONNULL(file->getFd()), &stats));
uint64_t blockSize = stats.st_blksize;
#endif
file->zero(1 << 20, blockSize);
file->datasync();
#if !_WIN32
// TODO(someday): This doesn't work on Windows. I don't know why. We're definitely using the
// proper ioctl. Oh well.
KJ_EXPECT(file->stat().spaceUsed < meta.spaceUsed);
#endif
KJ_EXPECT(file->read(1 << 20, buf) == 7);
KJ_EXPECT(StringPtr(reinterpret_cast<char*>(buf), 6) == StringPtr("\0\0\0\0\0\0", 6));
}
#endif
} // namespace
} // namespace kj