├── examples ├── test-data │ ├── dlc1 │ │ ├── file1.txt │ │ └── file.txt │ ├── dlc2 │ │ ├── file2.txt │ │ └── file.txt │ ├── files │ │ └── test.txt │ └── test.zip ├── CMakeLists.txt └── example.cpp ├── .gitmodules ├── changelog.txt ├── include └── vfspp │ ├── VFS.h │ ├── Global.h │ ├── ThreadingPolicy.hpp │ ├── IFileSystem.h │ ├── Alias.hpp │ ├── FileInfo.hpp │ ├── IFile.h │ ├── ZipFile.hpp │ ├── NativeFile.hpp │ ├── ZipFileSystem.hpp │ ├── VirtualFileSystem.hpp │ ├── MemoryFileSystem.hpp │ ├── MemoryFile.hpp │ ├── FilesystemCompat.hpp │ └── NativeFileSystem.hpp ├── .gitignore ├── LICENSE ├── CMakeLists.txt └── README.md /examples/test-data/dlc1/file1.txt: -------------------------------------------------------------------------------- 1 | (\_/) 2 | (o.o) 3 | (")(") 4 | -------------------------------------------------------------------------------- /examples/test-data/dlc2/file2.txt: -------------------------------------------------------------------------------- 1 | (\(\ 2 | (='.') 3 | o(_(")(") 4 | -------------------------------------------------------------------------------- /examples/test-data/dlc1/file.txt: -------------------------------------------------------------------------------- 1 | /\_/\ 2 | ( ^.^ ) 3 | (")(") 4 | -------------------------------------------------------------------------------- /examples/test-data/dlc2/file.txt: -------------------------------------------------------------------------------- 1 | /\_/\ 2 | ( o.o ) 3 | > ^ < 4 | -------------------------------------------------------------------------------- /examples/test-data/files/test.txt: -------------------------------------------------------------------------------- 1 | The quick brown fox jumps over the lazy dog 2 | -------------------------------------------------------------------------------- /examples/test-data/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextgeniuspro/vfspp/HEAD/examples/test-data/test.zip -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/miniz-cpp"] 2 | path = vendor/miniz-cpp 3 | url = https://github.com/nextgeniuspro/miniz-cpp.git 4 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | - Added Copy-on-write logic for Memory files 2 | - Each time file is opened a new handle (IFile object) is created 3 | - Removed legacy C-style code 4 | - -------------------------------------------------------------------------------- /include/vfspp/VFS.h: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_H 2 | #define VFSPP_H 3 | 4 | #include "VirtualFileSystem.hpp" 5 | #include "NativeFileSystem.hpp" 6 | #include "MemoryFileSystem.hpp" 7 | #include "ZipFileSystem.hpp" 8 | 9 | #endif // VFSPP_H 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | *.smod 19 | 20 | # Compiled Static libraries 21 | *.lai 22 | *.la 23 | *.a 24 | *.lib 25 | 26 | # Executables 27 | *.exe 28 | *.out 29 | *.app 30 | /build 31 | /examples/build 32 | .vscode/settings.json 33 | 34 | /out/** 35 | -------------------------------------------------------------------------------- /include/vfspp/Global.h: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_GLOBAL_H 2 | #define VFSPP_GLOBAL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #ifndef VFSPP_DISABLE_STD_FILESYSTEM 22 | #include 23 | #endif 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | 34 | #endif // VFSPP_GLOBAL_H -------------------------------------------------------------------------------- /include/vfspp/ThreadingPolicy.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_THREADINGPOLICY_HPP 2 | #define VFSPP_THREADINGPOLICY_HPP 3 | 4 | #include 5 | 6 | #include "Global.h" 7 | 8 | namespace vfspp 9 | { 10 | 11 | /* 12 | * Use this policy for multi-threaded applications 13 | */ 14 | struct MultiThreadedPolicy { 15 | static std::lock_guard Lock(std::mutex& m) noexcept { return std::lock_guard(m); } 16 | }; 17 | 18 | 19 | /* 20 | * Use this policy for single-threaded applications 21 | */ 22 | struct SingleThreadedPolicy { 23 | struct DummyLock {}; 24 | static DummyLock Lock(std::mutex&) noexcept { return {}; } 25 | }; 26 | 27 | // Select default `ThreadingPolicy` based on compile-time macro. 28 | #if defined(VFSPP_MT_SUPPORT_ENABLED) 29 | using ThreadingPolicy = MultiThreadedPolicy; 30 | #else 31 | using ThreadingPolicy = SingleThreadedPolicy; 32 | #endif 33 | 34 | } // namespace vfspp 35 | 36 | #endif // VFSPP_THREADINGPOLICY_HPP -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | # Project name and version 4 | project(vfspp VERSION 1.0 LANGUAGES CXX) 5 | option(BUILD_EXAMPLES "Build examples" OFF) 6 | # Add the miniz-cpp library 7 | add_subdirectory(vendor/miniz-cpp EXCLUDE_FROM_ALL) 8 | 9 | # Set C++ standard 10 | set(CMAKE_CXX_STANDARD 20) 11 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 12 | set(CMAKE_CXX_EXTENSIONS OFF) 13 | 14 | # Add the include directory as a target 15 | add_library(vfspp INTERFACE) 16 | add_library(vfspp::vfspp ALIAS vfspp) 17 | 18 | # Suppress warnings originating from bundled miniz-cpp headers 19 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") 20 | target_compile_options(vfspp INTERFACE -Wno-misleading-indentation -Wno-deprecated-declarations) 21 | endif() 22 | 23 | target_include_directories(vfspp 24 | INTERFACE 25 | $ 26 | $ 27 | $ 28 | ) 29 | 30 | target_compile_features(vfspp INTERFACE cxx_std_20) 31 | 32 | if (BUILD_EXAMPLES) 33 | add_subdirectory(examples) 34 | endif() 35 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(vfsppexample) 4 | 5 | add_executable(vfsppexample example.cpp) 6 | 7 | add_definitions(-DVFSPP_MT_SUPPORT_ENABLED) # Enable multi-threading support 8 | # add_definitions(-DVFSPP_DISABLE_STD_FILESYSTEM) # Disable std::filesystem support 9 | 10 | target_link_libraries(vfsppexample PRIVATE vfspp::vfspp) 11 | target_compile_features(vfsppexample PRIVATE cxx_std_20) 12 | 13 | # Copy test-data folder next to the produced binary 14 | set(VFSPP_EXAMPLE_TESTDATA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test-data) 15 | add_custom_command(TARGET vfsppexample POST_BUILD 16 | COMMAND ${CMAKE_COMMAND} -E echo "Copying test-data to $/test-data" 17 | COMMAND ${CMAKE_COMMAND} -E rm -rf "$/test-data" 18 | COMMAND ${CMAKE_COMMAND} -E copy_directory "${VFSPP_EXAMPLE_TESTDATA_DIR}" "$/test-data" 19 | VERBATIM 20 | ) 21 | 22 | # Ensure Visual Studio debugger runs from the executable directory so relative 'test-data/...' 23 | if (MSVC) 24 | set_property(TARGET vfsppexample PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "$") 25 | endif() -------------------------------------------------------------------------------- /include/vfspp/IFileSystem.h: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_IFILESYSTEM_H 2 | #define VFSPP_IFILESYSTEM_H 3 | 4 | #include "IFile.h" 5 | 6 | namespace vfspp 7 | { 8 | 9 | using IFileSystemPtr = std::shared_ptr; 10 | using IFileSystemWeakPtr = std::weak_ptr; 11 | 12 | class IFileSystem 13 | { 14 | public: 15 | using FilesList = std::vector; 16 | 17 | public: 18 | IFileSystem() = default; 19 | ~IFileSystem() = default; 20 | 21 | /* 22 | * Initialize filesystem, call this method as soon as possible 23 | */ 24 | [[nodiscard]] 25 | virtual bool Initialize() = 0; 26 | /* 27 | * Shutdown filesystem 28 | */ 29 | virtual void Shutdown() = 0; 30 | 31 | /* 32 | * Check if filesystem is initialized 33 | */ 34 | [[nodiscard]] 35 | virtual bool IsInitialized() const = 0; 36 | 37 | /* 38 | * Get base path 39 | */ 40 | [[nodiscard]] 41 | virtual const std::string& BasePath() const = 0; 42 | 43 | /* 44 | * Get mounted path 45 | */ 46 | [[nodiscard]] 47 | virtual const std::string& VirtualPath() const = 0; 48 | 49 | /* 50 | * Retrieve all files in filesystem. Heavy operation, avoid calling this often 51 | */ 52 | [[nodiscard]] 53 | virtual FilesList GetFilesList() const = 0; 54 | 55 | /* 56 | * Check is readonly filesystem 57 | */ 58 | [[nodiscard]] 59 | virtual bool IsReadOnly() const = 0; 60 | 61 | /* 62 | * Open existing file for reading, if not exists return null 63 | */ 64 | virtual IFilePtr OpenFile(const std::string& virtualPath, IFile::FileMode mode) = 0; 65 | 66 | /* 67 | * Close file 68 | */ 69 | virtual void CloseFile(IFilePtr file) = 0; 70 | 71 | /* 72 | * Create file on writeable filesystem. Return true if file already exists 73 | */ 74 | virtual IFilePtr CreateFile(const std::string& virtualPath) = 0; 75 | 76 | /* 77 | * Remove existing file on writable filesystem 78 | */ 79 | virtual bool RemoveFile(const std::string& virtualPath) = 0; 80 | 81 | /* 82 | * Copy existing file on writable filesystem 83 | */ 84 | virtual bool CopyFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) = 0; 85 | 86 | /* 87 | * Rename existing file on writable filesystem (Move file) 88 | */ 89 | virtual bool RenameFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath) = 0; 90 | 91 | /* 92 | * Check if file exists on filesystem 93 | */ 94 | [[nodiscard]] 95 | virtual bool IsFileExists(const std::string& virtualPath) const = 0; 96 | }; 97 | 98 | }; // namespace vfspp 99 | 100 | #endif // VFSPP_IFILESYSTEM_H 101 | -------------------------------------------------------------------------------- /include/vfspp/Alias.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_ALIAS_HPP 2 | #define VFSPP_ALIAS_HPP 3 | 4 | #include "Global.h" 5 | #include 6 | 7 | namespace vfspp 8 | { 9 | 10 | class Alias 11 | { 12 | public: 13 | Alias(); 14 | explicit Alias(std::string_view alias); 15 | 16 | static Alias Root(); 17 | 18 | const std::string& String() const noexcept; 19 | std::string_view View() const noexcept; 20 | size_t Length() const noexcept; 21 | 22 | bool operator==(const Alias& other) const noexcept; 23 | bool operator!=(const Alias& other) const noexcept; 24 | 25 | struct Hash 26 | { 27 | size_t operator()(const Alias& alias) const noexcept 28 | { 29 | return alias.HashValue(); 30 | } 31 | }; 32 | 33 | private: 34 | static std::string Normalize(std::string_view alias); 35 | size_t HashValue() const noexcept; 36 | 37 | private: 38 | std::string m_Value; 39 | }; 40 | 41 | inline Alias::Alias() 42 | : m_Value(Normalize("/")) 43 | { 44 | } 45 | 46 | inline Alias::Alias(std::string_view alias) 47 | : m_Value(Normalize(alias)) 48 | { 49 | } 50 | 51 | inline Alias Alias::Root() 52 | { 53 | return Alias("/"); 54 | } 55 | 56 | inline const std::string& Alias::String() const noexcept 57 | { 58 | return m_Value; 59 | } 60 | 61 | inline std::string_view Alias::View() const noexcept 62 | { 63 | return m_Value; 64 | } 65 | 66 | inline size_t Alias::Length() const noexcept 67 | { 68 | return m_Value.length(); 69 | } 70 | 71 | inline bool Alias::operator==(const Alias& other) const noexcept 72 | { 73 | return m_Value == other.m_Value; 74 | } 75 | 76 | inline bool Alias::operator!=(const Alias& other) const noexcept 77 | { 78 | return !(*this == other); 79 | } 80 | 81 | inline std::string Alias::Normalize(std::string_view alias) 82 | { 83 | std::string normalized(alias); 84 | 85 | const char* whitespace = " \t\n\r"; 86 | size_t begin = normalized.find_first_not_of(whitespace); 87 | if (begin == std::string::npos) { 88 | normalized.clear(); 89 | } else if (begin > 0) { 90 | normalized.erase(0, begin); 91 | } 92 | 93 | if (!normalized.empty()) { 94 | size_t end = normalized.find_last_not_of(whitespace); 95 | if (end != std::string::npos && end + 1 < normalized.size()) { 96 | normalized.erase(end + 1); 97 | } 98 | } 99 | 100 | if (normalized.empty()) { 101 | normalized = "/"; 102 | } 103 | 104 | if (normalized.front() != '/') { 105 | normalized.insert(normalized.begin(), '/'); 106 | } 107 | 108 | while (normalized.size() > 1 && normalized.back() == '/') { 109 | normalized.pop_back(); 110 | } 111 | 112 | if (normalized.back() != '/') { 113 | normalized.push_back('/'); 114 | } 115 | 116 | return normalized; 117 | } 118 | 119 | inline size_t Alias::HashValue() const noexcept 120 | { 121 | return std::hash{}(m_Value); 122 | } 123 | 124 | } // namespace vfspp 125 | 126 | #endif // VFSPP_ALIAS_HPP 127 | -------------------------------------------------------------------------------- /include/vfspp/FileInfo.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_FILEINFO_HPP 2 | #define VFSPP_FILEINFO_HPP 3 | 4 | #include "Global.h" 5 | 6 | #ifdef VFSPP_DISABLE_STD_FILESYSTEM 7 | #include "FilesystemCompat.hpp" 8 | namespace fs = vfspp::fs_compat; 9 | #else 10 | namespace fs = std::filesystem; 11 | #endif 12 | 13 | namespace vfspp 14 | { 15 | 16 | class FileInfo final 17 | { 18 | public: 19 | FileInfo(const std::string& aliasPath, const std::string& basePath, const std::string& fileName) 20 | { 21 | Configure(aliasPath, basePath, fileName); 22 | } 23 | 24 | FileInfo() = delete; 25 | ~FileInfo() = default; 26 | 27 | /* 28 | * Get file name with extension 29 | */ 30 | inline const std::string& Filename() const 31 | { 32 | return m_Filename; 33 | } 34 | 35 | /* 36 | * Get file name without extension 37 | */ 38 | inline const std::string& BaseFilename() const 39 | { 40 | return m_BaseFilename; 41 | } 42 | 43 | /* 44 | * Get file extension 45 | */ 46 | inline const std::string& Extension() const 47 | { 48 | return m_Extension; 49 | } 50 | 51 | /* 52 | * Get path to the file without alias or base path 53 | */ 54 | inline const std::string& FilePath() const 55 | { 56 | return m_Filepath; 57 | } 58 | 59 | /* 60 | * Get aliased file path, the path used to access file in virtual filesystem 61 | */ 62 | inline const std::string& VirtualPath() const 63 | { 64 | return m_VirtualPath; 65 | } 66 | 67 | /* 68 | * Get native file path, the path used to access file in native filesystem 69 | */ 70 | inline const std::string& NativePath() const 71 | { 72 | return m_NativePath; 73 | } 74 | 75 | private: 76 | void Configure(const std::string& aliasPath, const std::string& basePath, const std::string& fileName) 77 | { 78 | // Remove alias path from file name if any 79 | std::string strippedFileName = fileName; 80 | if (!basePath.empty() && fileName.find(basePath) == 0) { 81 | strippedFileName = fileName.substr(basePath.length()); 82 | } 83 | 84 | // Strip leading separators 85 | while (!strippedFileName.empty() && (strippedFileName.front() == '/' || strippedFileName.front() == '\\')) { 86 | strippedFileName.erase(strippedFileName.begin()); 87 | } 88 | 89 | const auto filePath = fs::path(strippedFileName); 90 | 91 | m_Filepath = filePath.generic_string(); 92 | m_VirtualPath = (fs::path(aliasPath) / filePath).generic_string(); 93 | m_NativePath = (fs::path(basePath) / filePath).generic_string(); 94 | 95 | m_Filename = filePath.filename().string(); 96 | m_Extension = filePath.has_extension() ? filePath.extension().string() : ""; 97 | m_BaseFilename = filePath.stem().string(); 98 | } 99 | 100 | private: 101 | std::string m_Filename; 102 | std::string m_BaseFilename; 103 | std::string m_Extension; 104 | 105 | std::string m_Filepath; 106 | std::string m_VirtualPath; 107 | std::string m_NativePath; 108 | }; 109 | 110 | inline bool operator ==(const FileInfo& fi1, const FileInfo& fi2) 111 | { 112 | return fi1.VirtualPath() == fi2.VirtualPath(); 113 | } 114 | 115 | inline bool operator <(const FileInfo& fi1, const FileInfo& fi2) 116 | { 117 | return fi1.VirtualPath() < fi2.VirtualPath(); 118 | } 119 | 120 | }; // namespace vfspp 121 | 122 | #endif // VFSPP_FILEINFO_HPP 123 | -------------------------------------------------------------------------------- /examples/example.cpp: -------------------------------------------------------------------------------- 1 | #include "vfspp/VFS.h" 2 | 3 | #include 4 | #include 5 | 6 | 7 | using namespace vfspp; 8 | using namespace std::string_view_literals; 9 | 10 | void PrintFileContent(const std::string& msg, IFilePtr file) 11 | { 12 | if (file && file->IsOpened()) { 13 | std::string data(256, 0); 14 | file->Read(std::span(reinterpret_cast(data.data()), data.size())); 15 | 16 | std::printf("%s\n%s\n", msg.c_str(), data.c_str()); 17 | } 18 | } 19 | 20 | int main() 21 | { 22 | auto vfs = std::make_shared(); 23 | 24 | // Native filesystem example 25 | printf("Native filesystem test:\n"); 26 | 27 | if (!vfs->CreateFileSystem("/", "test-data/files")) { 28 | std::fprintf(stderr, "Failed to mount native filesystem\n"); 29 | return 1; 30 | } 31 | 32 | if (IFilePtr file = vfs->OpenFile("/test.txt", IFile::FileMode::ReadWrite)) { 33 | std::string data = "The quick brown fox jumps over the lazy dog\n"; 34 | file->Write(std::span(reinterpret_cast(data.data()), data.size())); 35 | } 36 | 37 | if (IFilePtr file2 = vfs->OpenFile("/test.txt", IFile::FileMode::Read)) { 38 | PrintFileContent("File /test.txt:", file2); 39 | } 40 | 41 | // Memory filesystem example 42 | printf("Memory filesystem test:\n"); 43 | 44 | if (!vfs->CreateFileSystem("/memory")) { 45 | std::fprintf(stderr, "Failed to mount memory filesystem\n"); 46 | return 1; 47 | } 48 | 49 | if (IFilePtr memFile = vfs->OpenFile("/memory/file.txt", IFile::FileMode::ReadWrite)) { 50 | std::string data = "The quick brown fox jumps over the lazy dog\n"; 51 | memFile->Write(std::span(reinterpret_cast(data.data()), data.size())); 52 | } 53 | 54 | if (IFilePtr memFile2 = vfs->OpenFile("/memory/file.txt", IFile::FileMode::Read)) { 55 | PrintFileContent("File /memory/file.txt:", memFile2); 56 | } 57 | 58 | // Zip filesystem example 59 | printf("Zip filesystem test:\n"); 60 | 61 | auto zipFSResult = vfs->CreateFileSystem("/zip", "test-data/test.zip"); 62 | if (!zipFSResult) { 63 | std::fprintf(stderr, "Failed to mount zip filesystem\n"); 64 | return 1; 65 | } 66 | 67 | const auto& files = zipFSResult.value()->GetFilesList(); 68 | for (const auto& file : files) { 69 | printf("Zip file entry: %s\n", file.VirtualPath().c_str()); 70 | } 71 | 72 | if (IFilePtr zipFile = vfs->OpenFile("/zip/file.txt", IFile::FileMode::Read)) { 73 | PrintFileContent("File /zip/file.txt:", zipFile); 74 | } 75 | 76 | // DLC filesystem example 77 | printf("DLC filesystem test:\n"); 78 | 79 | if (!vfs->CreateFileSystem("/dlc", "test-data/dlc1")) { 80 | std::fprintf(stderr, "Failed to mount native filesystem for dlc1\n"); 81 | return 1; 82 | } 83 | 84 | if (IFilePtr dlcFile = vfs->OpenFile("/dlc/file.txt", IFile::FileMode::Read)) { 85 | PrintFileContent("File /dlc/file.txt that exists in dlc1:", dlcFile); 86 | } 87 | 88 | if (!vfs->CreateFileSystem("/dlc", "test-data/dlc2")) { 89 | std::fprintf(stderr, "Failed to mount native filesystem for dlc2\n"); 90 | return 1; 91 | } 92 | 93 | if (IFilePtr dlcFile = vfs->OpenFile("/dlc/file.txt", IFile::FileMode::Read)) { 94 | PrintFileContent("File /dlc/file.txt patched by dlc2:", dlcFile); 95 | } 96 | 97 | if (IFilePtr dlcFile = vfs->OpenFile("/dlc/file1.txt", IFile::FileMode::Read)) { 98 | PrintFileContent("File /dlc/file1.txt that exists only in dlc1:", dlcFile); 99 | } 100 | 101 | if (IFilePtr dlcFile = vfs->OpenFile("/dlc/file2.txt", IFile::FileMode::Read)) { 102 | PrintFileContent("File /dlc/file2.txt that exists only in dlc2:", dlcFile); 103 | } 104 | 105 | return 0; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /include/vfspp/IFile.h: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_IFILE_H 2 | #define VFSPP_IFILE_H 3 | 4 | #include "Global.h" 5 | #include "FileInfo.hpp" 6 | 7 | #include 8 | 9 | namespace vfspp 10 | { 11 | 12 | using IFilePtr = std::shared_ptr; 13 | using IFileWeakPtr = std::weak_ptr; 14 | 15 | class IFile 16 | { 17 | public: 18 | /* 19 | * Seek modes 20 | */ 21 | enum class Origin 22 | { 23 | Begin, 24 | End, 25 | Set 26 | }; 27 | 28 | /* 29 | * Open file mode 30 | */ 31 | enum class FileMode : uint8_t 32 | { 33 | Read = (1 << 0), 34 | Write = (1 << 1), 35 | ReadWrite = Read | Write, 36 | Append = (1 << 2), 37 | Truncate = (1 << 3) 38 | }; 39 | 40 | public: 41 | IFile() = default; 42 | virtual ~IFile() = default; 43 | 44 | /* 45 | * Get file information 46 | */ 47 | [[nodiscard]] 48 | virtual const FileInfo& GetFileInfo() const = 0; 49 | 50 | /* 51 | * Returns file size 52 | */ 53 | [[nodiscard]] 54 | virtual uint64_t Size() const = 0; 55 | 56 | /* 57 | * Check is readonly filesystem 58 | */ 59 | [[nodiscard]] 60 | virtual bool IsReadOnly() const = 0; 61 | 62 | /* 63 | * Open file for reading/writing 64 | */ 65 | [[nodiscard]] 66 | virtual bool Open(FileMode mode) = 0; 67 | 68 | /* 69 | * Close file 70 | */ 71 | virtual void Close() = 0; 72 | 73 | /* 74 | * Check is file ready for reading/writing 75 | */ 76 | [[nodiscard]] 77 | virtual bool IsOpened() const = 0; 78 | 79 | /* 80 | * Seek on a file 81 | */ 82 | virtual uint64_t Seek(uint64_t offset, Origin origin) = 0; 83 | 84 | /* 85 | * Returns offset in file 86 | */ 87 | [[nodiscard]] 88 | virtual uint64_t Tell() const = 0; 89 | 90 | /* 91 | * Read data from file to buffer 92 | */ 93 | virtual uint64_t Read(std::span buffer) = 0; 94 | 95 | /* 96 | * Read data from file to vector 97 | */ 98 | virtual uint64_t Read(std::vector& buffer, uint64_t size) = 0; 99 | 100 | /* 101 | * Write buffer data to file 102 | */ 103 | virtual uint64_t Write(std::span buffer) = 0; 104 | 105 | /* 106 | * Write data from vector to file 107 | */ 108 | virtual uint64_t Write(const std::vector& buffer) = 0; 109 | 110 | /* 111 | * Helpers to check if mode has specific flag 112 | */ 113 | static bool ModeHasFlag(FileMode mode, FileMode flag) 114 | { 115 | return (static_cast(mode) & static_cast(flag)) != 0; 116 | } 117 | 118 | /* 119 | * Validate if mode is correct 120 | */ 121 | [[nodiscard]] 122 | static bool IsModeValid(FileMode mode) 123 | { 124 | // Must have at least read or write flag 125 | if (!ModeHasFlag(mode, FileMode::Read) && !ModeHasFlag(mode, FileMode::Write)) { 126 | return false; 127 | } 128 | 129 | // Append mode requires write flag 130 | if (ModeHasFlag(mode, FileMode::Append) && !ModeHasFlag(mode, FileMode::Write)) { 131 | return false; 132 | } 133 | 134 | // Truncate mode requires write flag 135 | if (ModeHasFlag(mode, FileMode::Truncate) && !ModeHasFlag(mode, FileMode::Write)) { 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | }; 142 | 143 | inline bool operator==(IFilePtr f1, IFilePtr f2) 144 | { 145 | if (!f1 || !f2) { 146 | return false; 147 | } 148 | 149 | return f1->GetFileInfo() == f2->GetFileInfo(); 150 | } 151 | 152 | // Overload bitwise operators for FileMode enum class 153 | constexpr IFile::FileMode operator|(IFile::FileMode lhs, IFile::FileMode rhs) { 154 | return static_cast( 155 | static_cast(lhs) | static_cast(rhs)); 156 | } 157 | 158 | constexpr IFile::FileMode operator&(IFile::FileMode lhs, IFile::FileMode rhs) { 159 | return static_cast( 160 | static_cast(lhs) & static_cast(rhs)); 161 | } 162 | 163 | constexpr IFile::FileMode operator^(IFile::FileMode lhs, IFile::FileMode rhs) { 164 | return static_cast( 165 | static_cast(lhs) ^ static_cast(rhs)); 166 | } 167 | 168 | constexpr IFile::FileMode operator~(IFile::FileMode perm) { 169 | return static_cast(~static_cast(perm)); 170 | } 171 | 172 | } // namespace vfspp 173 | 174 | #endif // VFSPP_IFILE_H 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vfspp 2 | 3 | vfspp is a C++ Virtual File System header-only library that allows manipulation of files from memory, zip archives, or native filesystems as if they were part of a single native filesystem. This is particularly useful for game developers who want to use resources in the native filesystem during development and then pack them into an archive for distribution builds. The library is thread-safe, ensuring safe operations in multi-threaded environments. Note: This library requires C++20 or later. 4 | 5 | ## Usage Example 6 | 7 | ```C++ 8 | // Register native filesystem during development or zip for distribution build 9 | 10 | // Multi-threading is enabled by defining the `VFSPP_MT_SUPPORT_ENABLED` macro at compile time 11 | // (for example: add `-DVFSPP_MT_SUPPORT_ENABLED` to your compiler flags). 12 | auto vfs = std::make_shared(); 13 | 14 | #if defined(DISTRIBUTION_BUILD) 15 | auto rootFS = vfs->CreateFileSystem("/", "Resources.zip"); 16 | #else 17 | auto rootFS = vfs->CreateFileSystem("/", GetBundlePath() + "Resources"); 18 | #endif 19 | 20 | ``` 21 | 22 | It's often useful to have several mounted filesystems. For example: 23 | - "/" as your root native filesystem 24 | - "/tmp" as a memory filesystem for working with temporary files without disk operations 25 | - "/resources" as a zip filesystem with game resources 26 | 27 | Here's an example of how to set up multiple filesystems: 28 | 29 | ```C++ 30 | 31 | // Multi-threading is enabled by defining the `VFSPP_MT_SUPPORT_ENABLED` macro at compile time 32 | // (for example: add `-DVFSPP_MT_SUPPORT_ENABLED` to your compiler flags). 33 | auto vfs = std::make_shared(); 34 | 35 | if (!vfs->CreateFileSystem("/", GetBundlePath() + "Documents/")) { 36 | // Handle error 37 | } 38 | if (!vfs->CreateFileSystem("/resources", "Resources.zip")) { 39 | // Handle error 40 | } 41 | if (!vfs->CreateFileSystem("/tmp")) { 42 | // Handle error 43 | } 44 | 45 | // Example: Open a save file 46 | if (auto saveFile = vfs->OpenFile(FileInfo("/savefile.sav"), IFile::FileMode::Read)) { 47 | // Parse game save 48 | // ... 49 | } 50 | 51 | // Example: Work with a temporary file in memory 52 | if (auto userAvatarFile = vfs->OpenFile(FileInfo("/tmp/avatar.jpg"), IFile::FileMode::ReadWrite)) { 53 | // Load avatar from network and store it in memory 54 | // ... 55 | userAvatarFile->Write(data.data(), data.size()); 56 | } 57 | 58 | // Example: Load a resource from the zip file 59 | if (auto textureFile = vfs->OpenFile(FileInfo("/resources/background.pvr"), IFile::FileMode::Read)) { 60 | // Create texture 61 | // ... 62 | } 63 | ``` 64 | 65 | ### Patching/DLC feature 66 | 67 | vfspp supports patching/DLC feature. You can mount multiple filesystems to the same alias and access files merged from all filesystems. For example, you can mount the base game filesystem and the patch filesystem. If the file is present in the patch filesystem, it will be used; otherwise, the file from the base game filesystem will be used. Search order performed from the last mounted filesystem to the first filesystem. The first filesystem mounted to the alias will be the default filesystem, so creating a new file will be created in the first 'base' filesystem. 68 | 69 | ```C++ 70 | if (!vfs->CreateFileSystem("/dlc", "test-data/dlc1")) { 71 | std::fprintf(stderr, "Failed to mount native filesystem for dlc1\n"); 72 | return 1; 73 | } 74 | 75 | if (IFilePtr dlcFile = vfs->OpenFile(FileInfo("/dlc/file.txt"), IFile::FileMode::Read)) { 76 | PrintFileContent("File /dlc/file.txt that exists in dlc1:", dlcFile); 77 | } 78 | 79 | if (!vfs->CreateFileSystem("/dlc", "test-data/dlc2")) { 80 | std::fprintf(stderr, "Failed to mount native filesystem for dlc2\n"); 81 | return 1; 82 | } 83 | 84 | if (IFilePtr dlcFile = vfs->OpenFile(FileInfo("/dlc/file.txt"), IFile::FileMode::Read)) { 85 | PrintFileContent("File /dlc/file.txt patched by dlc2:", dlcFile); 86 | } 87 | 88 | if (IFilePtr dlcFile = vfs->OpenFile(FileInfo("/dlc/file1.txt"), IFile::FileMode::Read)) { 89 | PrintFileContent("File /dlc/file1.txt that exists only in dlc1:", dlcFile); 90 | } 91 | 92 | if (IFilePtr dlcFile = vfs->OpenFile(FileInfo("/dlc/file2.txt"), IFile::FileMode::Read)) { 93 | PrintFileContent("File /dlc/file2.txt that exists only in dlc2:", dlcFile); 94 | } 95 | ``` 96 | 97 | ## How To Integrate with cmake 98 | 99 | - Add vfspp as submodule to your project 100 | ```bash 101 | git submodule add https://github.com/nextgeniuspro/vfspp.git vendor/vfspp 102 | ``` 103 | - Update submodules to download dependencies 104 | ```bash 105 | git submodule update --init --recursive 106 | ``` 107 | - Add vfspp as subdirectory to your CMakeLists.txt 108 | ```cmake 109 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vendor/vfspp vfspp_build) 110 | ``` 111 | - Add vfspp as dependency to your target and set C++20 standard 112 | ```cmake 113 | target_link_libraries( PRIVATE vfspp) 114 | target_compile_features( PRIVATE cxx_std_20) 115 | ``` 116 | 117 | See examples/CMakeLists.txt for example of usage 118 | 119 | ## How To Build Example # 120 | 121 | - Run cmake to generate project windows project files 122 | ```bash 123 | cmake -B ./build -G "Visual Studio 17 2022" . -DBUILD_EXAMPLES=1 124 | ``` 125 | or to generate Xcode project files 126 | ```bash 127 | cmake -B ./build -G "Xcode" . -DBUILD_EXAMPLES=1 128 | ``` 129 | 130 | - Open generated project files and build the target `vfsppexample` 131 | 132 | ## What to do if your system doesn't support std::filesystem? 133 | 134 | If your system's standard library doesn't support `std::filesystem`, like Sega Dreamcast under KallistiOS, you need to specify VFSPP_DISABLE_STD_FILESYSTEM macro at compile time to use emulation layer provided by vfspp. -------------------------------------------------------------------------------- /include/vfspp/ZipFile.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_ZIPFILE_HPP 2 | #define VFSPP_ZIPFILE_HPP 3 | 4 | #include "IFile.h" 5 | #include "ThreadingPolicy.hpp" 6 | #include "zip_file.hpp" 7 | 8 | #include 9 | 10 | namespace vfspp 11 | { 12 | 13 | using ZipFilePtr = std::shared_ptr; 14 | using ZipFileWeakPtr = std::weak_ptr; 15 | 16 | 17 | class ZipFile final : public IFile 18 | { 19 | public: 20 | ZipFile(const FileInfo& fileInfo, uint32_t entryID, uint64_t size, std::shared_ptr zipArchive) 21 | : m_FileInfo(fileInfo) 22 | , m_EntryID(entryID) 23 | , m_Size(size) 24 | , m_ZipArchive(zipArchive) 25 | { 26 | } 27 | 28 | ~ZipFile() 29 | { 30 | Close(); 31 | } 32 | 33 | /* 34 | * Get file information 35 | */ 36 | [[nodiscard]] 37 | virtual const FileInfo& GetFileInfo() const override 38 | { 39 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 40 | return GetFileInfoImpl(); 41 | } 42 | 43 | /* 44 | * Returns file size 45 | */ 46 | [[nodiscard]] 47 | virtual uint64_t Size() const override 48 | { 49 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 50 | return SizeImpl(); 51 | } 52 | 53 | /* 54 | * Check is readonly filesystem 55 | */ 56 | [[nodiscard]] 57 | virtual bool IsReadOnly() const override 58 | { 59 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 60 | return IsReadOnlyImpl(); 61 | } 62 | 63 | /* 64 | * Open file for reading/writting 65 | */ 66 | [[nodiscard]] 67 | virtual bool Open(FileMode mode) override 68 | { 69 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 70 | return OpenImpl(mode); 71 | } 72 | 73 | /* 74 | * Close file 75 | */ 76 | virtual void Close() override 77 | { 78 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 79 | CloseImpl(); 80 | } 81 | 82 | /* 83 | * Check is file ready for reading/writing 84 | */ 85 | [[nodiscard]] 86 | virtual bool IsOpened() const override 87 | { 88 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 89 | return IsOpenedImpl(); 90 | } 91 | 92 | /* 93 | * Seek on a file 94 | */ 95 | virtual uint64_t Seek(uint64_t offset, Origin origin) override 96 | { 97 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 98 | return SeekImpl(offset, origin); 99 | } 100 | /* 101 | * Returns offset in file 102 | */ 103 | [[nodiscard]] 104 | virtual uint64_t Tell() const override 105 | { 106 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 107 | return TellImpl(); 108 | } 109 | 110 | /* 111 | * Read data from file to buffer 112 | */ 113 | virtual uint64_t Read(std::span buffer) override 114 | { 115 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 116 | return ReadImpl(buffer); 117 | } 118 | 119 | /* 120 | * Read data from file to vector 121 | */ 122 | virtual uint64_t Read(std::vector& buffer, uint64_t size) override 123 | { 124 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 125 | return ReadImpl(buffer, size); 126 | } 127 | 128 | /* 129 | * Write buffer data to file 130 | */ 131 | virtual uint64_t Write(std::span buffer) override 132 | { 133 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 134 | return WriteImpl(buffer); 135 | } 136 | 137 | /* 138 | * Write data from vector to file 139 | */ 140 | virtual uint64_t Write(const std::vector& buffer) override 141 | { 142 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 143 | return WriteImpl(buffer); 144 | } 145 | 146 | private: 147 | inline const FileInfo& GetFileInfoImpl() const 148 | { 149 | return m_FileInfo; 150 | } 151 | 152 | inline uint64_t SizeImpl() const 153 | { 154 | return m_Size; 155 | } 156 | 157 | inline bool IsReadOnlyImpl() const 158 | { 159 | return true; 160 | } 161 | 162 | inline bool OpenImpl(FileMode mode) 163 | { 164 | if (!IFile::IsModeValid(mode)) { 165 | return false; 166 | } 167 | 168 | if (IsReadOnlyImpl() && IFile::ModeHasFlag(mode, FileMode::Write)) { 169 | return false; 170 | } 171 | 172 | if (IsOpenedImpl()) { 173 | SeekImpl(0, IFile::Origin::Begin); 174 | return true; 175 | } 176 | 177 | m_SeekPos = 0; 178 | 179 | std::shared_ptr zipArchive = m_ZipArchive.lock(); 180 | if (!zipArchive) { 181 | return false; 182 | } 183 | 184 | return true; 185 | } 186 | 187 | inline void CloseImpl() 188 | { 189 | m_SeekPos = 0; 190 | } 191 | 192 | inline bool IsOpenedImpl() const 193 | { 194 | return !m_ZipArchive.expired(); 195 | } 196 | 197 | inline uint64_t SeekImpl(uint64_t offset, Origin origin) 198 | { 199 | if (!IsOpenedImpl()) { 200 | return 0; 201 | } 202 | 203 | const auto size = SizeImpl(); 204 | 205 | if (origin == IFile::Origin::Begin) { 206 | m_SeekPos = offset; 207 | } else if (origin == IFile::Origin::End) { 208 | m_SeekPos = (offset <= size) ? size - offset : 0; 209 | } else if (origin == IFile::Origin::Set) { 210 | m_SeekPos += offset; 211 | } 212 | m_SeekPos = std::min(m_SeekPos, size); 213 | 214 | return m_SeekPos; 215 | } 216 | 217 | inline uint64_t TellImpl() const 218 | { 219 | return m_SeekPos; 220 | } 221 | 222 | struct PartialExtractContext { 223 | size_t Offset; // Bytes to skip 224 | size_t SizeToRead; // Number of bytes we want to read 225 | size_t TotalRead; // How many bytes written so far 226 | unsigned char* OutBuffer; // Pointer to output buffer 227 | }; 228 | 229 | // Callback used by miniz during extraction 230 | static size_t partialExtractCallback(void* opaque, mz_uint64 fileOffset, const void* buffer, size_t size) 231 | { 232 | PartialExtractContext* ctx = reinterpret_cast(opaque); 233 | 234 | // If data comes before the desired offset, skip it 235 | if (fileOffset + size <= ctx->Offset) { 236 | // Entire block is before the target range, skip 237 | return size; 238 | } 239 | 240 | // Determine how much of this block overlaps with the desired range 241 | size_t startInBlock = 0; 242 | if (fileOffset < ctx->Offset) { 243 | startInBlock = ctx->Offset - fileOffset; 244 | } 245 | 246 | size_t available = size - startInBlock; 247 | 248 | // Stop if we've already read enough 249 | if (ctx->TotalRead >= ctx->SizeToRead) { 250 | return size; 251 | } 252 | 253 | // How much we can copy this time 254 | size_t remaining = ctx->SizeToRead - ctx->TotalRead; 255 | size_t numToCopy = (available < remaining) ? available : remaining; 256 | 257 | // Copy data into buffer 258 | std::memcpy(ctx->OutBuffer + ctx->TotalRead, static_cast(buffer) + startInBlock, numToCopy); 259 | 260 | ctx->TotalRead += numToCopy; 261 | return size; 262 | } 263 | 264 | inline uint64_t ReadImpl(std::span buffer) 265 | { 266 | if (!IsOpenedImpl()) { 267 | return 0; 268 | } 269 | 270 | std::shared_ptr zip = m_ZipArchive.lock(); 271 | if (!zip) { 272 | return 0; 273 | } 274 | 275 | if (m_Size <= m_SeekPos) { 276 | return 0; 277 | } 278 | 279 | const auto requestedBytes = static_cast(buffer.size_bytes()); 280 | if (requestedBytes == 0) { 281 | return 0; 282 | } 283 | 284 | const auto bytesLeft = m_Size - m_SeekPos; 285 | auto bytesToRead = std::min(bytesLeft, requestedBytes); 286 | if (bytesToRead == 0) { 287 | return 0; 288 | } 289 | 290 | PartialExtractContext ctx{}; 291 | ctx.Offset = m_SeekPos; 292 | ctx.SizeToRead = bytesToRead; 293 | ctx.TotalRead = 0; 294 | ctx.OutBuffer = static_cast(buffer.data()); 295 | 296 | mz_bool ok = mz_zip_reader_extract_to_callback( 297 | zip.get(), 298 | m_EntryID, 299 | partialExtractCallback, 300 | &ctx, 301 | 0 // flags 302 | ); 303 | 304 | if (!ok) { 305 | return 0; 306 | } 307 | 308 | m_SeekPos += ctx.TotalRead; 309 | return ctx.TotalRead; 310 | } 311 | 312 | inline uint64_t WriteImpl(std::span buffer) 313 | { 314 | return 0; 315 | } 316 | 317 | inline uint64_t ReadImpl(std::vector& buffer, uint64_t size) 318 | { 319 | buffer.resize(size); 320 | return ReadImpl(std::span(buffer.data(), buffer.size())); 321 | } 322 | 323 | inline uint64_t WriteImpl(const std::vector& buffer) 324 | { 325 | return 0; 326 | } 327 | 328 | private: 329 | FileInfo m_FileInfo; 330 | uint32_t m_EntryID; 331 | uint64_t m_Size; 332 | std::weak_ptr m_ZipArchive; 333 | uint64_t m_SeekPos = 0; 334 | mutable std::mutex m_Mutex; 335 | }; 336 | 337 | } // namespace vfspp 338 | 339 | #endif // VFSPP_ZIPFILE_HPP -------------------------------------------------------------------------------- /include/vfspp/NativeFile.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_NATIVEFILE_HPP 2 | #define VFSPP_NATIVEFILE_HPP 3 | 4 | #include "IFile.h" 5 | #include "ThreadingPolicy.hpp" 6 | 7 | #ifdef VFSPP_DISABLE_STD_FILESYSTEM 8 | #include "FilesystemCompat.hpp" 9 | namespace fs = vfspp::fs_compat; 10 | #else 11 | namespace fs = std::filesystem; 12 | #endif 13 | 14 | namespace vfspp 15 | { 16 | 17 | using NativeFilePtr = std::shared_ptr; 18 | using NativeFileWeakPtr = std::weak_ptr; 19 | 20 | 21 | class NativeFile final : public IFile 22 | { 23 | public: 24 | NativeFile(const FileInfo& fileInfo) 25 | : m_FileInfo(fileInfo) 26 | { 27 | } 28 | 29 | NativeFile(const FileInfo& fileInfo, std::fstream&& stream) 30 | : m_FileInfo(fileInfo) 31 | , m_Stream(std::move(stream)) 32 | { 33 | } 34 | 35 | ~NativeFile() 36 | { 37 | Close(); 38 | } 39 | 40 | /* 41 | * Get file information 42 | */ 43 | [[nodiscard]] 44 | virtual const FileInfo& GetFileInfo() const override 45 | { 46 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 47 | return GetFileInfoImpl(); 48 | } 49 | 50 | /* 51 | * Returns file size 52 | */ 53 | [[nodiscard]] 54 | virtual uint64_t Size() const override 55 | { 56 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 57 | return SizeImpl(); 58 | } 59 | 60 | /* 61 | * Check is readonly filesystem 62 | */ 63 | [[nodiscard]] 64 | virtual bool IsReadOnly() const override 65 | { 66 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 67 | return IsReadOnlyImpl(); 68 | } 69 | 70 | /* 71 | * Open file for reading/writting 72 | */ 73 | [[nodiscard]] 74 | virtual bool Open(FileMode mode) override 75 | { 76 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 77 | return OpenImpl(mode); 78 | } 79 | 80 | /* 81 | * Close file 82 | */ 83 | virtual void Close() override 84 | { 85 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 86 | CloseImpl(); 87 | } 88 | 89 | /* 90 | * Check is file ready for reading/writing 91 | */ 92 | [[nodiscard]] 93 | virtual bool IsOpened() const override 94 | { 95 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 96 | return IsOpenedImpl(); 97 | } 98 | 99 | /* 100 | * Seek on a file 101 | */ 102 | virtual uint64_t Seek(uint64_t offset, Origin origin) override 103 | { 104 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 105 | return SeekImpl(offset, origin); 106 | } 107 | /* 108 | * Returns offset in file 109 | */ 110 | [[nodiscard]] 111 | virtual uint64_t Tell() const override 112 | { 113 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 114 | return TellImpl(); 115 | } 116 | 117 | /* 118 | * Read data from file to buffer 119 | */ 120 | virtual uint64_t Read(std::span buffer) override 121 | { 122 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 123 | return ReadImpl(buffer); 124 | } 125 | 126 | /* 127 | * Read data from file to vector 128 | */ 129 | virtual uint64_t Read(std::vector& buffer, uint64_t size) override 130 | { 131 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 132 | return ReadImpl(buffer, size); 133 | } 134 | 135 | /* 136 | * Write buffer data to file 137 | */ 138 | virtual uint64_t Write(std::span buffer) override 139 | { 140 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 141 | return WriteImpl(buffer); 142 | } 143 | 144 | /* 145 | * Write data from vector to file 146 | */ 147 | virtual uint64_t Write(const std::vector& buffer) override 148 | { 149 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 150 | return WriteImpl(buffer); 151 | } 152 | 153 | private: 154 | inline const FileInfo& GetFileInfoImpl() const 155 | { 156 | return m_FileInfo; 157 | } 158 | 159 | inline uint64_t SizeImpl() const 160 | { 161 | if (!IsOpenedImpl()) { 162 | return 0; 163 | } 164 | 165 | std::error_code ec; 166 | auto size = fs::file_size(m_FileInfo.NativePath(), ec); 167 | if (ec) { 168 | return 0; 169 | } 170 | 171 | return size; 172 | } 173 | 174 | inline bool IsReadOnlyImpl() const 175 | { 176 | return !IFile::ModeHasFlag(m_Mode, FileMode::Write); 177 | } 178 | 179 | inline bool OpenImpl(FileMode mode) 180 | { 181 | if (!IFile::IsModeValid(mode)) { 182 | return false; 183 | } 184 | 185 | if (IsOpenedImpl() && m_Mode == mode) { 186 | SeekImpl(0, Origin::Begin); 187 | return true; 188 | } 189 | 190 | m_Mode = mode; 191 | 192 | std::ios_base::openmode open_mode = std::ios_base::binary; 193 | if (IFile::ModeHasFlag(mode, FileMode::Read)) { 194 | open_mode |= std::fstream::in; 195 | } 196 | if (IFile::ModeHasFlag(mode, FileMode::Write)) { 197 | open_mode |= std::fstream::out; 198 | } 199 | if (IFile::ModeHasFlag(mode, FileMode::Append)) { 200 | open_mode |= std::fstream::app; 201 | } 202 | if (IFile::ModeHasFlag(mode, FileMode::Truncate)) { 203 | open_mode |= std::fstream::trunc; 204 | } 205 | 206 | m_Stream.open(m_FileInfo.NativePath(), open_mode); 207 | return m_Stream.is_open(); 208 | } 209 | 210 | inline void CloseImpl() 211 | { 212 | if (IsOpenedImpl()) { 213 | m_Stream.close(); 214 | m_Mode = FileMode::Read; 215 | } 216 | } 217 | 218 | inline bool IsOpenedImpl() const 219 | { 220 | return m_Stream.is_open(); 221 | } 222 | 223 | inline uint64_t SeekImpl(uint64_t offset, Origin origin) 224 | { 225 | if (!IsOpenedImpl()) { 226 | return 0; 227 | } 228 | 229 | std::ios_base::seekdir dir = std::ios_base::beg; 230 | if (origin == IFile::Origin::End) { 231 | dir = std::ios_base::end; 232 | } else if (origin == IFile::Origin::Set) { 233 | dir = std::ios_base::cur; 234 | } 235 | 236 | // Seek both read and write pointers to keep them synchronized 237 | m_Stream.seekg(offset, dir); 238 | m_Stream.seekp(offset, dir); 239 | 240 | if (m_Stream.fail()) { 241 | m_Stream.clear(); 242 | return 0; 243 | } 244 | 245 | return TellImpl(); 246 | } 247 | 248 | inline uint64_t TellImpl() const 249 | { 250 | if (!IsOpenedImpl()) { 251 | return 0; 252 | } 253 | 254 | if (IFile::ModeHasFlag(m_Mode, FileMode::Read)) { 255 | auto pos = m_Stream.tellg(); 256 | return (pos != std::streampos(-1)) ? static_cast(pos) : 0; 257 | } else if (IFile::ModeHasFlag(m_Mode, FileMode::Write)) { 258 | auto pos = m_Stream.tellp(); 259 | return (pos != std::streampos(-1)) ? static_cast(pos) : 0; 260 | } 261 | 262 | return 0; 263 | } 264 | 265 | inline uint64_t ReadImpl(std::span buffer) 266 | { 267 | if (!IsOpenedImpl()) { 268 | return 0; 269 | } 270 | 271 | // Skip reading if file is not opened for reading 272 | if (!IFile::ModeHasFlag(m_Mode, FileMode::Read)) { 273 | return 0; 274 | } 275 | 276 | const auto currentPos = TellImpl(); 277 | const auto fileSize = SizeImpl(); 278 | 279 | if (currentPos >= fileSize) { 280 | return 0; 281 | } 282 | const auto bytesLeft = fileSize - currentPos; 283 | 284 | const auto requestedBytes = static_cast(buffer.size_bytes()); 285 | if (requestedBytes == 0) { 286 | return 0; 287 | } 288 | 289 | auto bytesToRead = std::min(bytesLeft, requestedBytes); 290 | if (bytesToRead == 0) { 291 | return 0; 292 | } 293 | 294 | m_Stream.read(reinterpret_cast(buffer.data()), bytesToRead); 295 | if (m_Stream.good() || m_Stream.eof()) { 296 | return static_cast(m_Stream.gcount()); 297 | } 298 | // Clear error flags for failed read 299 | m_Stream.clear(); 300 | return 0; 301 | } 302 | 303 | inline uint64_t WriteImpl(std::span buffer) 304 | { 305 | if (!IsOpenedImpl()) { 306 | return 0; 307 | } 308 | 309 | // Skip writing if file is opened for reading only 310 | if (!IFile::ModeHasFlag(m_Mode, FileMode::Write)) { 311 | return 0; 312 | } 313 | 314 | const auto writeSize = buffer.size_bytes(); 315 | if (writeSize == 0) { 316 | return 0; 317 | } 318 | 319 | m_Stream.write(reinterpret_cast(buffer.data()), writeSize); 320 | if (m_Stream.good()) { 321 | return writeSize; 322 | } 323 | return 0; 324 | } 325 | 326 | inline uint64_t ReadImpl(std::vector& buffer, uint64_t size) 327 | { 328 | buffer.resize(size); 329 | return ReadImpl(std::span(buffer.data(), buffer.size())); 330 | } 331 | 332 | inline uint64_t WriteImpl(const std::vector& buffer) 333 | { 334 | return WriteImpl(std::span(buffer.data(), buffer.size())); 335 | } 336 | 337 | private: 338 | FileInfo m_FileInfo; 339 | mutable std::fstream m_Stream; 340 | FileMode m_Mode = FileMode::Read; 341 | mutable std::mutex m_Mutex; 342 | }; 343 | 344 | } // namespace vfspp 345 | 346 | #endif // VFSPP_NATIVEFILE_HPP -------------------------------------------------------------------------------- /include/vfspp/ZipFileSystem.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_ZIPFILESYSTEM_HPP 2 | #define VFSPP_ZIPFILESYSTEM_HPP 3 | 4 | #include "IFileSystem.h" 5 | #include "Global.h" 6 | #include "ThreadingPolicy.hpp" 7 | #include "ZipFile.hpp" 8 | #include "zip_file.hpp" 9 | 10 | #ifdef VFSPP_DISABLE_STD_FILESYSTEM 11 | #include "FilesystemCompat.hpp" 12 | namespace fs = vfspp::fs_compat; 13 | #else 14 | namespace fs = std::filesystem; 15 | #endif 16 | 17 | namespace vfspp 18 | { 19 | 20 | using ZipFileSystemPtr = std::shared_ptr; 21 | using ZipFileSystemWeakPtr = std::weak_ptr; 22 | 23 | 24 | class ZipFileSystem final : public IFileSystem 25 | { 26 | public: 27 | ZipFileSystem(const std::string& aliasPath, const std::string& zipPath) 28 | : m_AliasPath(aliasPath) 29 | , m_ZipPath(zipPath) 30 | { 31 | } 32 | 33 | ~ZipFileSystem() 34 | { 35 | Shutdown(); 36 | } 37 | 38 | /* 39 | * Initialize filesystem, call this method as soon as possible 40 | */ 41 | [[nodiscard]] 42 | virtual bool Initialize() override 43 | { 44 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 45 | return InitializeImpl(); 46 | } 47 | 48 | /* 49 | * Shutdown filesystem 50 | */ 51 | virtual void Shutdown() override 52 | { 53 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 54 | ShutdownImpl(); 55 | } 56 | 57 | /* 58 | * Check if filesystem is initialized 59 | */ 60 | [[nodiscard]] 61 | virtual bool IsInitialized() const override 62 | { 63 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 64 | return IsInitializedImpl(); 65 | } 66 | 67 | /* 68 | * Get base path 69 | */ 70 | [[nodiscard]] 71 | virtual const std::string& BasePath() const override 72 | { 73 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 74 | return BasePathImpl(); 75 | } 76 | 77 | /* 78 | * Get mounted path 79 | */ 80 | [[nodiscard]] 81 | virtual const std::string& VirtualPath() const override 82 | { 83 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 84 | return AliasPathImpl(); 85 | } 86 | 87 | /* 88 | * Retrieve file list according filter 89 | */ 90 | [[nodiscard]] 91 | virtual FilesList GetFilesList() const override 92 | { 93 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 94 | return GetFilesListST(); 95 | } 96 | 97 | /* 98 | * Check is readonly filesystem 99 | */ 100 | [[nodiscard]] 101 | virtual bool IsReadOnly() const override 102 | { 103 | return true; 104 | } 105 | 106 | /* 107 | * Open existing file for reading, if not exists returns null for readonly filesystem. 108 | * If file not exists and filesystem is writable then create new file 109 | */ 110 | virtual IFilePtr OpenFile(const std::string& virtualPath, IFile::FileMode mode) override 111 | { 112 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 113 | return OpenFileImpl(virtualPath, mode); 114 | } 115 | 116 | /* 117 | * Create file on writeable filesystem. Returns true if file created successfully 118 | */ 119 | virtual IFilePtr CreateFile(const std::string& virtualPath) override 120 | { 121 | return nullptr; 122 | } 123 | 124 | /* 125 | * Remove existing file on writable filesystem 126 | */ 127 | virtual bool RemoveFile(const std::string& virtualPath) override 128 | { 129 | return false; 130 | } 131 | 132 | /* 133 | * Copy existing file on writable filesystem 134 | */ 135 | virtual bool CopyFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) override 136 | { 137 | return false; 138 | } 139 | 140 | /* 141 | * Rename existing file on writable filesystem 142 | */ 143 | virtual bool RenameFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath) override 144 | { 145 | return false; 146 | } 147 | /* 148 | * Close file 149 | */ 150 | virtual void CloseFile(IFilePtr file) override 151 | { 152 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 153 | CloseFileImpl(file); 154 | } 155 | /* 156 | * Check if file exists on filesystem 157 | */ 158 | [[nodiscard]] 159 | virtual bool IsFileExists(const std::string& virtualPath) const override 160 | { 161 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 162 | return IsFileExistsImpl(virtualPath); 163 | } 164 | 165 | private: 166 | struct FileEntry 167 | { 168 | FileInfo Info; 169 | std::vector OpenedHandles; 170 | 171 | uint32_t EntryID; 172 | uint64_t Size; 173 | 174 | explicit FileEntry(const FileInfo& info, uint32_t entryID, uint64_t size) 175 | : Info(info) 176 | , EntryID(entryID) 177 | , Size(size) 178 | { 179 | } 180 | 181 | void CleanupOpenedHandles(IFilePtr fileToExclude = nullptr) 182 | { 183 | OpenedHandles.erase(std::remove_if(OpenedHandles.begin(), OpenedHandles.end(), [&](const ZipFileWeakPtr& weak) { 184 | return weak.expired() || weak.lock() == fileToExclude; 185 | }), OpenedHandles.end()); 186 | } 187 | }; 188 | 189 | inline bool InitializeImpl() 190 | { 191 | if (m_IsInitialized) { 192 | return true; 193 | } 194 | 195 | if (!fs::is_regular_file(m_ZipPath)) { 196 | return false; 197 | } 198 | 199 | m_ZipArchive = std::make_shared(); 200 | 201 | mz_bool status = mz_zip_reader_init_file(m_ZipArchive.get(), m_ZipPath.c_str(), 0); 202 | if (!status) { 203 | return false; 204 | } 205 | 206 | BuildFilelist(AliasPathImpl(), BasePathImpl(), m_ZipArchive, m_Files); 207 | m_IsInitialized = true; 208 | return true; 209 | } 210 | 211 | inline void ShutdownImpl() 212 | { 213 | m_ZipPath = ""; 214 | m_Files.clear(); 215 | 216 | // close zip archive 217 | if (m_ZipArchive) { 218 | mz_zip_reader_end(m_ZipArchive.get()); 219 | m_ZipArchive = nullptr; 220 | } 221 | 222 | m_IsInitialized = false; 223 | } 224 | 225 | inline bool IsInitializedImpl() const 226 | { 227 | return m_IsInitialized; 228 | } 229 | 230 | inline const std::string& BasePathImpl() const 231 | { 232 | return m_BasePath; // Empty for zip filesystem 233 | } 234 | 235 | inline const std::string& AliasPathImpl() const 236 | { 237 | return m_AliasPath; 238 | } 239 | 240 | inline FilesList GetFilesListST() const 241 | { 242 | FilesList fileList; 243 | fileList.reserve(m_Files.size()); 244 | for (const auto& [path, entry] : m_Files) { 245 | fileList.push_back(entry.Info); 246 | } 247 | return fileList; 248 | } 249 | 250 | inline IFilePtr OpenFileImpl(const std::string& virtualPath, IFile::FileMode mode) 251 | { 252 | const auto entryIt = m_Files.find(virtualPath); 253 | if (entryIt == m_Files.end()) { 254 | return nullptr; 255 | } 256 | auto& entry = entryIt->second; 257 | 258 | ZipFilePtr file = std::make_shared(entry.Info, entry.EntryID, entry.Size, m_ZipArchive); 259 | if (!file || !file->Open(mode)) { 260 | return nullptr; 261 | } 262 | 263 | entry.OpenedHandles.push_back(file); 264 | 265 | return file; 266 | } 267 | 268 | inline void CloseFileImpl(IFilePtr file) 269 | { 270 | CloseFileAndCleanupOpenedHandles(file); 271 | } 272 | 273 | inline bool IsFileExistsImpl(const std::string& virtualPath) const 274 | { 275 | return m_Files.find(virtualPath) != m_Files.end(); 276 | } 277 | 278 | void BuildFilelist(const std::string& aliasPath, const std::string& basePath, std::shared_ptr zipArchive, std::unordered_map& outFiles) 279 | { 280 | for (mz_uint i = 0; i < mz_zip_reader_get_num_files(zipArchive.get()); i++) { 281 | mz_zip_archive_file_stat file_stat; 282 | if (!mz_zip_reader_file_stat(zipArchive.get(), i, &file_stat)) { 283 | // TODO: log error 284 | continue; 285 | } 286 | 287 | // Skip directories (entries ending with '/') 288 | std::string filename = file_stat.m_filename; 289 | if (!filename.empty() && filename.back() == '/') { 290 | continue; 291 | } 292 | 293 | FileInfo fileInfo(aliasPath, basePath, filename); 294 | outFiles.emplace( 295 | fileInfo.VirtualPath(), 296 | FileEntry( 297 | fileInfo, 298 | static_cast(file_stat.m_file_index), 299 | static_cast(file_stat.m_uncomp_size) 300 | ) 301 | ); 302 | } 303 | } 304 | 305 | inline void CloseFileAndCleanupOpenedHandles(IFilePtr fileToClose = nullptr) 306 | { 307 | if (fileToClose) { 308 | const auto absolutePath = fileToClose->GetFileInfo().VirtualPath(); 309 | 310 | const auto it = m_Files.find(absolutePath); 311 | if (it == m_Files.end()) { 312 | return; 313 | } 314 | 315 | fileToClose->Close(); 316 | } 317 | 318 | for (auto& [path, entry] : m_Files) { 319 | entry.CleanupOpenedHandles(fileToClose); 320 | } 321 | } 322 | 323 | private: 324 | std::string m_AliasPath; 325 | std::string m_BasePath; 326 | std::string m_ZipPath; 327 | std::shared_ptr m_ZipArchive = nullptr; 328 | bool m_IsInitialized = false; 329 | mutable std::mutex m_Mutex; 330 | 331 | std::unordered_map m_Files; 332 | }; 333 | 334 | } // namespace vfspp 335 | 336 | #endif // VFSPP_ZIPFILESYSTEM_HPP 337 | -------------------------------------------------------------------------------- /include/vfspp/VirtualFileSystem.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_VIRTUALFILESYSTEM_HPP 2 | #define VFSPP_VIRTUALFILESYSTEM_HPP 3 | 4 | #include "IFileSystem.h" 5 | #include "IFile.h" 6 | #include "Alias.hpp" 7 | #include "ThreadingPolicy.hpp" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | namespace vfspp 15 | { 16 | 17 | using VirtualFileSystemPtr = std::shared_ptr; 18 | using VirtualFileSystemWeakPtr = std::weak_ptr; 19 | 20 | 21 | class VirtualFileSystem final 22 | { 23 | public: 24 | using FileSystemList = std::vector; 25 | using FileSystemMap = std::unordered_map; 26 | 27 | public: 28 | VirtualFileSystem() 29 | { 30 | } 31 | 32 | ~VirtualFileSystem() 33 | { 34 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 35 | for (const auto& fs : m_FileSystems) { 36 | for (const auto& f : fs.second) { 37 | f->Shutdown(); 38 | } 39 | } 40 | } 41 | 42 | /* 43 | * Register new filesystem. Alias is a base prefix to file access. 44 | * For ex. registered filesystem has base path '/home/media', but registered 45 | * with alias '/', so it possible to access files with path '/filename' 46 | * instead of '/home/media/filename 47 | */ 48 | void AddFileSystem(const Alias& alias, IFileSystemPtr filesystem) 49 | { 50 | if (!filesystem) { 51 | return; 52 | } 53 | 54 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 55 | 56 | m_FileSystems[alias].push_back(filesystem); 57 | if (std::find(m_SortedAlias.begin(), m_SortedAlias.end(), alias) == m_SortedAlias.end()) { 58 | m_SortedAlias.push_back(alias); 59 | } 60 | std::sort(m_SortedAlias.begin(), m_SortedAlias.end(), [](const Alias& first, const Alias& second) { 61 | return first.Length() > second.Length(); 62 | }); 63 | } 64 | 65 | void AddFileSystem(std::string alias, IFileSystemPtr filesystem) 66 | { 67 | AddFileSystem(Alias(std::move(alias)), filesystem); 68 | } 69 | 70 | template 71 | [[nodiscard]] auto CreateFileSystem(const Alias& alias, Args&&... args) -> std::optional> 72 | { 73 | auto filesystem = std::make_shared(alias.String(), std::forward(args)...); 74 | if (!filesystem) { 75 | return {}; 76 | } 77 | 78 | if (!filesystem->Initialize()) { 79 | return {}; 80 | } 81 | 82 | AddFileSystem(alias, filesystem); 83 | return filesystem; 84 | } 85 | 86 | template 87 | [[nodiscard]] auto CreateFileSystem(std::string alias, Args&&... args) -> std::optional> 88 | { 89 | return CreateFileSystem(Alias(std::move(alias)), std::forward(args)...); 90 | } 91 | 92 | /* 93 | * Remove registered filesystem 94 | */ 95 | void RemoveFileSystem(const Alias& alias, IFileSystemPtr filesystem) 96 | { 97 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 98 | 99 | auto it = m_FileSystems.find(alias); 100 | if (it != m_FileSystems.end()) { 101 | auto &list = it->second; 102 | list.erase(std::remove(list.begin(), list.end(), filesystem), list.end()); 103 | if (list.empty()) { 104 | m_FileSystems.erase(it); 105 | m_SortedAlias.erase(std::remove(m_SortedAlias.begin(), m_SortedAlias.end(), alias), m_SortedAlias.end()); 106 | } 107 | } 108 | } 109 | 110 | void RemoveFileSystem(std::string alias, IFileSystemPtr filesystem) 111 | { 112 | RemoveFileSystem(Alias(std::move(alias)), filesystem); 113 | } 114 | 115 | /* 116 | * Check if filesystem with 'alias' added 117 | */ 118 | [[nodiscard]] 119 | bool HasFileSystem(const Alias& alias, IFileSystemPtr fileSystem) const 120 | { 121 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 122 | auto it = m_FileSystems.find(alias); 123 | if (it != m_FileSystems.end()) { 124 | return std::find(it->second.begin(), it->second.end(), fileSystem) != it->second.end(); 125 | } 126 | return false; 127 | } 128 | 129 | [[nodiscard]] 130 | bool HasFileSystem(std::string alias, IFileSystemPtr fileSystem) const 131 | { 132 | return HasFileSystem(Alias(std::move(alias)), fileSystem); 133 | } 134 | 135 | /* 136 | * Unregister all filesystems with 'alias' 137 | */ 138 | void UnregisterAlias(const Alias& alias) 139 | { 140 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 141 | m_FileSystems.erase(alias); 142 | m_SortedAlias.erase(std::remove(m_SortedAlias.begin(), m_SortedAlias.end(), alias), m_SortedAlias.end()); 143 | } 144 | 145 | void UnregisterAlias(std::string alias) 146 | { 147 | UnregisterAlias(Alias(std::move(alias))); 148 | } 149 | 150 | /* 151 | * Check if there any filesystem with 'alias' registered 152 | */ 153 | [[nodiscard]] 154 | bool IsAliasRegistered(const Alias& alias) const 155 | { 156 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 157 | return m_FileSystems.find(alias) != m_FileSystems.end(); 158 | } 159 | 160 | [[nodiscard]] 161 | bool IsAliasRegistered(std::string alias) const 162 | { 163 | return IsAliasRegistered(Alias(std::move(alias))); 164 | } 165 | 166 | /* 167 | * Get all added filesystems with 'alias' 168 | */ 169 | [[nodiscard]] 170 | std::optional> GetFilesystems(const Alias& alias) 171 | { 172 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 173 | 174 | return GetFilesystemsImpl(alias); 175 | } 176 | 177 | [[nodiscard]] 178 | std::optional> GetFilesystems(std::string alias) 179 | { 180 | return GetFilesystems(Alias(std::move(alias))); 181 | } 182 | 183 | private: 184 | template 185 | auto VisitMountedFileSystems(const std::string& virtualPath, Callback&& callback) const 186 | { 187 | using CallbackResult = decltype(callback(std::declval(), std::declval())); 188 | 189 | for (const Alias& alias : m_SortedAlias) { 190 | if (!virtualPath.starts_with(alias.String())) { 191 | continue; 192 | } 193 | 194 | auto fsResult = GetFilesystemsImpl(alias); 195 | if (!fsResult) { 196 | continue; 197 | } 198 | 199 | const auto& filesystems = fsResult->get(); 200 | for (auto it = filesystems.rbegin(); it != filesystems.rend(); ++it) { 201 | IFileSystemPtr fs = *it; 202 | bool isMain = (fs == filesystems.front()); 203 | 204 | CallbackResult result = callback(fs, isMain); 205 | if (result) { 206 | return result; 207 | } 208 | } 209 | } 210 | 211 | return CallbackResult{}; 212 | } 213 | 214 | public: 215 | 216 | /* 217 | * Iterate over all registered filesystems and find first ocurrences of file. 218 | * Iteration occurs from the most recently added filesystem to the oldest one. 219 | */ 220 | IFilePtr OpenFile(const std::string& virtualPath, IFile::FileMode mode) 221 | { 222 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 223 | 224 | auto result = VisitMountedFileSystems(virtualPath, [&](IFileSystemPtr fs, bool /*isMain*/) -> std::optional { 225 | if (fs->IsFileExists(virtualPath)) { 226 | if (IFilePtr file = fs->OpenFile(virtualPath, mode)) { 227 | return file; 228 | } 229 | } 230 | return std::nullopt; 231 | }); 232 | 233 | return result.value_or(nullptr); 234 | } 235 | 236 | /* 237 | * Check if file exists in any registered filesystem 238 | */ 239 | bool IsFileExists(const std::string& virtualPath) const 240 | { 241 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 242 | 243 | auto result = VisitMountedFileSystems(virtualPath, [&](IFileSystemPtr fs, bool /*isMain*/) -> std::optional { 244 | if (fs->IsFileExists(virtualPath)) { 245 | return true; 246 | } 247 | return std::nullopt; 248 | }); 249 | 250 | return result.value_or(false); 251 | } 252 | 253 | /* 254 | * List all files from all registered filesystems 255 | * Returns a vector of all file paths with their aliases 256 | * Files from later registered filesystems override earlier ones 257 | */ 258 | std::vector ListAllFiles() const 259 | { 260 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 261 | 262 | std::vector allFiles; 263 | std::unordered_set seenFiles; 264 | 265 | for (const Alias& alias : m_SortedAlias) { 266 | auto fsResult = GetFilesystemsImpl(alias); 267 | if (!fsResult) { 268 | continue; 269 | } 270 | 271 | const auto& filesystems = fsResult->get(); 272 | for (auto it = filesystems.rbegin(); it != filesystems.rend(); ++it) { 273 | 274 | IFileSystemPtr fs = *it; 275 | const IFileSystem::FilesList& fileList = fs->GetFilesList(); 276 | 277 | for (const auto& fileInfo : fileList) { 278 | const auto& virtualPath = fileInfo.VirtualPath(); 279 | if (seenFiles.emplace(virtualPath).second) { 280 | allFiles.push_back(std::move(virtualPath)); 281 | } 282 | } 283 | } 284 | } 285 | 286 | std::sort(allFiles.begin(), allFiles.end()); 287 | return allFiles; 288 | } 289 | 290 | private: 291 | [[nodiscard]] 292 | std::optional> GetFilesystemsImpl(const Alias& alias) const 293 | { 294 | auto it = m_FileSystems.find(alias); 295 | if (it != m_FileSystems.end()) { 296 | return std::cref(it->second); 297 | } 298 | 299 | return {}; 300 | } 301 | 302 | private: 303 | FileSystemMap m_FileSystems; 304 | std::vector m_SortedAlias; 305 | mutable std::mutex m_Mutex; 306 | }; 307 | 308 | 309 | }; // namespace vfspp 310 | 311 | #endif // VFSPP_VIRTUALFILESYSTEM_HPP 312 | -------------------------------------------------------------------------------- /include/vfspp/MemoryFileSystem.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_MEMORYFILESYSTEM_HPP 2 | #define VFSPP_MEMORYFILESYSTEM_HPP 3 | 4 | #include "IFileSystem.h" 5 | #include "Global.h" 6 | #include "MemoryFile.hpp" 7 | 8 | namespace vfspp 9 | { 10 | 11 | using MemoryFileSystemPtr = std::shared_ptr; 12 | using MemoryFileSystemWeakPtr = std::weak_ptr; 13 | 14 | 15 | class MemoryFileSystem final : public IFileSystem 16 | { 17 | public: 18 | MemoryFileSystem(const std::string& aliasPath) 19 | : m_AliasPath(aliasPath) 20 | { 21 | } 22 | 23 | ~MemoryFileSystem() 24 | { 25 | Shutdown(); 26 | } 27 | 28 | /* 29 | * Initialize filesystem, call this method as soon as possible 30 | */ 31 | [[nodiscard]] 32 | virtual bool Initialize() override 33 | { 34 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 35 | return InitializeImpl(); 36 | } 37 | 38 | /* 39 | * Shutdown filesystem 40 | */ 41 | virtual void Shutdown() override 42 | { 43 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 44 | ShutdownImpl(); 45 | } 46 | 47 | /* 48 | * Check if filesystem is initialized 49 | */ 50 | [[nodiscard]] 51 | virtual bool IsInitialized() const override 52 | { 53 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 54 | return IsInitializedImpl(); 55 | } 56 | 57 | /* 58 | * Get base path 59 | */ 60 | [[nodiscard]] 61 | virtual const std::string& BasePath() const override 62 | { 63 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 64 | return BasePathImpl(); 65 | } 66 | 67 | /* 68 | * Get mounted path 69 | */ 70 | [[nodiscard]] 71 | virtual const std::string& VirtualPath() const override 72 | { 73 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 74 | return AliasPathImpl(); 75 | } 76 | 77 | /* 78 | * Retrieve all files in filesystem. Heavy operation, avoid calling this often 79 | */ 80 | [[nodiscard]] 81 | virtual FilesList GetFilesList() const override 82 | { 83 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 84 | return GetFilesListImpl(); 85 | } 86 | 87 | /* 88 | * Check is readonly filesystem 89 | */ 90 | [[nodiscard]] 91 | virtual bool IsReadOnly() const override 92 | { 93 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 94 | return IsReadOnlyImpl(); 95 | } 96 | 97 | /* 98 | * Open existing file for reading, if not exists returns null for readonly filesystem. 99 | * If file not exists and filesystem is writable then create new file 100 | */ 101 | virtual IFilePtr OpenFile(const std::string& virtualPath, IFile::FileMode mode) override 102 | { 103 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 104 | return OpenFileImpl(virtualPath, mode); 105 | } 106 | 107 | /* 108 | * Close file 109 | */ 110 | virtual void CloseFile(IFilePtr file) override 111 | { 112 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 113 | CloseFileImpl(file); 114 | } 115 | 116 | /* 117 | * Create file on writeable filesystem. Returns IFilePtr object for created file 118 | */ 119 | virtual IFilePtr CreateFile(const std::string& virtualPath) override 120 | { 121 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 122 | return OpenFileImpl(virtualPath, IFile::FileMode::ReadWrite | IFile::FileMode::Truncate); 123 | } 124 | 125 | /* 126 | * Remove existing file on writable filesystem 127 | */ 128 | virtual bool RemoveFile(const std::string& virtualPath) override 129 | { 130 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 131 | return RemoveFileImpl(virtualPath); 132 | } 133 | 134 | /* 135 | * Copy existing file on writable filesystem 136 | */ 137 | virtual bool CopyFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) override 138 | { 139 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 140 | return CopyFileImpl(srcVirtualPath, dstVirtualPath, overwrite); 141 | } 142 | 143 | /* 144 | * Rename existing file on writable filesystem 145 | */ 146 | virtual bool RenameFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath) override 147 | { 148 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 149 | return RenameFileImpl(srcVirtualPath, dstVirtualPath); 150 | } 151 | 152 | /* 153 | * Check if file exists on filesystem 154 | */ 155 | [[nodiscard]] 156 | virtual bool IsFileExists(const std::string& virtualPath) const override 157 | { 158 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 159 | return IsFileExistsImpl(virtualPath); 160 | } 161 | 162 | private: 163 | inline bool InitializeImpl() 164 | { 165 | if (m_IsInitialized) { 166 | return true; 167 | } 168 | m_IsInitialized = true; 169 | return m_IsInitialized; 170 | } 171 | 172 | inline void ShutdownImpl() 173 | { 174 | m_Files.clear(); 175 | 176 | m_IsInitialized = false; 177 | } 178 | 179 | inline bool IsInitializedImpl() const 180 | { 181 | return m_IsInitialized; 182 | } 183 | 184 | const std::string& BasePathImpl() const 185 | { 186 | return m_AliasPath; // Native base path is always match to alias path for memory filesystem 187 | } 188 | 189 | const std::string& AliasPathImpl() const 190 | { 191 | return m_AliasPath; 192 | } 193 | 194 | inline FilesList GetFilesListImpl() const 195 | { 196 | FilesList list; 197 | list.reserve(m_Files.size()); 198 | for (const auto& [path, entry] : m_Files) { 199 | list.push_back(entry.Info); 200 | } 201 | return list; 202 | } 203 | 204 | inline bool IsReadOnlyImpl() const 205 | { 206 | return false; 207 | } 208 | 209 | inline IFilePtr OpenFileImpl(const std::string& virtualPath, IFile::FileMode mode) 210 | { 211 | FileInfo fileInfo(AliasPathImpl(), BasePathImpl(), virtualPath); 212 | 213 | auto [entryIt, inserted] = m_Files.try_emplace(virtualPath, fileInfo, std::make_shared()); 214 | if (!inserted) { 215 | return nullptr; 216 | } 217 | 218 | auto& entry = entryIt->second; 219 | if (!entry.Object) { 220 | entry.Object = std::make_shared(); 221 | } 222 | if (!entry.Object) { 223 | return nullptr; 224 | } 225 | 226 | auto file = std::make_shared(entry.Info, entry.Object); 227 | if (!file || !file->Open(mode)) { 228 | return nullptr; 229 | } 230 | 231 | entry.OpenedHandles.push_back(file); 232 | 233 | return file; 234 | } 235 | 236 | inline void CloseFileImpl(IFilePtr file) 237 | { 238 | CloseFileAndCleanupOpenedHandles(file); 239 | } 240 | 241 | inline bool RemoveFileImpl(const std::string& virtualPath) 242 | { 243 | auto it = m_Files.find(virtualPath); 244 | if (it == m_Files.end()) { 245 | return false; 246 | } 247 | 248 | CloseFileAndCleanupOpenedHandles(); 249 | 250 | m_Files.erase(it); 251 | 252 | return true; 253 | } 254 | 255 | inline bool CopyFileImpl(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) 256 | { 257 | const auto srcIt = m_Files.find(srcVirtualPath); 258 | if (srcIt == m_Files.end()) { 259 | return false; 260 | } 261 | 262 | const auto destIt = m_Files.find(dstVirtualPath); 263 | 264 | // Destination file exists and overwrite is false 265 | if (destIt != m_Files.end() && !overwrite) { 266 | return false; 267 | } 268 | 269 | // Remove existing destination file 270 | if (destIt != m_Files.end()) { 271 | m_Files.erase(destIt); 272 | } 273 | 274 | // Create copy of memory object 275 | MemoryFileObjectPtr newObject; 276 | if (srcIt->second.Object) { 277 | newObject = std::make_shared(*srcIt->second.Object); 278 | } else { 279 | newObject = std::make_shared(); 280 | } 281 | 282 | FileInfo fileInfo(AliasPathImpl(), BasePathImpl(), dstVirtualPath); 283 | m_Files.insert({dstVirtualPath, FileEntry(fileInfo, std::move(newObject))}); 284 | 285 | return true; 286 | } 287 | 288 | inline bool RenameFileImpl(const std::string& srcVirtualPath, const std::string& dstVirtualPath) 289 | { 290 | bool result = CopyFileImpl(srcVirtualPath, dstVirtualPath, false); 291 | if (result) { 292 | result = RemoveFileImpl(srcVirtualPath); 293 | } 294 | 295 | return result; 296 | } 297 | 298 | inline bool IsFileExistsImpl(const std::string& virtualPath) const 299 | { 300 | return m_Files.find(virtualPath) != m_Files.end(); 301 | } 302 | 303 | inline void CloseFileAndCleanupOpenedHandles(IFilePtr fileToClose = nullptr) 304 | { 305 | if (fileToClose) { 306 | const auto absolutePath = fileToClose->GetFileInfo().VirtualPath(); 307 | 308 | const auto it = m_Files.find(absolutePath); 309 | if (it == m_Files.end()) { 310 | return; 311 | } 312 | 313 | fileToClose->Close(); 314 | } 315 | 316 | for (auto& [path, entry] : m_Files) { 317 | entry.CleanupOpenedHandles(fileToClose); 318 | } 319 | } 320 | 321 | private: 322 | std::string m_AliasPath; 323 | bool m_IsInitialized = false; 324 | mutable std::mutex m_Mutex; 325 | 326 | struct FileEntry 327 | { 328 | FileInfo Info; 329 | MemoryFileObjectPtr Object; 330 | using WeakHandle = MemoryFileWeakPtr; 331 | std::vector OpenedHandles; 332 | 333 | FileEntry(const FileInfo& info, MemoryFileObjectPtr object) 334 | : Info(info) 335 | , Object(object) 336 | { 337 | } 338 | 339 | void CleanupOpenedHandles(IFilePtr fileToExclude = nullptr) 340 | { 341 | OpenedHandles.erase(std::remove_if(OpenedHandles.begin(), OpenedHandles.end(), [&](const WeakHandle& weak) { 342 | return weak.expired() || weak.lock() == fileToExclude; 343 | }), OpenedHandles.end()); 344 | } 345 | }; 346 | 347 | std::unordered_map m_Files; 348 | }; 349 | 350 | } // namespace vfspp 351 | 352 | #endif // VFSPP_MEMORYFILESYSTEM_HPP 353 | -------------------------------------------------------------------------------- /include/vfspp/MemoryFile.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_MEMORYFILE_HPP 2 | #define VFSPP_MEMORYFILE_HPP 3 | 4 | #include "IFile.h" 5 | #include "ThreadingPolicy.hpp" 6 | 7 | namespace vfspp 8 | { 9 | 10 | using MemoryFileObjectPtr = std::shared_ptr; 11 | using MemoryFileObjectWeakPtr = std::weak_ptr; 12 | 13 | using MemoryFilePtr = std::shared_ptr; 14 | using MemoryFileWeakPtr = std::weak_ptr; 15 | 16 | class MemoryFileObject 17 | { 18 | friend class MemoryFile; 19 | friend class MemoryFileSystem; 20 | 21 | using DataPtr = std::shared_ptr>; 22 | 23 | public: 24 | MemoryFileObject() 25 | { 26 | } 27 | 28 | MemoryFileObject(const MemoryFileObject& other) 29 | { 30 | CopyFrom(other); 31 | } 32 | 33 | MemoryFileObject& operator=(const MemoryFileObject& other) 34 | { 35 | if (this != &other) { 36 | CopyFrom(other); 37 | } 38 | return *this; 39 | } 40 | 41 | // Read access 42 | [[nodiscard]] 43 | DataPtr GetData() const noexcept 44 | { 45 | return std::atomic_load(&m_Data); 46 | } 47 | 48 | // Write access (copy-on-write) 49 | [[nodiscard]] 50 | DataPtr GetWritableData() 51 | { 52 | auto old = std::atomic_load(&m_Data); 53 | 54 | if (!old || old.use_count() == 1) { 55 | return old; 56 | } 57 | 58 | // Copy when shared with someone else 59 | auto copy = std::make_shared>(*old); 60 | std::atomic_store(&m_Data, copy); 61 | return copy; 62 | } 63 | 64 | void Reset() noexcept 65 | { 66 | std::atomic_store(&m_Data, std::make_shared>()); 67 | } 68 | 69 | void CopyFrom(const MemoryFileObject& other) 70 | { 71 | auto otherData = other.GetData(); 72 | if (!otherData) { 73 | Reset(); 74 | return; 75 | } 76 | 77 | auto copy = std::make_shared>(*otherData); 78 | std::atomic_store(&m_Data, std::move(copy)); 79 | } 80 | 81 | private: 82 | DataPtr m_Data = std::make_shared>(); 83 | }; 84 | 85 | 86 | 87 | class MemoryFile final : public IFile 88 | { 89 | friend class MemoryFileSystem; 90 | 91 | public: 92 | MemoryFile(const FileInfo& fileInfo, MemoryFileObjectPtr object) 93 | : m_Object(std::move(object)) 94 | , m_FileInfo(fileInfo) 95 | { 96 | if (!m_Object) { 97 | m_Object = std::make_shared(); 98 | } 99 | } 100 | 101 | ~MemoryFile() 102 | { 103 | Close(); 104 | } 105 | 106 | /* 107 | * Get file information 108 | */ 109 | [[nodiscard]] 110 | virtual const FileInfo& GetFileInfo() const override 111 | { 112 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 113 | return GetFileInfoImpl(); 114 | } 115 | 116 | /* 117 | * Returns file size 118 | */ 119 | [[nodiscard]] 120 | virtual uint64_t Size() const override 121 | { 122 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 123 | return SizeImpl(); 124 | } 125 | 126 | /* 127 | * Check is readonly filesystem 128 | */ 129 | [[nodiscard]] 130 | virtual bool IsReadOnly() const override 131 | { 132 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 133 | return IsReadOnlyImpl(); 134 | } 135 | 136 | /* 137 | * Open file for reading/writting 138 | */ 139 | [[nodiscard]] 140 | virtual bool Open(FileMode mode) override 141 | { 142 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 143 | return OpenImpl(mode); 144 | } 145 | 146 | /* 147 | * Close file 148 | */ 149 | virtual void Close() override 150 | { 151 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 152 | CloseImpl(); 153 | } 154 | 155 | /* 156 | * Check is file ready for reading/writing 157 | */ 158 | [[nodiscard]] 159 | virtual bool IsOpened() const override 160 | { 161 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 162 | return IsOpenedImpl(); 163 | } 164 | 165 | /* 166 | * Seek on a file 167 | */ 168 | virtual uint64_t Seek(uint64_t offset, Origin origin) override 169 | { 170 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 171 | return SeekImpl(offset, origin); 172 | } 173 | /* 174 | * Returns offset in file 175 | */ 176 | [[nodiscard]] 177 | virtual uint64_t Tell() const override 178 | { 179 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 180 | return TellImpl(); 181 | } 182 | 183 | /* 184 | * Read data from file to buffer 185 | */ 186 | virtual uint64_t Read(std::span buffer) override 187 | { 188 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 189 | return ReadImpl(buffer); 190 | } 191 | 192 | /* 193 | * Read data from file to vector 194 | */ 195 | virtual uint64_t Read(std::vector& buffer, uint64_t size) override 196 | { 197 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 198 | return ReadImpl(buffer, size); 199 | } 200 | 201 | /* 202 | * Write buffer data to file 203 | */ 204 | virtual uint64_t Write(std::span buffer) override 205 | { 206 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 207 | return WriteImpl(buffer); 208 | } 209 | 210 | /* 211 | * Write data from vector to file 212 | */ 213 | virtual uint64_t Write(const std::vector& buffer) override 214 | { 215 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 216 | return WriteImpl(buffer); 217 | } 218 | 219 | private: 220 | inline MemoryFileObject& Object() const 221 | { 222 | assert(m_Object); 223 | return *m_Object; 224 | } 225 | 226 | inline const FileInfo& GetFileInfoImpl() const 227 | { 228 | return m_FileInfo; 229 | } 230 | 231 | inline uint64_t SizeImpl() const 232 | { 233 | if (!IsOpenedImpl()) { 234 | return 0; 235 | } 236 | 237 | auto data = m_Object->GetData(); 238 | if (!data) { 239 | return 0; 240 | } 241 | 242 | return data->size(); 243 | } 244 | 245 | inline bool IsReadOnlyImpl() const 246 | { 247 | return !IFile::ModeHasFlag(m_Mode, FileMode::Write); 248 | } 249 | 250 | inline bool OpenImpl(FileMode mode) 251 | { 252 | if (!IFile::IsModeValid(mode)) { 253 | return false; 254 | } 255 | 256 | if (IsOpenedImpl() && m_Mode == mode) { 257 | SeekImpl(0, Origin::Begin); 258 | return true; 259 | } 260 | 261 | m_Mode = mode; 262 | m_SeekPos = 0; 263 | 264 | if (IFile::ModeHasFlag(mode, FileMode::Append)) { 265 | const auto size = SizeImpl(); 266 | m_SeekPos = size > 0 ? size - 1 : 0; 267 | } 268 | if (IFile::ModeHasFlag(mode, FileMode::Truncate)) { 269 | m_Object->Reset(); 270 | } 271 | 272 | m_IsOpened = true; 273 | return true; 274 | } 275 | 276 | inline void CloseImpl() 277 | { 278 | m_IsOpened = false; 279 | m_SeekPos = 0; 280 | m_Mode = FileMode::Read; 281 | } 282 | 283 | inline bool IsOpenedImpl() const 284 | { 285 | return m_IsOpened; 286 | } 287 | 288 | inline uint64_t SeekImpl(uint64_t offset, Origin origin) 289 | { 290 | if (!IsOpenedImpl()) { 291 | return 0; 292 | } 293 | 294 | const auto size = SizeImpl(); 295 | 296 | if (origin == Origin::Begin) { 297 | m_SeekPos = offset; 298 | } else if (origin == Origin::End) { 299 | m_SeekPos = (offset < size) ? size - offset : 0; 300 | } else if (origin == Origin::Set) { 301 | m_SeekPos += offset; 302 | } 303 | m_SeekPos = std::min(m_SeekPos, size); 304 | 305 | return m_SeekPos; 306 | } 307 | 308 | inline uint64_t TellImpl() const 309 | { 310 | return m_SeekPos; 311 | } 312 | 313 | inline uint64_t ReadImpl(std::span buffer) 314 | { 315 | if (!IsOpenedImpl()) { 316 | return 0; 317 | } 318 | 319 | // Skip reading if file is not opened for reading 320 | if (!IFile::ModeHasFlag(m_Mode, FileMode::Read)) { 321 | return 0; 322 | } 323 | 324 | auto data = m_Object->GetData(); 325 | if (!data) { 326 | return 0; 327 | } 328 | 329 | const auto availableBytes = data->size(); 330 | if (availableBytes <= m_SeekPos) { 331 | return 0; 332 | } 333 | 334 | const auto requestedBytes = static_cast(buffer.size_bytes()); 335 | if (requestedBytes == 0) { 336 | return 0; 337 | } 338 | 339 | const auto bytesLeft = availableBytes - m_SeekPos; 340 | auto bytesToRead = std::min(bytesLeft, requestedBytes); 341 | if (bytesToRead == 0) { 342 | return 0; 343 | } 344 | 345 | std::memcpy(buffer.data(), data->data() + m_SeekPos, static_cast(bytesToRead)); 346 | m_SeekPos += bytesToRead; 347 | return bytesToRead; 348 | } 349 | 350 | inline uint64_t WriteImpl(std::span buffer) 351 | { 352 | if (!IsOpenedImpl()) { 353 | return 0; 354 | } 355 | 356 | // Skip writing if file is opened for reading only 357 | if (!IFile::ModeHasFlag(m_Mode, FileMode::Write)) { 358 | return 0; 359 | } 360 | 361 | // Copy on write 362 | auto data = m_Object->GetWritableData(); 363 | if (!data) { 364 | return 0; 365 | } 366 | 367 | const auto writeSize = buffer.size_bytes(); 368 | if (writeSize == 0) { 369 | return 0; 370 | } 371 | 372 | if (m_SeekPos + writeSize > data->size()) { 373 | data->resize(m_SeekPos + writeSize); 374 | } 375 | 376 | std::memcpy(data->data() + m_SeekPos, buffer.data(), writeSize); 377 | 378 | m_SeekPos += writeSize; 379 | return writeSize; 380 | } 381 | 382 | inline uint64_t ReadImpl(std::vector& buffer, uint64_t size) 383 | { 384 | buffer.resize(size); 385 | return ReadImpl(std::span(buffer.data(), buffer.size())); 386 | } 387 | 388 | inline uint64_t WriteImpl(const std::vector& buffer) 389 | { 390 | return WriteImpl(std::span(buffer.data(), buffer.size())); 391 | } 392 | 393 | private: 394 | MemoryFileObjectPtr m_Object; 395 | FileInfo m_FileInfo; 396 | bool m_IsOpened = false; 397 | uint64_t m_SeekPos = 0; 398 | FileMode m_Mode = FileMode::Read; 399 | mutable std::mutex m_Mutex; 400 | }; 401 | 402 | } // namespace vfspp 403 | 404 | #endif // VFSPP_MEMORYFILE_HPP 405 | -------------------------------------------------------------------------------- /include/vfspp/FilesystemCompat.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_FILESYSTEM_COMPAT_HPP 2 | #define VFSPP_FILESYSTEM_COMPAT_HPP 3 | 4 | #include "Global.h" 5 | 6 | #ifdef VFSPP_DISABLE_STD_FILESYSTEM 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace vfspp 17 | { 18 | namespace fs_compat 19 | { 20 | 21 | enum class perms : unsigned 22 | { 23 | none = static_cast(0), 24 | owner_write = static_cast(S_IWUSR) 25 | }; 26 | 27 | inline perms operator&(perms lhs, perms rhs) 28 | { 29 | return static_cast(static_cast(lhs) & static_cast(rhs)); 30 | } 31 | 32 | inline perms operator|(perms lhs, perms rhs) 33 | { 34 | return static_cast(static_cast(lhs) | static_cast(rhs)); 35 | } 36 | 37 | class path 38 | { 39 | public: 40 | path() = default; 41 | path(const std::string& p) 42 | : m_Path(NormalizeSeparators(p)) 43 | { 44 | } 45 | 46 | path(const char* p) 47 | : m_Path(NormalizeSeparators(p ? std::string(p) : std::string())) 48 | { 49 | } 50 | 51 | bool empty() const 52 | { 53 | return m_Path.empty(); 54 | } 55 | 56 | std::string generic_string() const 57 | { 58 | return m_Path; 59 | } 60 | 61 | std::string string() const 62 | { 63 | return m_Path; 64 | } 65 | 66 | const std::string& native() const 67 | { 68 | return m_Path; 69 | } 70 | 71 | path filename() const 72 | { 73 | return path(ExtractFilename(m_Path)); 74 | } 75 | 76 | bool has_extension() const 77 | { 78 | return !ExtractExtension(m_Path).empty(); 79 | } 80 | 81 | path extension() const 82 | { 83 | return path(ExtractExtension(m_Path)); 84 | } 85 | 86 | path stem() const 87 | { 88 | return path(ExtractStem(m_Path)); 89 | } 90 | 91 | path operator/(const path& other) const 92 | { 93 | if (other.m_Path.empty()) { 94 | return *this; 95 | } 96 | if (m_Path.empty()) { 97 | return other; 98 | } 99 | 100 | std::string combined = m_Path; 101 | if (combined.back() != '/') { 102 | combined.push_back('/'); 103 | } 104 | 105 | std::string tail = other.m_Path; 106 | if (!tail.empty() && tail.front() == '/') { 107 | tail.erase(tail.begin()); 108 | } 109 | 110 | combined += tail; 111 | return path(combined); 112 | } 113 | 114 | private: 115 | static std::string NormalizeSeparators(const std::string& in) 116 | { 117 | std::string out; 118 | out.reserve(in.size()); 119 | for (char c : in) { 120 | out.push_back(c == '\\' ? '/' : c); 121 | } 122 | return out; 123 | } 124 | 125 | static std::string ExtractFilename(const std::string& in) 126 | { 127 | if (in.empty()) { 128 | return {}; 129 | } 130 | 131 | const auto pos = in.find_last_of('/') ; 132 | if (pos == std::string::npos) { 133 | return in; 134 | } 135 | if (pos + 1u >= in.size()) { 136 | return {}; 137 | } 138 | return in.substr(pos + 1u); 139 | } 140 | 141 | static std::string ExtractExtension(const std::string& in) 142 | { 143 | const auto filename = ExtractFilename(in); 144 | const auto dotPos = filename.find_last_of('.'); 145 | if (dotPos == std::string::npos || dotPos == 0u) { 146 | return {}; 147 | } 148 | return filename.substr(dotPos); 149 | } 150 | 151 | static std::string ExtractStem(const std::string& in) 152 | { 153 | const auto filename = ExtractFilename(in); 154 | const auto dotPos = filename.find_last_of('.'); 155 | if (dotPos == std::string::npos || dotPos == 0u) { 156 | return filename; 157 | } 158 | return filename.substr(0u, dotPos); 159 | } 160 | 161 | private: 162 | std::string m_Path; 163 | }; 164 | 165 | struct file_status 166 | { 167 | perms permissions() const 168 | { 169 | return m_Perms; 170 | } 171 | 172 | bool is_directory() const 173 | { 174 | return m_IsDirectory; 175 | } 176 | 177 | bool is_regular_file() const 178 | { 179 | return m_IsRegular; 180 | } 181 | 182 | perms m_Perms = perms::none; 183 | bool m_IsDirectory = false; 184 | bool m_IsRegular = false; 185 | }; 186 | 187 | inline file_status status(const std::string& p) 188 | { 189 | struct stat st; 190 | if (::stat(p.c_str(), &st) != 0) { 191 | return {}; 192 | } 193 | 194 | file_status fs; 195 | fs.m_Perms = (st.st_mode & S_IWUSR) ? perms::owner_write : perms::none; 196 | fs.m_IsDirectory = S_ISDIR(st.st_mode); 197 | fs.m_IsRegular = S_ISREG(st.st_mode); 198 | return fs; 199 | } 200 | 201 | inline file_status status(const path& p) 202 | { 203 | return status(p.generic_string()); 204 | } 205 | 206 | // Forward declarations for path-based functions so string overloads can call them 207 | inline uintmax_t file_size(const path& p, std::error_code& ec); 208 | inline void rename(const path& from, const path& to, std::error_code& ec); 209 | inline bool remove(const path& p); 210 | 211 | enum class copy_options 212 | { 213 | skip_existing, 214 | overwrite_existing 215 | }; 216 | 217 | inline bool copy_file(const path& from, const path& to, copy_options option); 218 | 219 | inline uintmax_t file_size(const std::string& p, std::error_code& ec) 220 | { 221 | struct stat st; 222 | if (::stat(p.c_str(), &st) != 0) { 223 | ec = std::error_code(errno, std::generic_category()); 224 | return 0; 225 | } 226 | 227 | ec.clear(); 228 | return static_cast(st.st_size); 229 | } 230 | 231 | inline bool exists(const std::string& p) 232 | { 233 | struct stat st; 234 | return ::stat(p.c_str(), &st) == 0; 235 | } 236 | 237 | inline bool exists(const path& p) 238 | { 239 | return exists(p.generic_string()); 240 | } 241 | 242 | inline bool is_directory(const file_status& status) 243 | { 244 | return status.is_directory(); 245 | } 246 | 247 | inline bool is_directory(const std::string& p) 248 | { 249 | struct stat st; 250 | return (::stat(p.c_str(), &st) == 0) && S_ISDIR(st.st_mode); 251 | } 252 | 253 | inline bool is_directory(const path& p) 254 | { 255 | return is_directory(p.generic_string()); 256 | } 257 | 258 | inline bool is_regular_file(const std::string& p) 259 | { 260 | struct stat st; 261 | return (::stat(p.c_str(), &st) == 0) && S_ISREG(st.st_mode); 262 | } 263 | 264 | inline bool is_regular_file(const path& p) 265 | { 266 | return is_regular_file(p.generic_string()); 267 | } 268 | 269 | inline bool is_regular_file(const file_status& status) 270 | { 271 | return status.is_regular_file(); 272 | } 273 | 274 | inline uintmax_t file_size(const path& p, std::error_code& ec) 275 | { 276 | struct stat st; 277 | if (::stat(p.generic_string().c_str(), &st) != 0) { 278 | ec = std::error_code(errno, std::generic_category()); 279 | return 0; 280 | } 281 | 282 | ec.clear(); 283 | return static_cast(st.st_size); 284 | } 285 | inline bool copy_file(const path& from, const path& to, copy_options option) 286 | { 287 | if (!is_regular_file(from)) { 288 | return false; 289 | } 290 | 291 | const bool dstExists = exists(to); 292 | if (dstExists && option == copy_options::skip_existing) { 293 | return false; 294 | } 295 | 296 | std::ifstream src(from.generic_string(), std::ios::binary); 297 | std::ofstream dst(to.generic_string(), std::ios::binary | std::ios::trunc); 298 | if (!src.is_open() || !dst.is_open()) { 299 | return false; 300 | } 301 | 302 | dst << src.rdbuf(); 303 | return dst.good(); 304 | } 305 | 306 | inline bool copy_file(const std::string& from, const std::string& to, copy_options option) 307 | { 308 | return copy_file(path(from), path(to), option); 309 | } 310 | 311 | inline void rename(const path& from, const path& to, std::error_code& ec) 312 | { 313 | if (::rename(from.generic_string().c_str(), to.generic_string().c_str()) != 0) { 314 | ec = std::error_code(errno, std::generic_category()); 315 | return; 316 | } 317 | ec.clear(); 318 | } 319 | 320 | inline void rename(const std::string& from, const std::string& to, std::error_code& ec) 321 | { 322 | rename(path(from), path(to), ec); 323 | } 324 | 325 | inline bool remove(const path& p) 326 | { 327 | return ::remove(p.generic_string().c_str()) == 0; 328 | } 329 | 330 | inline bool remove(const std::string& p) 331 | { 332 | return remove(path(p)); 333 | } 334 | 335 | class directory_entry 336 | { 337 | public: 338 | directory_entry() = default; 339 | 340 | explicit directory_entry(const fs_compat::path& p) 341 | : m_Path(p) 342 | , m_Status(fs_compat::status(p)) 343 | { 344 | } 345 | 346 | const fs_compat::path& path() const 347 | { 348 | return m_Path; 349 | } 350 | 351 | file_status status() const 352 | { 353 | return m_Status; 354 | } 355 | 356 | private: 357 | fs_compat::path m_Path; 358 | file_status m_Status; 359 | }; 360 | 361 | class directory_iterator 362 | { 363 | public: 364 | directory_iterator() = default; 365 | 366 | explicit directory_iterator(const std::string& root) 367 | { 368 | LoadEntries(root); 369 | } 370 | 371 | explicit directory_iterator(const path& root) 372 | { 373 | LoadEntries(root.generic_string()); 374 | } 375 | 376 | directory_iterator begin() const 377 | { 378 | return *this; 379 | } 380 | 381 | directory_iterator end() const 382 | { 383 | directory_iterator it; 384 | it.m_Entries = m_Entries; 385 | it.m_Index = m_Entries ? m_Entries->size() : 0u; 386 | return it; 387 | } 388 | 389 | directory_iterator& operator++() 390 | { 391 | if (m_Index < Size()) { 392 | ++m_Index; 393 | } 394 | return *this; 395 | } 396 | 397 | const directory_entry& operator*() const 398 | { 399 | return (*m_Entries)[m_Index]; 400 | } 401 | 402 | const directory_entry* operator->() const 403 | { 404 | return &(*m_Entries)[m_Index]; 405 | } 406 | 407 | bool operator!=(const directory_iterator& other) const 408 | { 409 | return m_Entries != other.m_Entries || m_Index != other.m_Index; 410 | } 411 | 412 | private: 413 | void LoadEntries(const std::string& root) 414 | { 415 | m_Entries = std::make_shared>(); 416 | 417 | DIR* dir = ::opendir(root.c_str()); 418 | if (!dir) { 419 | return; 420 | } 421 | 422 | struct dirent* ent; 423 | while ((ent = ::readdir(dir)) != nullptr) { 424 | const std::string name = ent->d_name; 425 | if (name == "." || name == "..") { 426 | continue; 427 | } 428 | 429 | path entryPath = path(root) / path(name); 430 | m_Entries->emplace_back(directory_entry(entryPath)); 431 | } 432 | ::closedir(dir); 433 | } 434 | 435 | size_t Size() const 436 | { 437 | return m_Entries ? m_Entries->size() : 0u; 438 | } 439 | 440 | private: 441 | std::shared_ptr> m_Entries; 442 | size_t m_Index = 0u; 443 | }; 444 | 445 | } // namespace fs_compat 446 | } // namespace vfspp 447 | 448 | #endif // VFSPP_DISABLE_STD_FILESYSTEM 449 | 450 | #endif // VFSPP_FILESYSTEM_COMPAT_HPP 451 | -------------------------------------------------------------------------------- /include/vfspp/NativeFileSystem.hpp: -------------------------------------------------------------------------------- 1 | #ifndef VFSPP_NATIVEFILESYSTEM_HPP 2 | #define VFSPP_NATIVEFILESYSTEM_HPP 3 | 4 | #include "IFileSystem.h" 5 | #include "Global.h" 6 | #include "ThreadingPolicy.hpp" 7 | #include "NativeFile.hpp" 8 | 9 | #ifdef VFSPP_DISABLE_STD_FILESYSTEM 10 | #include "FilesystemCompat.hpp" 11 | namespace fs = vfspp::fs_compat; 12 | #else 13 | namespace fs = std::filesystem; 14 | #endif 15 | 16 | namespace vfspp 17 | { 18 | 19 | using NativeFileSystemPtr = std::shared_ptr; 20 | using NativeFileSystemWeakPtr = std::weak_ptr; 21 | 22 | class NativeFileSystem final : public IFileSystem 23 | { 24 | public: 25 | NativeFileSystem(const std::string& aliasPath, const std::string& basePath) 26 | : m_AliasPath(aliasPath) 27 | , m_BasePath(basePath) 28 | { 29 | } 30 | 31 | ~NativeFileSystem() 32 | { 33 | Shutdown(); 34 | } 35 | 36 | /* 37 | * Initialize filesystem, call this method as soon as possible 38 | */ 39 | [[nodiscard]] 40 | virtual bool Initialize() override 41 | { 42 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 43 | return InitializeImpl(); 44 | } 45 | 46 | /* 47 | * Shutdown filesystem 48 | */ 49 | virtual void Shutdown() override 50 | { 51 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 52 | ShutdownImpl(); 53 | } 54 | 55 | /* 56 | * Check if filesystem is initialized 57 | */ 58 | [[nodiscard]] 59 | virtual bool IsInitialized() const override 60 | { 61 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 62 | return IsInitializedImpl(); 63 | } 64 | 65 | /* 66 | * Get base path 67 | */ 68 | [[nodiscard]] 69 | virtual const std::string& BasePath() const override 70 | { 71 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 72 | return BasePathImpl(); 73 | } 74 | 75 | /* 76 | * Get mounted path 77 | */ 78 | [[nodiscard]] 79 | virtual const std::string& VirtualPath() const override 80 | { 81 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 82 | return AliasPathImpl(); 83 | } 84 | 85 | /* 86 | * Retrieve all files in filesystem. Heavy operation, avoid calling this often 87 | */ 88 | [[nodiscard]] 89 | virtual FilesList GetFilesList() const override 90 | { 91 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 92 | return GetFilesListImpl(); 93 | } 94 | 95 | /* 96 | * Check is readonly filesystem 97 | */ 98 | [[nodiscard]] 99 | virtual bool IsReadOnly() const override 100 | { 101 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 102 | return IsReadOnlyImpl(); 103 | } 104 | 105 | /* 106 | * Open existing file for reading, if not exists returns null for readonly filesystem. 107 | * If file not exists and filesystem is writable then create new file 108 | */ 109 | virtual IFilePtr OpenFile(const std::string& virtualPath, IFile::FileMode mode) override 110 | { 111 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 112 | return OpenFileImpl(virtualPath, mode); 113 | } 114 | 115 | /* 116 | * Close file 117 | */ 118 | virtual void CloseFile(IFilePtr file) override 119 | { 120 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 121 | CloseFileImpl(file); 122 | } 123 | 124 | /* 125 | * Create file on writeable filesystem. Returns true if file created successfully 126 | */ 127 | virtual IFilePtr CreateFile(const std::string& virtualPath) override 128 | { 129 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 130 | return OpenFileImpl(virtualPath, IFile::FileMode::ReadWrite | IFile::FileMode::Truncate); 131 | } 132 | 133 | /* 134 | * Remove existing file on writable filesystem 135 | */ 136 | virtual bool RemoveFile(const std::string& virtualPath) override 137 | { 138 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 139 | return RemoveFileImpl(virtualPath); 140 | } 141 | 142 | /* 143 | * Copy existing file on writable filesystem 144 | */ 145 | virtual bool CopyFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) override 146 | { 147 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 148 | return CopyFileImpl(srcVirtualPath, dstVirtualPath, overwrite); 149 | } 150 | 151 | /* 152 | * Rename existing file on writable filesystem 153 | */ 154 | virtual bool RenameFile(const std::string& srcVirtualPath, const std::string& dstVirtualPath) override 155 | { 156 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 157 | return RenameFileImpl(srcVirtualPath, dstVirtualPath); 158 | } 159 | 160 | /* 161 | * Check if file exists on filesystem 162 | */ 163 | [[nodiscard]] 164 | virtual bool IsFileExists(const std::string& virtualPath) const override 165 | { 166 | [[maybe_unused]] auto lock = ThreadingPolicy::Lock(m_Mutex); 167 | return IsFileExistsImpl(virtualPath); 168 | } 169 | 170 | private: 171 | struct FileEntry 172 | { 173 | FileInfo Info; 174 | std::vector OpenedHandles; 175 | 176 | explicit FileEntry(const FileInfo& info) 177 | : Info(info) 178 | { 179 | } 180 | 181 | void CleanupOpenedHandles(IFilePtr fileToExclude = nullptr) 182 | { 183 | OpenedHandles.erase(std::remove_if(OpenedHandles.begin(), OpenedHandles.end(), [&](const NativeFileWeakPtr& weak) { 184 | return weak.expired() || weak.lock() == fileToExclude; 185 | }), OpenedHandles.end()); 186 | } 187 | }; 188 | 189 | inline bool InitializeImpl() 190 | { 191 | if (m_IsInitialized) { 192 | return true; 193 | } 194 | 195 | if (!fs::is_directory(BasePathImpl())) { 196 | printf("NativeFileSystem: Base path is not a directory: %s\n", BasePathImpl().c_str()); 197 | return false; 198 | } 199 | 200 | if (!fs::exists(BasePathImpl())) { // TODO: create if not exists flag 201 | printf("NativeFileSystem: Base path does not exist: %s\n", BasePathImpl().c_str()); 202 | return false; 203 | } 204 | 205 | BuildFilelist(AliasPathImpl(), BasePathImpl(), m_Files); 206 | m_IsInitialized = true; 207 | return true; 208 | } 209 | 210 | inline void ShutdownImpl() 211 | { 212 | m_BasePath = ""; 213 | m_AliasPath = ""; 214 | m_Files.clear(); 215 | 216 | m_IsInitialized = false; 217 | } 218 | 219 | inline bool IsInitializedImpl() const 220 | { 221 | return m_IsInitialized; 222 | } 223 | 224 | const std::string& BasePathImpl() const 225 | { 226 | return m_BasePath; 227 | } 228 | 229 | const std::string& AliasPathImpl() const 230 | { 231 | return m_AliasPath; 232 | } 233 | 234 | inline FilesList GetFilesListImpl() const 235 | { 236 | FilesList list; 237 | list.reserve(m_Files.size()); 238 | for (const auto& [path, entry] : m_Files) { 239 | list.push_back(entry.Info); 240 | } 241 | return list; 242 | } 243 | 244 | inline bool IsReadOnlyImpl() const 245 | { 246 | if (!IsInitializedImpl()) { 247 | return true; 248 | } 249 | 250 | auto perms = fs::status(BasePathImpl()).permissions(); 251 | return (perms & fs::perms::owner_write) == fs::perms::none; 252 | } 253 | 254 | inline IFilePtr OpenFileImpl(const std::string& virtualPath, IFile::FileMode mode) 255 | { 256 | const bool requestWrite = IFile::ModeHasFlag(mode, IFile::FileMode::Write); 257 | if (IsReadOnlyImpl() && requestWrite) { 258 | return nullptr; 259 | } 260 | 261 | const auto it = m_Files.find(virtualPath); 262 | if (it == m_Files.end() && requestWrite) { 263 | // Create new file entry if not exists in writable mode 264 | FileInfo fileInfo(AliasPathImpl(), BasePathImpl(), virtualPath); 265 | m_Files.emplace(fileInfo.VirtualPath(), fileInfo); 266 | } 267 | 268 | const auto entryIt = m_Files.find(virtualPath); 269 | if (entryIt == m_Files.end()) { 270 | return nullptr; 271 | } 272 | auto& entry = entryIt->second; 273 | 274 | NativeFilePtr file = std::make_shared(entry.Info); 275 | if (!file || !file->Open(mode)) { 276 | return nullptr; 277 | } 278 | 279 | entry.OpenedHandles.push_back(file); 280 | 281 | return file; 282 | } 283 | 284 | inline void CloseFileImpl(IFilePtr file) 285 | { 286 | CloseFileAndCleanupOpenedHandles(file); 287 | } 288 | 289 | inline bool RemoveFileImpl(const std::string& virtualPath) 290 | { 291 | if (IsReadOnlyImpl()) { 292 | return false; 293 | } 294 | 295 | auto it = m_Files.find(virtualPath); 296 | if (it == m_Files.end()) { 297 | return false; 298 | } 299 | 300 | CloseFileAndCleanupOpenedHandles(); 301 | 302 | m_Files.erase(it); 303 | 304 | return fs::remove(it->second.Info.NativePath()); 305 | } 306 | 307 | inline bool CopyFileImpl(const std::string& srcVirtualPath, const std::string& dstVirtualPath, bool overwrite = false) 308 | { 309 | if (IsReadOnlyImpl()) { 310 | return false; 311 | } 312 | 313 | // Check is src and dst start with alias path 314 | if (srcVirtualPath.find(AliasPathImpl()) != 0 || dstVirtualPath.find(AliasPathImpl()) != 0) { 315 | return false; 316 | } 317 | 318 | // Check if src file exists 319 | const auto srcIt = m_Files.find(srcVirtualPath); 320 | if (srcIt == m_Files.end()) { 321 | return false; 322 | } 323 | const auto& srcPath = srcIt->second.Info; 324 | 325 | // Check if dst file exists 326 | const auto dstIt = m_Files.find(dstVirtualPath); 327 | if (dstIt != m_Files.end() && !overwrite) { 328 | return false; 329 | } 330 | const auto& dstPath = FileInfo(AliasPathImpl(), BasePathImpl(), dstVirtualPath); 331 | 332 | // Perform copy 333 | auto option = overwrite ? fs::copy_options::overwrite_existing : fs::copy_options::skip_existing; 334 | const bool ok = fs::copy_file(srcPath.NativePath(), dstPath.NativePath(), option); 335 | if (!ok) { 336 | return false; 337 | } 338 | 339 | // Remove existing dest entry if any 340 | const auto destIt = m_Files.find(dstPath.VirtualPath()); 341 | if (destIt != m_Files.end()) { 342 | m_Files.erase(destIt); 343 | } 344 | 345 | // Add new entry 346 | m_Files.insert({dstVirtualPath, FileEntry(dstPath)}); 347 | return true; 348 | } 349 | 350 | bool RenameFileImpl(const std::string& srcVirtualPath, const std::string& dstVirtualPath) 351 | { 352 | if (IsReadOnlyImpl()) { 353 | return false; 354 | } 355 | 356 | // Check is src and dst start with alias path 357 | if (srcVirtualPath.find(AliasPathImpl()) != 0 || dstVirtualPath.find(AliasPathImpl()) != 0) { 358 | return false; 359 | } 360 | 361 | // Check if src file exists 362 | const auto srcIt = m_Files.find(srcVirtualPath); 363 | if (srcIt == m_Files.end()) { 364 | return false; 365 | } 366 | const auto& srcPath = srcIt->second.Info; 367 | 368 | // Check if dst file exists 369 | const auto dstIt = m_Files.find(dstVirtualPath); 370 | if (dstIt != m_Files.end()) { 371 | return false; 372 | } 373 | const auto& dstPath = FileInfo(AliasPathImpl(), BasePathImpl(), dstVirtualPath); 374 | 375 | // Perform rename 376 | std::error_code ec; 377 | fs::rename(srcPath.NativePath(), dstPath.NativePath(), ec); 378 | if (ec) { 379 | return false; 380 | } 381 | 382 | // Remove existing src entry 383 | if (srcIt != m_Files.end()) { 384 | m_Files.erase(srcIt); 385 | } 386 | 387 | // Remove existing dest entry if any 388 | if (dstIt != m_Files.end()) { 389 | m_Files.erase(dstIt); 390 | } 391 | 392 | // Add new entry 393 | m_Files.insert({dstVirtualPath, FileEntry(dstPath)}); 394 | return true; 395 | } 396 | 397 | inline bool IsFileExistsImpl(const std::string& virtualPath) const 398 | { 399 | const auto fileIt = m_Files.find(virtualPath); 400 | return fileIt != m_Files.end() && fs::exists(fileIt->second.Info.NativePath()); 401 | } 402 | 403 | void BuildFilelist(const std::string& aliasPath, const std::string& basePath, std::unordered_map& outFiles) 404 | { 405 | for (const auto& entry : fs::directory_iterator(basePath)) { 406 | if (fs::is_directory(entry.status())) { 407 | BuildFilelist(aliasPath, entry.path().string(), outFiles); 408 | continue; 409 | } 410 | 411 | FileInfo fileInfo(aliasPath, BasePathImpl(), entry.path().string()); 412 | outFiles.emplace(fileInfo.VirtualPath(), fileInfo); 413 | } 414 | } 415 | 416 | inline void CloseFileAndCleanupOpenedHandles(IFilePtr fileToClose = nullptr) 417 | { 418 | if (fileToClose) { 419 | const auto virtualPath = fileToClose->GetFileInfo().VirtualPath(); 420 | 421 | const auto it = m_Files.find(virtualPath); 422 | if (it == m_Files.end()) { 423 | return; 424 | } 425 | 426 | fileToClose->Close(); 427 | } 428 | 429 | for (auto& [path, entry] : m_Files) { 430 | entry.CleanupOpenedHandles(fileToClose); 431 | } 432 | } 433 | 434 | private: 435 | std::string m_AliasPath; 436 | std::string m_BasePath; 437 | bool m_IsInitialized = false; 438 | mutable std::mutex m_Mutex; 439 | 440 | std::unordered_map m_Files; 441 | }; 442 | 443 | } // namespace vfspp 444 | 445 | #endif // VFSPP_NATIVEFILESYSTEM_HPP 446 | --------------------------------------------------------------------------------