// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Core module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include namespace Nz { inline VirtualDirectory::VirtualDirectory(std::weak_ptr parentDirectory) : m_parent(std::move(parentDirectory)), m_isUprootAllowed(false) { } inline VirtualDirectory::VirtualDirectory(std::filesystem::path physicalPath, std::weak_ptr parentDirectory) : m_physicalPath(std::move(physicalPath)), m_parent(std::move(parentDirectory)), m_isUprootAllowed(false) { } inline void VirtualDirectory::AllowUproot(bool uproot) { m_isUprootAllowed = uproot; } inline bool VirtualDirectory::Exists(std::string_view path) { return GetEntry(path, [](const auto&) {}); } template void VirtualDirectory::Foreach(F&& callback, bool includeDots) { if (includeDots) { Entry ourselves = VirtualDirectoryEntry{ shared_from_this() }; callback(std::string_view("."), ourselves); if (VirtualDirectoryPtr parent = m_parent.lock()) { Entry parentEntry = VirtualDirectoryEntry{ { 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)) { std::string filename = physicalEntry.path().filename().generic_u8string(); // 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) { 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)) { VirtualDirectoryPtr virtualDir = std::make_shared(physicalEntry.path(), weak_from_this()); entry = PhysicalDirectoryEntry{ { std::move(virtualDir) }, physicalEntry.path() }; } else continue; if (!CallbackReturn(callback, std::string_view(filename), entry)) return; } } } template bool VirtualDirectory::GetDirectoryEntry(std::string_view path, F&& callback) { return GetEntry(path, [&](const Entry& entry) { return std::visit([&](auto&& entry) { using T = std::decay_t; if constexpr (std::is_same_v || std::is_same_v) { return CallbackReturn(callback, static_cast(entry)); } else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { NazaraError("entry is a file"); return false; } else static_assert(AlwaysFalse(), "incomplete visitor"); }, entry); }); } template bool VirtualDirectory::GetEntry(std::string_view path, F&& callback) { assert(!path.empty()); VirtualDirectoryPtr currentDir = shared_from_this(); std::optional physicalPathBase; std::vector physicalDirectoryParts; return SplitPath(path, [&](std::string_view dirName) { assert(!dirName.empty()); if (physicalPathBase) { // Special case when traversing directory if (dirName == ".." && !m_isUprootAllowed) { // Don't allow to escape virtual directory if (!physicalDirectoryParts.empty()) physicalDirectoryParts.pop_back(); else physicalPathBase.reset(); } else if (dirName != ".") physicalDirectoryParts.emplace_back(dirName); return true; } return currentDir->GetEntryInternal(dirName, [&](const Entry& entry) { if (auto dirEntry = std::get_if(&entry)) { currentDir = dirEntry->directory; return true; } else if (auto physDirEntry = std::get_if(&entry)) { assert(!physicalPathBase); // We're traversing a physical directory physicalPathBase = physDirEntry->filePath; return true; } return false; }); }, [&](std::string_view name) { if (physicalPathBase) { std::filesystem::path filePath = *physicalPathBase; 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)) { VirtualDirectoryPtr virtualDir = std::make_shared(filePath, weak_from_this()); entry = PhysicalDirectoryEntry{ { std::move(virtualDir) }, std::move(filePath) }; } else return false; //< either not known or of a special type return CallbackReturn(callback, entry); } else return currentDir->GetEntryInternal(name, callback); }); } template bool VirtualDirectory::GetFileContent(std::string_view path, F&& callback) { return GetEntry(path, [&](const Entry& entry) { return std::visit([&](auto&& entry) { using T = std::decay_t; using P1 = const void*; using P2 = std::size_t; if constexpr (std::is_same_v) { return CallbackReturn(callback, static_cast(entry.data), SafeCast(entry.size)); } else if constexpr (std::is_same_v) { return CallbackReturn(callback, static_cast(entry.data.data()), SafeCast(entry.data.size())); } else if constexpr (std::is_same_v) { std::optional> source = File::ReadWhole(entry.filePath); if (!source.has_value()) return false; return CallbackReturn(callback, static_cast(source->data()), SafeCast(source->size())); } else if constexpr (std::is_same_v || std::is_same_v) { NazaraError("entry is a directory"); return false; } else static_assert(AlwaysFalse(), "incomplete visitor"); }, entry); }); } inline bool VirtualDirectory::IsUprootAllowed() const { return m_isUprootAllowed; } inline auto VirtualDirectory::StoreDirectory(std::string_view path, VirtualDirectoryPtr directory) -> VirtualDirectoryEntry& { assert(!path.empty()); std::shared_ptr dir; std::string_view entryName; if (!CreateOrRetrieveDirectory(path, dir, entryName)) throw std::runtime_error("invalid path"); if (entryName == "." || entryName == "..") throw std::runtime_error("invalid entry name"); return dir->StoreInternal(std::string(entryName), VirtualDirectoryEntry{ { std::move(directory) } }); } inline auto VirtualDirectory::StoreDirectory(std::string_view path, std::filesystem::path directoryPath) -> PhysicalDirectoryEntry& { assert(!path.empty()); std::shared_ptr dir; std::string_view entryName; if (!CreateOrRetrieveDirectory(path, dir, entryName)) throw std::runtime_error("invalid path"); if (entryName == "." || entryName == "..") throw std::runtime_error("invalid entry name"); PhysicalDirectoryEntry entry; entry.directory = std::make_shared(directoryPath, dir); entry.filePath = std::move(directoryPath); return dir->StoreInternal(std::string(entryName), std::move(entry)); } inline auto VirtualDirectory::StoreFile(std::string_view path, std::vector file) -> FileContentEntry& { assert(!path.empty()); std::shared_ptr dir; std::string_view entryName; if (!CreateOrRetrieveDirectory(path, dir, entryName)) throw std::runtime_error("invalid path"); if (entryName == "." || entryName == "..") throw std::runtime_error("invalid entry name"); return dir->StoreInternal(std::string(entryName), FileContentEntry{ std::move(file) }); } inline auto VirtualDirectory::StoreFile(std::string_view path, std::filesystem::path filePath) -> PhysicalFileEntry& { assert(!path.empty()); std::shared_ptr dir; std::string_view entryName; if (!CreateOrRetrieveDirectory(path, dir, entryName)) throw std::runtime_error("invalid path"); if (entryName == "." || entryName == "..") throw std::runtime_error("invalid entry name"); 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& { assert(!path.empty()); std::shared_ptr dir; std::string_view entryName; if (!CreateOrRetrieveDirectory(path, dir, entryName)) throw std::runtime_error("invalid path"); if (entryName == "." || entryName == "..") throw std::runtime_error("invalid entry name"); return dir->StoreInternal(std::string(entryName), DataPointerEntry{ data, size }); } template bool VirtualDirectory::GetEntryInternal(std::string_view name, F&& callback) { if (name == ".") { Entry entry{ VirtualDirectoryEntry{ { shared_from_this() } } }; return CallbackReturn(callback, entry); } if (name == "..") { VirtualDirectoryPtr parentEntry; if (VirtualDirectoryPtr parent = m_parent.lock()) parentEntry = std::move(parent); else if (!m_isUprootAllowed) parentEntry = shared_from_this(); if (parentEntry) { Entry entry = VirtualDirectoryEntry{ { std::move(parentEntry) } }; return CallbackReturn(callback, entry); } } 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)) { VirtualDirectoryPtr virtualDir = std::make_shared(filePath, weak_from_this()); entry = PhysicalDirectoryEntry{ { std::move(virtualDir) }, 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); } inline bool VirtualDirectory::CreateOrRetrieveDirectory(std::string_view path, std::shared_ptr& directory, std::string_view& entryName) { directory = shared_from_this(); bool allowCreation = true; return SplitPath(path, [&](std::string_view dirName) { assert(!dirName.empty()); bool dirFound = directory->GetEntryInternal(dirName, [&](const Entry& entry) { if (auto dirEntry = std::get_if(&entry)) directory = dirEntry->directory; else 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(directory); directory->StoreDirectory(dirName, newDirectory); directory = std::move(newDirectory); return true; }, [&](std::string_view name) { if (name.empty()) return false; entryName = name; return true; }); } template T& VirtualDirectory::StoreInternal(std::string name, T value) { assert(!name.empty()); auto it = std::lower_bound(m_content.begin(), m_content.end(), name, [](const ContentEntry& entry, std::string_view name) { 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(entryPtr->entry); } template bool VirtualDirectory::CallbackReturn(F&& callback, Args&& ...args) { using Ret = decltype(callback(std::forward(args)...)); if constexpr (std::is_void_v) { callback(std::forward(args)...); return true; } else { static_assert(std::is_same_v, "callback must either return a boolean or nothing"); return callback(std::forward(args)...); } } template bool VirtualDirectory::SplitPath(std::string_view path, F1&& dirCB, F2&& lastCB) { std::string_view nextPart; auto HandlePart = [&](std::string_view part) { if (part.empty()) return true; //< "a//b" == "a/b" if (!nextPart.empty() && !CallbackReturn(dirCB, nextPart)) return false; nextPart = part; return true; }; return SplitStringAny(path, R"(\/:)", HandlePart) && CallbackReturn(lastCB, nextPart); } } #include