Make CryNode::rename() more resilient to corner cases, e.g.

- when trying to move a directory into a subdirectory of itself
- make it into its own ancestor
- allow replacing a directory with another directory, but only if the target directory is empty

Nothing here should have an effect on overall CryFS behavior because fuse already catches those corner cases.
But it's better to handle them correctly. Better safe than sorry.
This commit is contained in:
Sebastian Messmer 2022-12-18 14:57:38 +01:00
parent cd5ac83b7d
commit 2425ce4bc0
17 changed files with 323 additions and 143 deletions

View File

@ -191,12 +191,17 @@ optional<unique_ref<fspp::Node>> CryDevice::Load(const bf::path &path) {
if (path.parent_path().empty()) {
//We are asked to load the base directory '/'.
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryDir>(this, none, none, _rootBlobId));
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryDir>(this, none, none, _rootBlobId, std::vector<BlockId>()));
}
auto parentWithGrandparent = LoadDirBlobWithParent(path.parent_path());
auto parent = std::move(parentWithGrandparent.blob);
auto grandparent = std::move(parentWithGrandparent.parent);
auto parentWithAncestors = LoadDirBlobWithAncestors(path.parent_path());
if (parentWithAncestors == none) {
return none;
}
auto parent = std::move(parentWithAncestors->blob);
auto grandparent = std::move(parentWithAncestors->parent);
auto ancestors = std::move(parentWithAncestors->ancestors);
ancestors.push_back(parent->blockId()); // parent's ancestors don't contain parent yet, but parent is our ancestor
auto optEntry = parent->GetChild(path.filename().string());
if (optEntry == boost::none) {
@ -206,25 +211,29 @@ optional<unique_ref<fspp::Node>> CryDevice::Load(const bf::path &path) {
switch(entry.type()) {
case fspp::Dir::EntryType::DIR:
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryDir>(this, std::move(parent), std::move(grandparent), entry.blockId()));
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryDir>(this, std::move(parent), std::move(grandparent), entry.blockId(), std::move(ancestors)));
case fspp::Dir::EntryType::FILE:
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryFile>(this, std::move(parent), std::move(grandparent), entry.blockId()));
return optional<unique_ref<fspp::Node>>(make_unique_ref<CryFile>(this, std::move(parent), std::move(grandparent), entry.blockId(), std::move(ancestors)));
case fspp::Dir::EntryType::SYMLINK:
return optional<unique_ref<fspp::Node>>(make_unique_ref<CrySymlink>(this, std::move(parent), std::move(grandparent), entry.blockId()));
return optional<unique_ref<fspp::Node>>(make_unique_ref<CrySymlink>(this, std::move(parent), std::move(grandparent), entry.blockId(), std::move(ancestors)));
}
ASSERT(false, "Switch/case not exhaustive");
}
CryDevice::DirBlobWithParent CryDevice::LoadDirBlobWithParent(const bf::path &path) {
auto blob = LoadBlobWithParent(path);
auto dir = dynamic_pointer_move<DirBlobRef>(blob.blob);
optional<CryDevice::DirBlobWithAncestors> CryDevice::LoadDirBlobWithAncestors(const bf::path &path) {
auto blob = LoadBlobWithAncestors(path);
if (blob == none) {
return none;
}
auto dir = dynamic_pointer_move<DirBlobRef>(blob->blob);
if (dir == none) {
throw FuseErrnoException(ENOTDIR); // Loaded blob is not a directory
}
return DirBlobWithParent{std::move(*dir), std::move(blob.parent)};
return DirBlobWithAncestors{std::move(*dir), std::move(blob->parent), std::move(blob->ancestors)};
}
CryDevice::BlobWithParent CryDevice::LoadBlobWithParent(const bf::path &path) {
optional<CryDevice::BlobWithAncestors> CryDevice::LoadBlobWithAncestors(const bf::path &path) {
std::vector<blockstore::BlockId> ancestors;
optional<unique_ref<DirBlobRef>> parentBlob = none;
optional<unique_ref<FsBlobRef>> currentBlobOpt = _fsBlobStore->load(_rootBlobId);
if (currentBlobOpt == none) {
@ -235,6 +244,7 @@ CryDevice::BlobWithParent CryDevice::LoadBlobWithParent(const bf::path &path) {
ASSERT(currentBlob->parentPointer() == BlockId::Null(), "Root Blob should have a nullptr as parent");
for (const bf::path &component : path.relative_path()) {
ancestors.push_back(currentBlob->blockId());
auto currentDir = dynamic_pointer_move<DirBlobRef>(currentBlob);
if (currentDir == none) {
throw FuseErrnoException(ENOTDIR); // Path component is not a dir
@ -242,19 +252,20 @@ CryDevice::BlobWithParent CryDevice::LoadBlobWithParent(const bf::path &path) {
auto childOpt = (*currentDir)->GetChild(component.string());
if (childOpt == boost::none) {
throw FuseErrnoException(ENOENT); // Child entry in directory not found
// Child entry in directory not found
return none;
}
BlockId childId = childOpt->blockId();
auto nextBlob = _fsBlobStore->load(childId);
if (nextBlob == none) {
throw FuseErrnoException(ENOENT); // Blob for directory entry not found
throw FuseErrnoException(EIO); // Blob for directory entry not found
}
parentBlob = std::move(*currentDir);
currentBlob = std::move(*nextBlob);
ASSERT(currentBlob->parentPointer() == (*parentBlob)->blockId(), "Blob has wrong parent pointer");
}
return BlobWithParent{std::move(currentBlob), std::move(parentBlob)};
return BlobWithAncestors{std::move(currentBlob), std::move(parentBlob), std::move(ancestors)};
//TODO (I think this is resolved, but I should test it)
// Running the python script, waiting for "Create files in sequential order...", then going into dir ~/tmp/cryfs-mount-.../Bonnie.../ and calling "ls"

View File

@ -28,11 +28,12 @@ public:
cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> CreateDirBlob(const blockstore::BlockId &parent);
cpputils::unique_ref<parallelaccessfsblobstore::SymlinkBlobRef> CreateSymlinkBlob(const boost::filesystem::path &target, const blockstore::BlockId &parent);
cpputils::unique_ref<parallelaccessfsblobstore::FsBlobRef> LoadBlob(const blockstore::BlockId &blockId);
struct DirBlobWithParent {
struct DirBlobWithAncestors {
cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> blob;
boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent;
std::vector<blockstore::BlockId> ancestors;
};
DirBlobWithParent LoadDirBlobWithParent(const boost::filesystem::path &path);
boost::optional<DirBlobWithAncestors> LoadDirBlobWithAncestors(const boost::filesystem::path &path);
void RemoveBlob(const blockstore::BlockId &blockId);
void onFsAction(std::function<void()> callback);
@ -65,11 +66,12 @@ private:
static cpputils::unique_ref<blockstore::BlockStore2> CreateIntegrityEncryptedBlockStore(cpputils::unique_ref<blockstore::BlockStore2> blockStore, const LocalStateDir& localStateDir, CryConfigFile *configFile, uint32_t myClientId, bool allowIntegrityViolations, bool missingBlockIsIntegrityViolation, std::function<void()> onIntegrityViolation);
static cpputils::unique_ref<blockstore::BlockStore2> CreateEncryptedBlockStore(const CryConfig &config, cpputils::unique_ref<blockstore::BlockStore2> baseBlockStore);
struct BlobWithParent {
struct BlobWithAncestors {
cpputils::unique_ref<parallelaccessfsblobstore::FsBlobRef> blob;
boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent;
std::vector<blockstore::BlockId> ancestors;
};
BlobWithParent LoadBlobWithParent(const boost::filesystem::path &path);
boost::optional<BlobWithAncestors> LoadBlobWithAncestors(const boost::filesystem::path &path);
DISALLOW_COPY_AND_ASSIGN(CryDevice);
};

View File

@ -28,8 +28,8 @@ using cryfs::parallelaccessfsblobstore::DirBlobRef;
namespace cryfs {
CryDir::CryDir(CryDevice *device, optional<unique_ref<DirBlobRef>> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId)
: CryNode(device, std::move(parent), std::move(grandparent), blockId) {
CryDir::CryDir(CryDevice *device, optional<unique_ref<DirBlobRef>> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId, std::vector<BlockId> ancestors)
: CryNode(device, std::move(parent), std::move(grandparent), blockId, std::move(ancestors)) {
}
CryDir::~CryDir() {
@ -81,6 +81,11 @@ vector<fspp::Dir::Entry> CryDir::children() {
return children;
}
size_t CryDir::numChildren() {
auto blob = LoadBlob();
return blob->NumChildren();
}
fspp::Dir::EntryType CryDir::getType() const {
device()->callFsActionCallbacks();
return fspp::Dir::EntryType::DIR;

View File

@ -10,7 +10,7 @@ namespace cryfs {
class CryDir final: public fspp::Dir, public CryNode {
public:
CryDir(CryDevice *device, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId);
CryDir(CryDevice *device, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId, std::vector<blockstore::BlockId> ancestors);
~CryDir();
//TODO return type variance to CryFile/CryDir?
@ -20,6 +20,7 @@ public:
//TODO Make Entry a public class instead of hidden in DirBlob (which is not publicly visible)
std::vector<fspp::Dir::Entry> children() override;
size_t numChildren();
fspp::Dir::EntryType getType() const override;

View File

@ -18,8 +18,8 @@ using cryfs::parallelaccessfsblobstore::FileBlobRef;
namespace cryfs {
CryFile::CryFile(CryDevice *device, unique_ref<DirBlobRef> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId)
: CryNode(device, std::move(parent), std::move(grandparent), blockId) {
CryFile::CryFile(CryDevice *device, unique_ref<DirBlobRef> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId, std::vector<BlockId> ancestors)
: CryNode(device, std::move(parent), std::move(grandparent), blockId, std::move(ancestors)) {
}
CryFile::~CryFile() {

View File

@ -11,7 +11,7 @@ namespace cryfs {
class CryFile final: public fspp::File, public CryNode {
public:
CryFile(CryDevice *device, cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId);
CryFile(CryDevice *device, cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId, std::vector<blockstore::BlockId> ancestors);
~CryFile();
cpputils::unique_ref<fspp::OpenFile> open(fspp::openflags_t flags) override;

View File

@ -14,6 +14,7 @@ namespace bf = boost::filesystem;
using blockstore::BlockId;
using cpputils::unique_ref;
using cpputils::dynamic_pointer_move;
using boost::optional;
using boost::none;
using std::shared_ptr;
@ -26,10 +27,11 @@ using fspp::fuse::FuseErrnoException;
namespace cryfs {
CryNode::CryNode(CryDevice *device, optional<unique_ref<DirBlobRef>> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId)
CryNode::CryNode(CryDevice *device, optional<unique_ref<DirBlobRef>> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId, std::vector<BlockId> ancestors)
: _device(device),
_parent(none),
_grandparent(none),
_ancestors(std::move(ancestors)),
_blockId(blockId) {
ASSERT(parent != none || grandparent == none, "Grandparent can only be set when parent is not none");
@ -78,12 +80,28 @@ fspp::TimestampUpdateBehavior CryNode::timestampUpdateBehavior() const {
void CryNode::rename(const bf::path &to) {
device()->callFsActionCallbacks();
if (_parent == none) {
//We are the root direcory.
// We are the root direcory.
throw FuseErrnoException(EBUSY);
}
auto targetDirWithParent = _device->LoadDirBlobWithParent(to.parent_path());
auto targetDir = std::move(targetDirWithParent.blob);
auto targetDirParent = std::move(targetDirWithParent.parent);
if (!to.has_parent_path()) {
// Target is the root directory
throw FuseErrnoException(EBUSY);
}
auto targetParentAndAncestors = _device->LoadDirBlobWithAncestors(to.parent_path());
if (targetParentAndAncestors == none) {
// Target parent directory doesn't exist
throw FuseErrnoException(ENOENT);
}
auto targetParent = std::move(targetParentAndAncestors->blob);
auto targetGrandparent = std::move(targetParentAndAncestors->parent);
auto targetAncestors = std::move(targetParentAndAncestors->ancestors);
targetAncestors.push_back(targetParent->blockId()); // targetParent is not an ancestor of the targetParent, but it's a target ancestor
if (std::find(targetAncestors.begin(), targetAncestors.end(), _blockId) != targetAncestors.end()) {
// We are trying to move a node into one of its subdirectories. This is not allowed.
throw FuseErrnoException(EINVAL);
}
auto old = (*_parent)->GetChild(_blockId);
if (old == boost::none) {
@ -93,17 +111,36 @@ void CryNode::rename(const bf::path &to) {
auto onOverwritten = [this] (const blockstore::BlockId &blockId) {
device()->RemoveBlob(blockId);
};
_updateParentModificationTimestamp();
if (targetDir->blockId() == (*_parent)->blockId()) {
targetDir->RenameChild(oldEntry.blockId(), to.filename().string(), onOverwritten);
if (targetParent->blockId() == (*_parent)->blockId()) {
_updateParentModificationTimestamp();
targetParent->RenameChild(oldEntry.blockId(), to.filename().string(), onOverwritten);
} else {
_updateTargetDirModificationTimestamp(*targetDir, std::move(targetDirParent));
targetDir->AddOrOverwriteChild(to.filename().string(), oldEntry.blockId(), oldEntry.type(), oldEntry.mode(), oldEntry.uid(), oldEntry.gid(),
oldEntry.lastAccessTime(), oldEntry.lastModificationTime(), onOverwritten);
auto preexistingTargetEntry = targetParent->GetChild(to.filename().string());
if (preexistingTargetEntry != boost::none && preexistingTargetEntry->type() == fspp::Dir::EntryType::DIR) {
if (getType() != fspp::Dir::EntryType::DIR) {
// A directory cannot be overwritten with a non-directory
throw FuseErrnoException(EISDIR);
}
auto preexistingTarget = device()->LoadBlob(preexistingTargetEntry->blockId());
auto preexistingTargetDir = dynamic_pointer_move<DirBlobRef>(preexistingTarget);
if (preexistingTargetDir == none) {
LOG(ERR, "Preexisting target is not a directory. But its parent dir entry says it's a directory");
throw FuseErrnoException(EIO);
}
if ((*preexistingTargetDir)->NumChildren() > 0) {
// Cannot overwrite a non-empty dir with a rename operation.
throw FuseErrnoException(ENOTEMPTY);
}
}
_updateParentModificationTimestamp();
_updateTargetDirModificationTimestamp(*targetParent, std::move(targetGrandparent));
targetParent->AddOrOverwriteChild(to.filename().string(), oldEntry.blockId(), oldEntry.type(), oldEntry.mode(), oldEntry.uid(), oldEntry.gid(),
oldEntry.lastAccessTime(), oldEntry.lastModificationTime(), onOverwritten);
(*_parent)->RemoveChild(oldEntry.name());
// targetDir is now the new parent for this node. Adapt to it, so we can call further operations on this node object.
LoadBlob()->setParentPointer(targetDir->blockId());
_parent = std::move(targetDir);
// targetParent is now the new parent for this node. Adapt to it, so we can call further operations on this node object.
LoadBlob()->setParentPointer(targetParent->blockId());
_parent = std::move(targetParent);
}
}

View File

@ -15,7 +15,7 @@ public:
virtual ~CryNode();
// TODO grandparent is only needed to set the timestamps of the parent directory on rename and remove. Delete grandparent parameter once we store timestamps in the blob itself instead of in the directory listing.
CryNode(CryDevice *device, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId);
CryNode(CryDevice *device, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId, std::vector<blockstore::BlockId> ancestors);
void access(int mask) const override;
stat_info stat() const override;
@ -51,6 +51,7 @@ private:
CryDevice *_device;
boost::optional<std::shared_ptr<parallelaccessfsblobstore::DirBlobRef>> _parent;
boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> _grandparent;
std::vector<blockstore::BlockId> _ancestors; // [n-1] is the parent, [n-2] is the grandparent, ..., [0] is the file system root,
blockstore::BlockId _blockId;
DISALLOW_COPY_AND_ASSIGN(CryNode);

View File

@ -21,8 +21,8 @@ using cryfs::parallelaccessfsblobstore::DirBlobRef;
namespace cryfs {
CrySymlink::CrySymlink(CryDevice *device, unique_ref<DirBlobRef> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId)
: CryNode(device, std::move(parent), std::move(grandparent), blockId) {
CrySymlink::CrySymlink(CryDevice *device, unique_ref<DirBlobRef> parent, optional<unique_ref<DirBlobRef>> grandparent, const BlockId &blockId, std::vector<blockstore::BlockId> ancestors)
: CryNode(device, std::move(parent), std::move(grandparent), blockId, std::move(ancestors)) {
}
CrySymlink::~CrySymlink() {

View File

@ -11,7 +11,7 @@ namespace cryfs {
class CrySymlink final: public fspp::Symlink, public CryNode {
public:
CrySymlink(CryDevice *device, cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId);
CrySymlink(CryDevice *device, cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef> parent, boost::optional<cpputils::unique_ref<parallelaccessfsblobstore::DirBlobRef>> grandparent, const blockstore::BlockId &blockId, std::vector<blockstore::BlockId> ancestors);
~CrySymlink();
boost::filesystem::path target() override;

View File

@ -175,34 +175,22 @@ TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromRootDir_LoadSymlink) {
TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromNonexistingDir_Load) {
this->InitDirStructure();
//TODO Change as soon as we have a concept of how to handle filesystem errors in the interface
EXPECT_ANY_THROW(
this->device->Load("/nonexisting/nonexisting2")
);
EXPECT_EQ(boost::none, this->device->Load("/nonexisting/nonexisting2"));
}
TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromNonexistingDir_LoadDir) {
this->InitDirStructure();
//TODO Change as soon as we have a concept of how to handle filesystem errors in the interface
EXPECT_ANY_THROW(
this->device->LoadDir("/nonexisting/nonexisting2")
);
EXPECT_EQ(boost::none, this->device->LoadDir("/nonexisting/nonexisting2"));
}
TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromNonexistingDir_LoadFile) {
this->InitDirStructure();
//TODO Change as soon as we have a concept of how to handle filesystem errors in the interface
EXPECT_ANY_THROW(
this->device->LoadFile("/nonexisting/nonexisting2")
);
EXPECT_EQ(boost::none, this->device->LoadFile("/nonexisting/nonexisting2"));
}
TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromNonexistingDir_LoadSymlink) {
this->InitDirStructure();
//TODO Change as soon as we have a concept of how to handle filesystem errors in the interface
EXPECT_ANY_THROW(
this->device->LoadSymlink("/nonexisting/nonexisting2")
);
EXPECT_EQ(boost::none, this->device->LoadSymlink("/nonexisting/nonexisting2"));
}
TYPED_TEST_P(FsppDeviceTest_One, LoadNonexistingFromExistingDir_Load) {

View File

@ -16,8 +16,10 @@ set(SOURCES
impl/config/CryConfigConsoleTest.cpp
impl/config/CryPasswordBasedKeyProviderTest.cpp
impl/config/CryPresetPasswordBasedKeyProviderTest.cpp
impl/filesystem/testutils/CryTestBase.cpp
impl/filesystem/CryFsTest.cpp
impl/filesystem/CryNodeTest.cpp
impl/filesystem/CryNodeTest_Rename.cpp
impl/filesystem/CryNodeTest_RenameNested.cpp
impl/filesystem/FileSystemTest.cpp
impl/localstate/LocalStateMetadataTest.cpp
impl/localstate/BasedirMetadataTest.cpp

View File

@ -1,83 +0,0 @@
#include <gtest/gtest.h>
#include "testutils/CryTestBase.h"
#include <cryfs/impl/filesystem/CryDir.h>
#include <cryfs/impl/filesystem/CryFile.h>
#include <cryfs/impl/filesystem/CryOpenFile.h>
using cpputils::unique_ref;
using cpputils::dynamic_pointer_move;
using namespace cryfs;
namespace bf = boost::filesystem;
// Many generic (black box) test cases for FsppNode are covered in Fspp fstest.
// This class adds some tests that need insight into how CryFS works.
class CryNodeTest : public ::testing::Test, public CryTestBase {
public:
static constexpr fspp::mode_t MODE_PUBLIC = fspp::mode_t()
.addUserReadFlag().addUserWriteFlag().addUserExecFlag()
.addGroupReadFlag().addGroupWriteFlag().addGroupExecFlag()
.addOtherReadFlag().addOtherWriteFlag().addOtherExecFlag();
unique_ref<CryNode> CreateFile(const bf::path &path) {
auto parentDir = device().LoadDir(path.parent_path()).value();
parentDir->createAndOpenFile(path.filename().string(), MODE_PUBLIC, fspp::uid_t(0), fspp::gid_t(0));
auto file = device().Load(path).value();
return dynamic_pointer_move<CryNode>(file).value();
}
unique_ref<CryNode> CreateDir(const bf::path &path) {
auto _parentDir = device().Load(path.parent_path()).value();
auto parentDir = dynamic_pointer_move<CryDir>(_parentDir).value();
parentDir->createDir(path.filename().string(), MODE_PUBLIC, fspp::uid_t(0), fspp::gid_t(0));
auto createdDir = device().Load(path).value();
return dynamic_pointer_move<CryNode>(createdDir).value();
}
unique_ref<CryNode> CreateSymlink(const bf::path &path) {
auto _parentDir = device().Load(path.parent_path()).value();
auto parentDir = dynamic_pointer_move<CryDir>(_parentDir).value();
parentDir->createSymlink(path.filename().string(), "/target", fspp::uid_t(0), fspp::gid_t(0));
auto createdSymlink = device().Load(path).value();
return dynamic_pointer_move<CryNode>(createdSymlink).value();
}
};
constexpr fspp::mode_t CryNodeTest::MODE_PUBLIC;
TEST_F(CryNodeTest, Rename_DoesntLeaveBlocksOver) {
auto node = CreateFile("/oldname");
EXPECT_EQ(2u, device().numBlocks()); // In the beginning, there is two blocks (the root block and the created file). If that is not true anymore, we'll have to adapt the test case.
node->rename("/newname");
EXPECT_EQ(2u, device().numBlocks()); // Still same number of blocks
}
// TODO Add similar test cases (i.e. checking number of blocks) for other situations in rename, and also for other operations (e.g. deleting files).
TEST_F(CryNodeTest, Rename_Overwrite_DoesntLeaveBlocksOver) {
auto node = CreateFile("/oldname");
CreateFile("/newexistingname");
EXPECT_EQ(3u, device().numBlocks()); // In the beginning, there is three blocks (the root block and the two created files). If that is not true anymore, we'll have to adapt the test case.
node->rename("/newexistingname");
EXPECT_EQ(2u, device().numBlocks()); // Only the blocks of one file are left
}
TEST_F(CryNodeTest, Rename_UpdatesParentPointers_File) {
this->CreateDir("/mydir");
auto node = this->CreateFile("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}
TEST_F(CryNodeTest, Rename_UpdatesParentPointers_Dir) {
this->CreateDir("/mydir");
auto node = this->CreateDir("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}
TEST_F(CryNodeTest, Rename_UpdatesParentPointers_Symlink) {
this->CreateDir("/mydir");
auto node = this->CreateSymlink("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}

View File

@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include "testutils/CryTestBase.h"
#include <cryfs/impl/filesystem/CryOpenFile.h>
using cpputils::unique_ref;
using cpputils::dynamic_pointer_move;
using namespace cryfs;
namespace bf = boost::filesystem;
// Many generic (black box) test cases for FsppNode are covered in Fspp fstest.
// This class adds some tests that need insight into how CryFS works.
class CryNodeTest_Rename : public ::testing::Test, public CryTestBase {
};
TEST_F(CryNodeTest_Rename, DoesntLeaveBlocksOver) {
auto node = CreateFile("/oldname");
EXPECT_EQ(2u, device().numBlocks()); // In the beginning, there is two blocks (the root block and the created file). If that is not true anymore, we'll have to adapt the test case.
node->rename("/newname");
EXPECT_EQ(2u, device().numBlocks()); // Still same number of blocks
}
// TODO Add similar test cases (i.e. checking number of blocks) for other situations in rename, and also for other operations (e.g. deleting files).
TEST_F(CryNodeTest_Rename, Overwrite_DoesntLeaveBlocksOver) {
auto node = CreateFile("/oldname");
CreateFile("/newexistingname");
EXPECT_EQ(3u, device().numBlocks()); // In the beginning, there is three blocks (the root block and the two created files). If that is not true anymore, we'll have to adapt the test case.
node->rename("/newexistingname");
EXPECT_EQ(2u, device().numBlocks()); // Only the blocks of one file are left
}
TEST_F(CryNodeTest_Rename, UpdatesParentPointers_File) {
this->CreateDir("/mydir");
auto node = this->CreateFile("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}
TEST_F(CryNodeTest_Rename, UpdatesParentPointers_Dir) {
this->CreateDir("/mydir");
auto node = this->CreateDir("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}
TEST_F(CryNodeTest_Rename, UpdatesParentPointers_Symlink) {
this->CreateDir("/mydir");
auto node = this->CreateSymlink("/oldname");
node->rename("/mydir/newname");
EXPECT_TRUE(node->checkParentPointer());
}

View File

@ -0,0 +1,126 @@
// This test tests various ways of renaming files in nested directory structures.
// It tests both that the rename operation succeeds, and that it doesn't deadlock
// This is important because our CryNode implementation accesses multiple blobs
// (source, source_parent, target_parent, target_grandparent) and if any of those
// overlap, we need to make sure that we don't deadlock by trying to load them
// at the same time. This is also why these tests nest quite deeply.
#include <gtest/gtest.h>
#include "testutils/CryTestBase.h"
#include <cryfs/impl/filesystem/CryDir.h>
#include <cryfs/impl/filesystem/CryFile.h>
#include <cryfs/impl/filesystem/CryOpenFile.h>
#include <fspp/fs_interface/FuseErrnoException.h>
#include <boost/algorithm/string/predicate.hpp>
using ::testing::Eq;
using ::testing::Return;
using ::testing::Combine;
using ::testing::ValuesIn;
using ::testing::TestWithParam;
using std::vector;
namespace bf = boost::filesystem;
using fspp::fuse::FuseErrnoException;
namespace {
vector<bf::path> SourceDirs() {
return {
"/",
"/a1",
"/a1/b1",
"/a1/b1/c1",
"/a1/b1/c1/d1",
"/a1/b1/c1/d1/e1",
"/a1/b1/c1/d1/e1/f1",
};
}
vector<bf::path> DestDirs() {
auto result = SourceDirs();
result.push_back("/a2");
result.push_back("/a2/b");
result.push_back("/a2/b/c");
result.push_back("/a2/b/c/d");
result.push_back("/a2/b/c/d/e");
result.push_back("/a2/b/c/d/e/f");
result.push_back("/a1/b2");
result.push_back("/a1/b2/c");
result.push_back("/a1/b2/c/d");
result.push_back("/a1/b2/c/d/e");
result.push_back("/a1/b2/c/d/e/f");
result.push_back("/a1/b1/c2");
result.push_back("/a1/b1/c2/d");
result.push_back("/a1/b1/c2/d/e");
result.push_back("/a1/b1/c2/d/e/f");
result.push_back("/a1/b1/c1/d2");
result.push_back("/a1/b1/c1/d2/e");
result.push_back("/a1/b1/c1/d2/e/f");
result.push_back("/a1/b1/c1/d1/e2");
result.push_back("/a1/b1/c1/d1/e2/f");
result.push_back("/a1/b1/c1/d1/e1/f2");
return result;
}
}
class CryNodeTest_RenameNested : public TestWithParam<std::tuple<bf::path, bf::path>>, public CryTestBase {
public:
void CreateDirs() {
for (const auto& dir : SourceDirs()) {
if (dir != "/") {
CreateDir(dir);
}
}
}
void create_path_if_not_exists(const bf::path& path) {
if (!Exists(path)) {
if (path.has_parent_path()) {
create_path_if_not_exists(path.parent_path());
}
CreateDir(path);
}
}
void expect_rename_succeeds(const bf::path& source_path, const bf::path& dest_path) {
auto source = device().Load(source_path).value();
source->rename(dest_path);
// TODO Test that rename was successful
}
void expect_rename_fails(const bf::path& source_path, const bf::path& dest_path, int expected_errno) {
auto source = device().Load(source_path).value();
try {
source->rename(dest_path);
ASSERT(false, "Expected throw FuseErrnoException(" + std::to_string(expected_errno) + " but didn't throw");
} catch (const FuseErrnoException& e) {
ASSERT_EQ(expected_errno, e.getErrno());
}
// TODO Test rename wasn't successful
}
};
INSTANTIATE_TEST_SUITE_P(All, CryNodeTest_RenameNested, Combine(ValuesIn(SourceDirs()), ValuesIn(DestDirs())));
TEST_P(CryNodeTest_RenameNested, Rename) {
CreateDirs();
auto source_path = std::get<0>(GetParam());
auto dest_path = std::get<1>(GetParam());
if (dest_path.has_parent_path()) {
create_path_if_not_exists(dest_path.parent_path());
}
if (source_path == "/" || dest_path == "/") {
expect_rename_fails(source_path, dest_path, EBUSY);
} else if (source_path == dest_path) {
expect_rename_succeeds(source_path, dest_path);
} else if (boost::starts_with(source_path, dest_path)) {
expect_rename_fails(source_path, dest_path, ENOTEMPTY);
} else if (boost::starts_with(dest_path, source_path)) {
expect_rename_fails(source_path, dest_path, EINVAL);
} else {
expect_rename_succeeds(source_path, dest_path);
}
}

View File

@ -0,0 +1,3 @@
#include "CryTestBase.h"
constexpr fspp::mode_t CryTestBase::MODE_PUBLIC;

View File

@ -2,6 +2,9 @@
#define MESSMER_CRYFS_TEST_CRYFS_FILESYSTEM_CRYTESTBASE_H
#include <cryfs/impl/filesystem/CryDevice.h>
#include <cryfs/impl/filesystem/CryDir.h>
#include <cryfs/impl/filesystem/CryNode.h>
#include <cryfs/impl/filesystem/CryOpenFile.h>
#include <cryfs/impl/config/CryPresetPasswordBasedKeyProvider.h>
#include <blockstore/implementations/inmemory/InMemoryBlockStore2.h>
#include <cpp-utils/tempfile/TempFile.h>
@ -36,6 +39,38 @@ public:
return *_device;
}
static constexpr fspp::mode_t MODE_PUBLIC = fspp::mode_t()
.addUserReadFlag().addUserWriteFlag().addUserExecFlag()
.addGroupReadFlag().addGroupWriteFlag().addGroupExecFlag()
.addOtherReadFlag().addOtherWriteFlag().addOtherExecFlag();
cpputils::unique_ref<cryfs::CryNode> CreateFile(const boost::filesystem::path &path) {
auto parentDir = device().LoadDir(path.parent_path()).value();
parentDir->createAndOpenFile(path.filename().string(), MODE_PUBLIC, fspp::uid_t(0), fspp::gid_t(0));
auto file = device().Load(path).value();
return cpputils::dynamic_pointer_move<cryfs::CryNode>(file).value();
}
cpputils::unique_ref<cryfs::CryNode> CreateDir(const boost::filesystem::path &path) {
auto _parentDir = device().Load(path.parent_path()).value();
auto parentDir = cpputils::dynamic_pointer_move<cryfs::CryDir>(_parentDir).value();
parentDir->createDir(path.filename().string(), MODE_PUBLIC, fspp::uid_t(0), fspp::gid_t(0));
auto createdDir = device().Load(path).value();
return cpputils::dynamic_pointer_move<cryfs::CryNode>(createdDir).value();
}
cpputils::unique_ref<cryfs::CryNode> CreateSymlink(const boost::filesystem::path &path) {
auto _parentDir = device().Load(path.parent_path()).value();
auto parentDir = cpputils::dynamic_pointer_move<cryfs::CryDir>(_parentDir).value();
parentDir->createSymlink(path.filename().string(), "/target", fspp::uid_t(0), fspp::gid_t(0));
auto createdSymlink = device().Load(path).value();
return cpputils::dynamic_pointer_move<cryfs::CryNode>(createdSymlink).value();
}
bool Exists(const boost::filesystem::path &path) {
return device().Load(path) != boost::none;
}
private:
cpputils::TempDir _tempLocalStateDir;
cryfs::LocalStateDir _localStateDir;