Core: Rewrite VirtualDirectory class and add more tests

This commit is contained in:
Jérôme Leclercq
2022-03-11 13:21:39 +01:00
parent db0c1e6e8c
commit db98b86eaf
4 changed files with 431 additions and 225 deletions

View File

@@ -8,89 +8,161 @@
namespace Nz
{
inline VirtualDirectory::VirtualDirectory(VirtualDirectoryEntry parentDirectory) :
m_parent(std::move(parentDirectory)),
m_wereDotRegistered(false)
inline VirtualDirectory::VirtualDirectory(std::weak_ptr<VirtualDirectory> parentDirectory) :
m_parent(std::move(parentDirectory))
{
}
inline VirtualDirectory::VirtualDirectory(std::filesystem::path physicalPath, VirtualDirectoryEntry parentDirectory) :
inline VirtualDirectory::VirtualDirectory(std::filesystem::path physicalPath, std::weak_ptr<VirtualDirectory> parentDirectory) :
m_physicalPath(std::move(physicalPath)),
m_parent(std::move(parentDirectory)),
m_wereDotRegistered(false)
m_parent(std::move(parentDirectory))
{
}
template<typename F>
void VirtualDirectory::Foreach(F&& cb, bool includeDots)
void VirtualDirectory::Foreach(F&& callback, bool includeDots)
{
if (includeDots)
EnsureDots();
for (auto&& pair : m_content)
{
if (!includeDots && (pair.first == "." || pair.first == ".."))
continue;
cb(pair.first, pair.second);
Entry ourselves = DirectoryEntry{ shared_from_this() };
callback(std::string_view("."), ourselves);
if (VirtualDirectoryPtr parent = m_parent.lock())
{
Entry parentEntry = DirectoryEntry{ parent };
if (!CallbackReturn(callback, std::string_view(".."), parentEntry))
return;
}
else if (!CallbackReturn(callback, std::string_view(".."), ourselves))
return;
}
for (auto&& entry : m_content)
{
if (!CallbackReturn(callback, std::string_view(entry.name), std::as_const(entry.entry)))
return;
}
if (m_physicalPath)
{
for (auto&& physicalEntry : std::filesystem::directory_iterator(*m_physicalPath))
{
Entry entry;
std::string filename = physicalEntry.path().filename().generic_u8string();
if (m_content.find(filename) != m_content.end())
continue; //< Physical file/directory has been overridden by a virtual one
if (physicalEntry.is_regular_file())
entry.emplace<PhysicalFileEntry>(physicalEntry.path());
else if (physicalEntry.is_directory())
// Check if physical file/directory has been overridden by a virtual one
auto it = std::lower_bound(m_content.begin(), m_content.end(), filename, [](const ContentEntry& entry, std::string_view name)
{
// FIXME: Allocating a shared_ptr on iteration is bad, not sure about a workaround
entry.emplace<VirtualDirectoryEntry>(std::make_shared<VirtualDirectory>(physicalEntry.path(), shared_from_this()));
}
return entry.name < name;
});
if (it != m_content.end() && it->name == filename)
continue;
std::filesystem::file_status status = physicalEntry.status();
Entry entry;
if (std::filesystem::is_regular_file(status))
entry = PhysicalFileEntry{ physicalEntry.path() };
else if (std::filesystem::is_directory(status))
entry = PhysicalDirectoryEntry{ physicalEntry.path() };
else
continue;
cb(physicalEntry.path().filename().generic_u8string(), entry);
if (!CallbackReturn(callback, std::string_view(filename), entry))
return;
}
}
}
inline bool VirtualDirectory::GetEntry(std::string_view path, Entry* entry)
template<typename F> bool VirtualDirectory::GetEntry(std::string_view path, F&& callback)
{
std::shared_ptr<VirtualDirectory> dir;
std::string_view entryName;
if (!RetrieveDirectory(path, false, dir, entryName))
return false;
VirtualDirectoryPtr currentDir = shared_from_this();
std::vector<std::string> physicalDirectoryParts;
return SplitPath(path, [&](std::string_view dirName)
{
if (!physicalDirectoryParts.empty())
{
// Special case when traversing directory
if (dirName == "..")
{
// Don't allow to escape virtual directory
if (!physicalDirectoryParts.empty())
physicalDirectoryParts.pop_back();
}
else if (dirName != ".")
physicalDirectoryParts.emplace_back(dirName);
if (!dir->GetEntryInternal(entryName, entry))
return false;
return true;
}
return true;
return currentDir->GetEntryInternal(dirName, [&](const Entry& entry)
{
if (auto dirEntry = std::get_if<DirectoryEntry>(&entry))
{
currentDir = dirEntry->directory;
return true;
}
else if (auto physDirEntry = std::get_if<PhysicalDirectoryEntry>(&entry))
{
// We're traversing a physical directory
physicalDirectoryParts.emplace_back(dirName);
return true;
}
return false;
});
},
[&](std::string_view name)
{
if (!physicalDirectoryParts.empty())
{
if (physicalDirectoryParts.empty() && (name == "." || name == ".."))
{
// Prevent escaping the virtual directory
Entry ourselves = DirectoryEntry{ shared_from_this() };
return CallbackReturn(callback, ourselves);
}
assert(m_physicalPath);
std::filesystem::path filePath = *m_physicalPath;
for (const auto& part : physicalDirectoryParts)
filePath /= part;
filePath /= name;
std::filesystem::file_status status = std::filesystem::status(filePath); //< FIXME: This will follow symlink, is this the intended behavior? (see symlink_status)
Entry entry;
if (std::filesystem::is_regular_file(status))
entry = PhysicalFileEntry{ std::move(filePath) };
else if (std::filesystem::is_directory(status))
entry = PhysicalDirectoryEntry{ std::move(filePath) };
else
return false; //< either not known or of a special type
return CallbackReturn(callback, entry);
}
else
return currentDir->GetEntryInternal(name, callback);
});
}
inline auto VirtualDirectory::StoreDirectory(std::string_view path, VirtualDirectoryEntry directory) -> VirtualDirectoryEntry&
inline auto VirtualDirectory::StoreDirectory(std::string_view path, VirtualDirectoryPtr directory) -> DirectoryEntry&
{
std::shared_ptr<VirtualDirectory> dir;
std::string_view entryName;
if (!RetrieveDirectory(path, true, dir, entryName))
throw std::runtime_error("invalid path");
return dir->StoreDirectoryInternal(std::string(entryName), std::move(directory));
return dir->StoreInternal(std::string(entryName), DirectoryEntry{ std::move(directory) });
}
inline auto VirtualDirectory::StoreDirectory(std::string_view path, std::filesystem::path directoryPath) -> VirtualDirectoryEntry&
inline auto VirtualDirectory::StoreDirectory(std::string_view path, std::filesystem::path directoryPath) -> PhysicalDirectoryEntry&
{
std::shared_ptr<VirtualDirectory> dir;
std::string_view entryName;
if (!RetrieveDirectory(path, true, dir, entryName))
throw std::runtime_error("invalid path");
return dir->StoreDirectoryInternal(std::string(entryName), std::move(directoryPath));
return dir->StoreInternal(std::string(entryName), PhysicalDirectoryEntry{ std::move(directoryPath) });
}
inline auto VirtualDirectory::StoreFile(std::string_view path, std::vector<UInt8> file) -> FileContentEntry&
@@ -100,7 +172,7 @@ namespace Nz
if (!RetrieveDirectory(path, true, dir, entryName))
throw std::runtime_error("invalid path");
return dir->StoreFileInternal(std::string(entryName), std::move(file));
return dir->StoreInternal(std::string(entryName), FileContentEntry{ std::move(file) });
}
inline auto VirtualDirectory::StoreFile(std::string_view path, std::filesystem::path filePath) -> PhysicalFileEntry&
@@ -110,7 +182,7 @@ namespace Nz
if (!RetrieveDirectory(path, true, dir, entryName))
throw std::runtime_error("invalid path");
return dir->StoreFileInternal(std::string(entryName), std::move(filePath));
return dir->StoreInternal(std::string(entryName), PhysicalFileEntry{ std::move(filePath) });
}
inline auto VirtualDirectory::StoreFile(std::string_view path, const void* data, std::size_t size) -> DataPointerEntry&
@@ -120,20 +192,55 @@ namespace Nz
if (!RetrieveDirectory(path, true, dir, entryName))
throw std::runtime_error("invalid path");
return dir->StoreFileInternal(std::string(entryName), data, size);
return dir->StoreInternal(std::string(entryName), DataPointerEntry{ data, size });
}
inline void VirtualDirectory::EnsureDots()
template<typename F> bool VirtualDirectory::GetEntryInternal(std::string_view name, F&& callback)
{
if (!m_wereDotRegistered)
if (name == ".")
{
StoreDirectoryInternal(".", shared_from_this());
if (m_parent)
StoreDirectoryInternal("..", m_parent);
Entry entry{ DirectoryEntry{ shared_from_this() } };
return CallbackReturn(callback, entry);
}
else if (name == "..")
{
Entry entry;
if (VirtualDirectoryPtr parent = m_parent.lock())
entry = DirectoryEntry{ std::move(parent) };
else
StoreDirectoryInternal("..", shared_from_this());
entry = DirectoryEntry{ shared_from_this() };
m_wereDotRegistered = true;
return CallbackReturn(callback, entry);
}
else
{
auto it = std::lower_bound(m_content.begin(), m_content.end(), name, [](const ContentEntry& entry, std::string_view name)
{
return entry.name < name;
});
if (it == m_content.end() || it->name != name)
{
// Virtual file not found, check if it has a physical one
if (m_physicalPath)
{
std::filesystem::path filePath = *m_physicalPath / name;
std::filesystem::file_status status = std::filesystem::status(filePath); //< FIXME: This will follow symlink, is this the intended behavior? (see symlink_status)
Entry entry;
if (std::filesystem::is_regular_file(status))
entry = PhysicalFileEntry{ std::move(filePath) };
else if (std::filesystem::is_directory(status))
entry = PhysicalDirectoryEntry{ std::move(filePath) };
else
return false; //< either not known or of a special type
return CallbackReturn(callback, entry);
}
return false;
}
return CallbackReturn(callback, it->entry);
}
}
@@ -143,25 +250,25 @@ namespace Nz
return SplitPath(path, [&](std::string_view dirName)
{
Entry entry;
if (directory->GetEntryInternal(dirName, &entry))
bool dirFound = directory->GetEntryInternal(dirName, [&](const Entry& entry)
{
if (auto dir = std::get_if<VirtualDirectoryEntry>(&entry))
directory = *dir;
if (auto dirEntry = std::get_if<DirectoryEntry>(&entry))
directory = dirEntry->directory;
else
return false;
}
else
{
if (allowCreation)
{
auto newDirectory = std::make_shared<VirtualDirectory>(directory);
directory = directory->StoreDirectoryInternal(std::string(dirName), newDirectory);
}
else
return false;
}
allowCreation = false; //< does exist but is not a directory
});
if (dirFound)
return true;
// Try to create a new directory
if (!allowCreation)
return false;
auto newDirectory = std::make_shared<VirtualDirectory>(directory);
directory->StoreDirectory(dirName, newDirectory);
directory = std::move(newDirectory);
return true;
},
[&](std::string_view name)
@@ -170,84 +277,44 @@ namespace Nz
});
}
inline bool VirtualDirectory::GetEntryInternal(std::string_view name, Entry* entry)
template<typename T>
T& VirtualDirectory::StoreInternal(std::string name, T value)
{
EnsureDots();
auto it = m_content.find(name);
if (it != m_content.end())
auto it = std::lower_bound(m_content.begin(), m_content.end(), name, [](const ContentEntry& entry, std::string_view name)
{
*entry = it->second;
return entry.name < name;
});
ContentEntry* entryPtr;
if (it == m_content.end() || it->name == name)
entryPtr = &*m_content.emplace(it);
else
entryPtr = &*it;
entryPtr->entry = std::move(value);
entryPtr->name = std::move(name);
return std::get<T>(entryPtr->entry);
}
template<typename F, typename... Args>
bool VirtualDirectory::CallbackReturn(F&& callback, Args&& ...args)
{
using Ret = decltype(callback(std::forward<Args>(args)...));
if constexpr (std::is_void_v<Ret>)
{
callback(std::forward<Args>(args)...);
return true;
}
else
{
if (m_physicalPath)
{
std::filesystem::path entryPath = *m_physicalPath / name;
if (std::filesystem::is_regular_file(entryPath))
entry->emplace<PhysicalFileEntry>(entryPath);
else if (std::filesystem::is_directory(entryPath))
{
// FIXME: Allocating a shared_ptr on iteration is bad, not sure about a workaround
*entry = StoreDirectoryInternal(std::string(name), entryPath);
}
else
return false;
return true;
}
return false;
static_assert(std::is_same_v<Ret, bool>, "callback must either return a boolean or nothing");
return callback(std::forward<Args>(args)...);
}
}
inline auto VirtualDirectory::StoreDirectoryInternal(std::string name, std::filesystem::path directoryPath) -> VirtualDirectoryEntry&
{
assert(name.find_first_of("\\/:") == name.npos);
auto it = m_content.insert_or_assign(std::move(name), std::make_shared<VirtualDirectory>(directoryPath, shared_from_this())).first;
return std::get<VirtualDirectoryEntry>(it->second);
}
inline auto VirtualDirectory::StoreDirectoryInternal(std::string name, VirtualDirectoryEntry directory) -> VirtualDirectoryEntry&
{
assert(name.find_first_of("\\/:") == name.npos);
auto it = m_content.insert_or_assign(std::move(name), std::move(directory)).first;
return std::get<VirtualDirectoryEntry>(it->second);
}
inline auto VirtualDirectory::StoreFileInternal(std::string name, std::vector<UInt8> fileContent) -> FileContentEntry&
{
assert(name.find_first_of("\\/:") == name.npos);
FileContentEntry fileEntry;
fileEntry.data = std::make_shared<std::vector<UInt8>>(std::move(fileContent));
auto it = m_content.insert_or_assign(std::move(name), std::move(fileEntry)).first;
return std::get<FileContentEntry>(it->second);
}
inline auto VirtualDirectory::StoreFileInternal(std::string name, std::filesystem::path filePath) -> PhysicalFileEntry&
{
assert(name.find_first_of("\\/:") == name.npos);
auto it = m_content.insert_or_assign(std::move(name), std::move(filePath)).first;
return std::get<PhysicalFileEntry>(it->second);
}
inline auto VirtualDirectory::StoreFileInternal(std::string name, const void* data, std::size_t size) -> DataPointerEntry&
{
assert(name.find_first_of("\\/:") == name.npos);
auto it = m_content.insert_or_assign(std::move(name), DataPointerEntry{ data, size }).first;
return std::get<DataPointerEntry>(it->second);
}
template<typename F1, typename F2>
inline bool VirtualDirectory::SplitPath(std::string_view path, F1&& dirCB, F2&& lastCB)
bool VirtualDirectory::SplitPath(std::string_view path, F1&& dirCB, F2&& lastCB)
{
std::size_t pos;
while ((pos = path.find_first_of("\\/:")) != std::string::npos)
@@ -258,8 +325,7 @@ namespace Nz
path = path.substr(pos + 1);
}
lastCB(path);
return true;
return CallbackReturn(lastCB, path);
}
}