// 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; } // 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() == ""); 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()[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"); } #ifndef HOLES_NOT_SUPPORTED 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