From 704e7d02a27651d8a610ab2a342310895d8566fa Mon Sep 17 00:00:00 2001 From: Talv Date: Wed, 31 Jul 2019 13:39:23 +0200 Subject: [PATCH 1/2] initial support for FUSE --- CMakeLists.txt | 20 +- CMakeModules/FindFUSE.cmake | 35 +++ include/cascfuse.hpp | 5 + include/common.hpp | 2 +- include/storage.hpp | 4 +- include/util.hpp | 5 +- src/cascfuse.cc | 426 ++++++++++++++++++++++++++++++++++++ src/storage.cc | 6 + src/stormex.cc | 25 ++- src/util.cc | 20 +- 10 files changed, 536 insertions(+), 12 deletions(-) create mode 100644 CMakeModules/FindFUSE.cmake create mode 100644 include/cascfuse.hpp create mode 100644 src/cascfuse.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f2a71a..ba8bbc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 2.6) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${STORMEXTRACT_BINARY_DIR}/bin") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${STORMEXTRACT_BINARY_DIR}/lib") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${STORMEXTRACT_BINARY_DIR}/bin") +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) set(default_build_type "RelWithDebInfo") if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) @@ -15,10 +16,16 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() +# compile flags set(CMAKE_CXX_FLAGS "-std=c++11") if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() +set(FUSE_REQUIRED_FLAGS "-D_FILE_OFFSET_BITS=64 -DFUSE_USE_VERSION=27") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${FUSE_REQUIRED_FLAGS}") + +# modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/CMakeModules") if (NOT EXISTS "${STORMEXTRACT_SOURCE_DIR}/CascLib/CMakeLists.txt") message(FATAL_ERROR @@ -38,6 +45,14 @@ endif() add_subdirectory(CascLib) add_subdirectory(plog) +# FUSE +find_package(FUSE REQUIRED) +if(NOT FUSE_FOUND) + message(FATAL_ERROR "Failed to find fuse") +else() + include_directories(${FUSE_INCLUDE_DIR}) +endif() + include_directories("${STORMEXTRACT_SOURCE_DIR}/include/" "${STORMEXTRACT_SOURCE_DIR}/CascLib/src/" "${STORMEXTRACT_SOURCE_DIR}/cxxopts/include/" @@ -47,12 +62,15 @@ include_directories("${STORMEXTRACT_SOURCE_DIR}/include/" set(SRC_FILES src/util.cc src/storage.cc + src/cascfuse.cc src/stormex.cc ) add_executable(stormex ${SRC_FILES}) -target_link_libraries(stormex plog) +target_link_libraries(stormex + ${FUSE_LIBRARIES} +) if(WIN32) target_link_libraries(stormex casc_static) diff --git a/CMakeModules/FindFUSE.cmake b/CMakeModules/FindFUSE.cmake new file mode 100644 index 0000000..2200bf5 --- /dev/null +++ b/CMakeModules/FindFUSE.cmake @@ -0,0 +1,35 @@ +# Find the FUSE includes and library +# +# FUSE_INCLUDE_DIR - where to find fuse.h, etc. +# FUSE_LIBRARIES - List of libraries when using FUSE. +# FUSE_FOUND - True if FUSE lib is found. + +# check if already in cache, be silent +IF (FUSE_INCLUDE_DIR) + SET (FUSE_FIND_QUIETLY TRUE) +ENDIF (FUSE_INCLUDE_DIR) + +# find includes +FIND_PATH (FUSE_INCLUDE_DIR fuse.h + /usr/local/include/osxfuse + /usr/local/include + /usr/include +) + +# find lib +if (APPLE) + SET(FUSE_NAMES libosxfuse.dylib fuse) +else (APPLE) + SET(FUSE_NAMES fuse) +endif (APPLE) +FIND_LIBRARY(FUSE_LIBRARIES + NAMES ${FUSE_NAMES} + PATHS /lib64 /lib /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib +) + +include (FindPackageHandleStandardArgs) +find_package_handle_standard_args ("FUSE" DEFAULT_MSG + FUSE_INCLUDE_DIR FUSE_LIBRARIES) + +mark_as_advanced (FUSE_INCLUDE_DIR FUSE_LIBRARIES) + diff --git a/include/cascfuse.hpp b/include/cascfuse.hpp new file mode 100644 index 0000000..ecf5d19 --- /dev/null +++ b/include/cascfuse.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "storage.hpp" + +int cascf_mount(const std::string& mountPoint, HANDLE hStorage); diff --git a/include/common.hpp b/include/common.hpp index 6bcf0a0..ccf410a 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -6,7 +6,7 @@ #include "mplog/ColorConsoleAppenderStderr.h" // #include "plog/Formatters/FuncMessageFormatter.h" -static const std::string stormexVersion = "2.0.1"; +static const std::string stormexVersion = "2.1.0"; static plog::ColorConsoleAppenderStdErr consoleAppender; // static plog::ColorConsoleAppender consoleAppender; diff --git a/include/storage.hpp b/include/storage.hpp index 4f47e2b..c4048d2 100644 --- a/include/storage.hpp +++ b/include/storage.hpp @@ -56,7 +56,9 @@ class StorageExplorer { HANDLE m_hStorage = nullptr; public: - const HANDLE& getHandle() { return m_hStorage; } + HANDLE getHandle() { return m_hStorage; } + + ~StorageExplorer(); /** * @brief Open CASC diff --git a/include/util.hpp b/include/util.hpp index 380e849..9c2b23b 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -21,6 +21,9 @@ int ensureDirExists(std::string strDestName); std::string formatFileSize(size_t size); /// Try to find in the Haystack the Needle - ignore case -bool findStringIC(const std::string& strHaystack, const std::string& strNeedle); +bool stringFindIC(const std::string& strHaystack, const std::string& strNeedle); +bool stringEqualIC(const std::string& str1, const std::string& str2); +void stringToLower(std::string& str); +std::string stringToLowerCopy(std::string str); #endif // __UTIL_HPP__ diff --git a/src/cascfuse.cc b/src/cascfuse.cc new file mode 100644 index 0000000..94deb76 --- /dev/null +++ b/src/cascfuse.cc @@ -0,0 +1,426 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define __CASCLIB_SELF__ +#include "../CascLib/src/CascLib.h" +#include "../CascLib/src/CascCommon.h" +#include "common.hpp" +#include "util.hpp" + +enum FsNodeKind { + Unknown, + Root, + Folder, + File, +}; + +class StringIHasher +{ +public: + size_t operator()(const std::string& k) const + { + std::string str = k; + std::transform(str.begin(), str.end(), str.begin(), [](const char& ch) { + switch (ch) { + case '\\': return '/'; + default: return static_cast(::tolower(ch)); + } + }); + return CalcNormNameHash(str.c_str(), str.size()); + } +}; + +class StringIComparator +{ +public: + bool operator()(const std::string& str1, const std::string& str2) const + { + return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](const char& ch1, const char& ch2) { + if ((ch1 == '/' || ch1 == '\\') && (ch2 == '/' || ch2 == '\\')) { + return true; + } + else { + return std::tolower(ch1) == std::tolower(ch2); + } + }); + } +}; + +class FsNode { + std::string m_name; + std::string m_filename; + FsNode* m_parent = NULL; + std::unordered_map m_children; +public: + const FsNodeKind m_kind = FsNodeKind::Unknown; + PCASC_CKEY_ENTRY ckeyEntry = NULL; + + FsNode(FsNodeKind nKind, std::string name, FsNode *parent) + : m_kind(nKind), m_name(name), m_parent(parent) + { + } + + FsNode(FsNodeKind nKind = FsNodeKind::Root) + : m_kind(nKind), m_name("/") + { + } + + void Insert(FsNode* childNode) + { + assert(m_kind != FsNodeKind::File); + m_children[stringToLowerCopy(childNode->Name())] = childNode; + } + + FsNode *Insert(FsNodeKind nKind, const std::string& name) + { + auto childNode = new FsNode(nKind, name, this); + Insert(childNode); + return childNode; + } + + const std::unordered_map& Children() + { + return m_children; + } + + const std::string& Name() + { + return m_name; + } + + FsNode* Parent() + { + return m_parent; + } + + void SetParent(FsNode* newParent) + { + m_parent = newParent; + } + + const std::string Filepath() + { + if (m_filename.length()) return m_filename; + + std::vector parentNodes; + auto currParent = this; + do { + parentNodes.push_back(currParent); + currParent = currParent->Parent(); + } while (currParent != NULL && (currParent->m_kind == FsNodeKind::Folder || currParent->m_kind == FsNodeKind::File)); + + std::ostringstream osPath; + for (auto it = parentNodes.rbegin(); it != parentNodes.rend(); it++) { + if ((*it)->m_kind == FsNodeKind::Root) { + osPath << (*it)->m_name; + } + else { + osPath << '/' << (*it)->m_name; + } + } + m_filename = osPath.str(); + + return m_filename; + } +}; + +class FsTree { + const size_t m_openFileLimit = 128; + FsNode m_rootNode; + std::unordered_map m_nodeMap; + std::map m_openFiles; + +public: + FsNode* GetRootNode() { return &m_rootNode; } + std::unordered_map& GetNodeMap() { return m_nodeMap; } + HANDLE m_hStorage = NULL; + + FsTree() + : m_rootNode(FsNodeKind::Root) + { + } + + void GenerateNodeHashMap(FsNode* fNode) + { + if (fNode->m_kind != FsNodeKind::Unknown) { + m_nodeMap[fNode->Filepath()] = fNode; + } + + for (auto childNode : fNode->Children()) { + GenerateNodeHashMap(childNode.second); + } + } + + FsNode* GetNodeAtPath(const char* path) + { + auto fNode = m_nodeMap.find(path); + if (fNode != m_nodeMap.end()) { + return fNode->second; + } + return NULL; + } + + FsNode* GetParentNodeOfFilename(const std::string& filename) + { + size_t pos_start = 0; + size_t pos_end = -1; + auto currentNode = GetRootNode(); + + while ((pos_end = filename.find('\\', pos_start)) != std::string::npos) { + std::string dirname = filename.substr(pos_start, pos_end - pos_start); + pos_start = pos_end + 1; + auto folderNodeEntry = currentNode->Children().find(dirname); + if (folderNodeEntry != currentNode->Children().end()) { + currentNode = folderNodeEntry->second; + } + else { + currentNode = currentNode->Insert(FsNodeKind::Folder, dirname); + } + } + + return currentNode; + } + + HANDLE GetNodeHandle(FsNode* fNode) + { + auto result = m_openFiles.find(fNode->Filepath()); + if (result != m_openFiles.end()) { + return result->second; + } + else { + if (m_openFiles.size() >= m_openFileLimit) { + LOG_DEBUG << "Open files limit reached (" << m_openFileLimit << "). Closing first half.."; + for (auto it = m_openFiles.cbegin(); it != m_openFiles.end();) { + if (m_openFiles.size() > m_openFileLimit / 2) { + LOG_VERBOSE << "Closing: " << it->first; + CascCloseFile(it->second); + it = m_openFiles.erase(it); + } + else { + // ++it; + break; + } + } + } + + HANDLE hFile; + if (!CascOpenFile(m_hStorage, fNode->ckeyEntry->CKey, CASC_LOCALE_ALL, CASC_OPEN_BY_CKEY, &hFile)) { + LOG_ERROR << "Couldn't open file " << fNode->Filepath(); + return NULL; + } + m_openFiles[fNode->Filepath()] = hFile; + + return hFile; + } + } +}; + +FsTree cfFileTree; + +static int cascf_getattr(const char *path, struct stat *stbuf) +{ + LOG_VERBOSE << path; + int res = 0; + memset(stbuf, 0, sizeof(struct stat)); + + auto fNode = cfFileTree.GetNodeAtPath(path); + if (fNode != NULL) { + switch (fNode->m_kind) { + case FsNodeKind::Root: + case FsNodeKind::Folder: + { + stbuf->st_mode = S_IFDIR | 0554; + stbuf->st_nlink = 2; + stbuf->st_size = 0; + break; + } + + case FsNodeKind::File: + { + stbuf->st_mode = S_IFREG | 0554; + stbuf->st_nlink = 1; + + stbuf->st_size = fNode->ckeyEntry->ContentSize; + break; + } + + default: + { + res = -ENOENT; + break; + } + } + } + else { + res = -ENOENT; + } + + return res; +} + +static int cascf_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) +{ + LOG_VERBOSE << path; + + auto fNode = cfFileTree.GetNodeAtPath(path); + if (fNode == NULL || fNode->m_kind == FsNodeKind::File) { + return -ENOENT; + } + + filler(buf, ".", NULL, 0); + filler(buf, "..", NULL, 0); + + for (auto childNode : fNode->Children()) { + filler(buf, childNode.second->Name().c_str(), NULL, 0); + } + + return 0; +} + +static int cascf_open(const char *path, struct fuse_file_info *fi) +{ + LOG_VERBOSE << path; + + auto fNode = cfFileTree.GetNodeAtPath(path); + if (fNode == NULL || fNode->m_kind != FsNodeKind::File) { + return -ENOENT; + } + + if((fi->flags & 3) != O_RDONLY) + return -EACCES; + + return 0; +} + +static int cascf_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) +{ + LOG_VERBOSE << path << " at " << offset << " size " << size; + + auto fNode = cfFileTree.GetNodeAtPath(path); + if (fNode == NULL) { + return -ENOENT; + } + + switch (fNode->m_kind) { + case FsNodeKind::File: + { + DWORD readLen; + auto fHandle = cfFileTree.GetNodeHandle(fNode); + if (fHandle == NULL) { + LOG_ERROR << "Failed to open " << fNode->Filepath() << " E" << GetLastError(); + return 0; + } + CascSetFilePointer(fHandle, offset, NULL, FILE_BEGIN); + if (!CascReadFile(cfFileTree.GetNodeHandle(fNode), buf, size, &readLen)) { + LOG_ERROR << "Failed to read " << fNode->Filepath() << " E" << GetLastError(); + return 0; + } + return readLen; + } + + default: + { + return 0; + } + } +} + +static struct fuse_operations cascf_oper = { + .getattr = cascf_getattr, + .open = cascf_open, + .read = cascf_read, + .readdir = cascf_readdir, +}; + +struct TFileTreeRootPub : public TFileTreeRoot +{ +public: + CASC_FILE_TREE FileTree; +}; + +void cascf_populate(HANDLE hStorage) +{ + auto hs = IsValidCascStorageHandle(hStorage); + cfFileTree.m_hStorage = hStorage; + + PLOG_INFO << "Building file tree.."; + + CASC_FIND_DATA findData; + HANDLE handle = CascFindFirstFile(hStorage, "*", &findData, NULL); + + if (handle == INVALID_HANDLE_VALUE) { + PLOG_FATAL << "CascFindFirstFile E(" << GetLastError() << ")"; + exit(GetLastError()); + } + + do { + if (!findData.bFileAvailable) continue; + + FsNode *folderNode; + + if (findData.bCanOpenByName) { + folderNode = cfFileTree.GetParentNodeOfFilename(findData.szFileName); + } + else if (findData.bCanOpenByCKey) { + std::string targetFilepath = "CKEY\\"; + targetFilepath += findData.szFileName; + folderNode = cfFileTree.GetParentNodeOfFilename(targetFilepath); + } + else { + LOG_WARNING << "findData.bCanOpenByCKey is false for " << findData.szFileName; + continue; + } + + auto fileNode = folderNode->Insert(FsNodeKind::File, findData.szPlainName); + fileNode->ckeyEntry = FindCKeyEntry_CKey(hs, findData.CKey); + } while (CascFindNextFile(handle, &findData)); + CascFindClose(handle); + + LOG_DEBUG << "Generating indexes.."; + cfFileTree.GenerateNodeHashMap(cfFileTree.GetRootNode()); +} + +int cascf_mount(const std::string& mountPoint, HANDLE hStorage) +{ + cascf_populate(hStorage); + + LOG_DEBUG << "Preparing to mount.."; + if (!pathExists(mountPoint)) { + LOG_FATAL << "Path doesn't exist or is not accessible" << mountPoint; + return -2; + } + + auto fChan = fuse_mount(mountPoint.c_str(), NULL); + if (fChan != NULL) { + auto fHandle = fuse_new(fChan, NULL, &cascf_oper, sizeof(cascf_oper), NULL); + if (fHandle != NULL) { + struct fuse_session *se = fuse_get_session(fHandle); + if (fuse_set_signal_handlers(se) == 0) { + LOG_DEBUG << "Entering CASC-FS loop.."; + fuse_loop(fHandle); + LOG_DEBUG << "Leaving CASC-FS loop.."; + + fuse_remove_signal_handlers(se); + } + + fuse_unmount(mountPoint.c_str(), fChan); + fuse_destroy(fHandle); + } + } + else { + LOG_FATAL << "Couldn't mount to " << mountPoint; + return -2; + } + + return 0; +} diff --git a/src/storage.cc b/src/storage.cc index 7d8336f..941a88f 100644 --- a/src/storage.cc +++ b/src/storage.cc @@ -1,5 +1,11 @@ #include "storage.hpp" +StorageExplorer::~StorageExplorer() +{ + PLOG_DEBUG << "Closing storage.."; + closeStorage(); +} + int StorageExplorer::openStorage(std::string src) { // Remove trailing slash at the end of path (CascLib doesn't like that, supposedly) diff --git a/src/stormex.cc b/src/stormex.cc index ac87cb9..06d0dec 100644 --- a/src/stormex.cc +++ b/src/stormex.cc @@ -11,6 +11,7 @@ #include "common.hpp" #include "util.hpp" #include "storage.hpp" +#include "cascfuse.hpp" #include "common/Common.h" class StormexContext { @@ -41,6 +42,10 @@ class StormexContext { bool dryRun; } m_extract; + struct { + std::string mountPoint; + } m_mount; + void scanExtraArgs(cxxopts::ParseResult pResult) { if (pResult.count("in-regex")) { @@ -129,12 +134,16 @@ void parseArguments(int argc, char* argv[]) ("P,progress", "Notify about progress during extraction.", cxxopts::value(appCtx.m_extract.progress)) ("n,dry-run", "Simulate extraction process without writing any data to the filesystem.", cxxopts::value(appCtx.m_extract.dryRun)); + options.add_options("Mount") + ("m,mount", + "Mount CASC as a filesystem", cxxopts::value(appCtx.m_mount.mountPoint), "[MOUNTPOINT]"); + options.parse_positional({"storage"}); auto result = options.parse(argc, argv); if (result.count("help")) { - std::cerr << options.help({ "Common", "Base", "List", "Filter", "Extract" }) << std::endl; + std::cerr << options.help({ "Common", "Base", "List", "Filter", "Extract", "Mount" }) << std::endl; exit(0); } @@ -146,11 +155,12 @@ void parseArguments(int argc, char* argv[]) // logging verbosity plog::Severity level = plog::Severity::warning; if (result.count("verbose")) { - if (result.count("verbose") >= plog::Severity::debug) { + auto vLvl = plog::Severity::verbose - plog::Severity::warning; + if (result.count("verbose") > vLvl) { std::cerr << "Maximum level of logging verbosity has been reached [" << plog::Severity::debug << "]" << std::endl; exit(1); } - level = static_cast(plog::Severity::fatal + result.count("verbose")); + level = static_cast(plog::Severity::warning + result.count("verbose")); } else if (result.count("quiet")) { plog::Severity level = plog::Severity::none; @@ -237,7 +247,7 @@ std::vector filterFiles(const std::vectorfilename, needle); + c = stringFindIC(entry->filename, needle); if (c) break; } if (!c) continue; @@ -299,6 +309,10 @@ int main(int argc, char* argv[]) PLOG_INFO << "Storage opened " << static_cast(stExplorer.getHandle()); try { + if (appCtx.m_mount.mountPoint.length()) { + return cascf_mount(appCtx.m_mount.mountPoint, stExplorer.getHandle()); + } + auto fResults = enumerateFiles(stExplorer); if (appCtx.m_list.listFiles) { @@ -335,8 +349,5 @@ int main(int argc, char* argv[]) throw; } - PLOG_DEBUG << "Closing storage.."; - stExplorer.closeStorage(); - return 0; } diff --git a/src/util.cc b/src/util.cc index 52d8eca..c56fe5c 100644 --- a/src/util.cc +++ b/src/util.cc @@ -84,7 +84,7 @@ std::string formatFileSize(size_t size) return result; } -bool findStringIC(const std::string& strHaystack, const std::string& strNeedle) +bool stringFindIC(const std::string& strHaystack, const std::string& strNeedle) { auto it = std::search( strHaystack.begin(), strHaystack.end(), @@ -94,3 +94,21 @@ bool findStringIC(const std::string& strHaystack, const std::string& strNeedle) }); return (it != strHaystack.end()); } + +bool stringEqualIC(const std::string& str1, const std::string& str2) +{ + return str1.size() == str2.size() && std::equal(str1.begin(), str1.end(), str2.begin(), [](const char& ch1, const char& ch2) { + return std::toupper(ch1) == std::toupper(ch2); + }); +} + +void stringToLower(std::string& str) +{ + std::transform(str.begin(), str.end(), str.begin(), ::tolower); +} + +std::string stringToLowerCopy(std::string str) +{ + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + return str; +} From 0dc9961e0287cba0a15bd06ff5c1d0ff329f8253 Mon Sep 17 00:00:00 2001 From: Talv Date: Wed, 31 Jul 2019 15:01:24 +0200 Subject: [PATCH 2/2] add FUSE support for Windows via Dokany --- CHANGELOG.md | 7 +++++ CMakeModules/FindFUSE.cmake | 24 ++++++++++------- README.md | 41 ++++++++++++++++++++++++++-- include/cascfuse.hpp | 2 +- src/cascfuse.cc | 53 +++++++++++++++++++++---------------- src/stormex.cc | 4 +-- 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04449e..75aa457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [2.1.0] - 2019-07-31 + +* Added basic support for mounting CASC as filesystem visible to the OS using [FUSE](https://github.com/libfuse/libfuse). + * Linux will work out of the box. + * MacOS is likely to work but hasn't been tested. + * Under Windows app is compiled against `dokanfuse` wrapper library. In order for it to work `Dokany` must be installed on the system. + ## [2.0.0] - 2019-07-26 * Refactored entire codebase.. diff --git a/CMakeModules/FindFUSE.cmake b/CMakeModules/FindFUSE.cmake index 2200bf5..2343e63 100644 --- a/CMakeModules/FindFUSE.cmake +++ b/CMakeModules/FindFUSE.cmake @@ -6,25 +6,31 @@ # check if already in cache, be silent IF (FUSE_INCLUDE_DIR) - SET (FUSE_FIND_QUIETLY TRUE) + SET (FUSE_FIND_QUIETLY TRUE) ENDIF (FUSE_INCLUDE_DIR) +SET(DOKANY_DIR "C:\\Program Files\\Dokan\\Dokan Library-1.3.0") + # find includes FIND_PATH (FUSE_INCLUDE_DIR fuse.h - /usr/local/include/osxfuse - /usr/local/include - /usr/include + /usr/local/include/osxfuse + /usr/local/include + /usr/include + "${DOKANY_DIR}\\include" ) # find lib -if (APPLE) +if(APPLE) SET(FUSE_NAMES libosxfuse.dylib fuse) -else (APPLE) +elseif (WIN32) + SET(FUSE_NAMES dokanfuse1) +else() SET(FUSE_NAMES fuse) -endif (APPLE) +endif(APPLE) + FIND_LIBRARY(FUSE_LIBRARIES - NAMES ${FUSE_NAMES} - PATHS /lib64 /lib /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib + NAMES ${FUSE_NAMES} + PATHS /lib64 /lib /usr/lib64 /usr/lib /usr/local/lib64 /usr/local/lib "${DOKANY_DIR}\\lib" ) include (FindPackageHandleStandardArgs) diff --git a/README.md b/README.md index d148173..a3ea40a 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,15 @@ Usage: -x, --extract-all Extract all files matching search filters. -X, --extract-file [FILE...] Extract file(s) matching exactly. -o, --outdir [PATH] Output directory for extracted files. - (default: ./) + (default: .) -p, --stdout Pipe content of a file(s) to stdout instead writing it to the filesystem. -P, --progress Notify about progress during extraction. -n, --dry-run Simulate extraction process without writing any data to the filesystem. + + Mount options: + -m, --mount [MOUNTPOINT] Mount CASC as a filesystem ``` ### Examples @@ -103,7 +106,7 @@ stormex '/mnt/s1/BnetGameLib/StarCraft II' -ld | sort -h ```sh stormex '/mnt/s1/BnetGameLib/StarCraft II' \ - -I '\/(DocumentInfo|Objects|Regions|Triggers)$' \ + -I '\\(DocumentInfo|Objects|Regions|Triggers)$' \ -I '\.(fx|xml|txt|json|galaxy|SC2Style|SC2Hotkeys|SC2Lib|TriggerLib|SC2Interface|SC2Locale|SC2Components|SC2Layout)$' \ -E '(dede|eses|esmx|frfr|itit|kokr|plpl|ptbr|ruru|zhcn|zhtw)\.sc2data' \ -E '(PreloadAssetDB|TextureReductionValues)\.txt$' \ @@ -122,6 +125,40 @@ stormex -S '/mnt/s1/BnetGameLib/StarCraft II' -X 'mods/core.sc2mod/base.sc2data/ stormex -S '/mnt/s1/BnetGameLib/StarCraft II' -X 'mods/core.sc2mod/base.sc2data/EditorData/Images/EditorLogo.dds' -p | magick dds: png: | display png: ``` +#### Mount as FUSE filesystem + +```sh +mkdir -p cascfs +stormex -v -S '/mnt/s1/BnetGameLib/StarCraft II' -m ./cascfs +``` + +Result: + +```sh +$ ls -l ./cascfs +dr-xr-xr-- - root 1 Jan 1970 campaigns +dr-xr-xr-- - root 1 Jan 1970 CKEY +.r-xr-xr-- 17M root 1 Jan 1970 DOWNLOAD +.r-xr-xr-- 41M root 1 Jan 1970 ENCODING +.r-xr-xr-- 59k root 1 Jan 1970 INSTALL +dr-xr-xr-- - root 1 Jan 1970 mods +.r-xr-xr-- 20M root 1 Jan 1970 ROOT +dr-xr-xr-- - root 1 Jan 1970 versions.osxarchive +dr-xr-xr-- - root 1 Jan 1970 versions.winarchive +``` + +##### Windows support via Dokany + +[Dokany](https://github.com/dokan-dev) provides a FUSE wrapper for Windows. You've to install [Dokany's system driver](https://github.com/dokan-dev/dokany/wiki/Installation) in order for this feature to work. + +As `[MOUNTPOINT]` argument provide a free drive letter. In following example CASC will be mounted at `S:\`. + +```sh +stormex.exe -v -S X:\shared\SC2.4.8.4.73286 -m S +``` + +[![](https://i.imgur.com/1y7zCTL.png)](https://i.imgur.com/1y7zCTL.png) + ## Credits * Powered by [CascLib](https://github.com/ladislav-zezula/CascLib) diff --git a/include/cascfuse.hpp b/include/cascfuse.hpp index ecf5d19..a15f0a7 100644 --- a/include/cascfuse.hpp +++ b/include/cascfuse.hpp @@ -2,4 +2,4 @@ #include "storage.hpp" -int cascf_mount(const std::string& mountPoint, HANDLE hStorage); +int cascfs_mount(const std::string& mountPoint, HANDLE hStorage); diff --git a/src/cascfuse.cc b/src/cascfuse.cc index 94deb76..6f66337 100644 --- a/src/cascfuse.cc +++ b/src/cascfuse.cc @@ -17,7 +17,12 @@ #include "common.hpp" #include "util.hpp" -enum FsNodeKind { +#ifndef WIN32 + #define FUSE_STAT struct stat + #define FUSE_OFF_T off_t +#endif + +enum class FsNodeKind { Unknown, Root, Folder, @@ -227,11 +232,11 @@ class FsTree { FsTree cfFileTree; -static int cascf_getattr(const char *path, struct stat *stbuf) +static int cascfs_getattr(const char *path, FUSE_STAT *stbuf) { LOG_VERBOSE << path; int res = 0; - memset(stbuf, 0, sizeof(struct stat)); + memset(stbuf, 0, sizeof(*stbuf)); auto fNode = cfFileTree.GetNodeAtPath(path); if (fNode != NULL) { @@ -268,7 +273,7 @@ static int cascf_getattr(const char *path, struct stat *stbuf) return res; } -static int cascf_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) +static int cascfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, FUSE_OFF_T offset, struct fuse_file_info *fi) { LOG_VERBOSE << path; @@ -287,7 +292,7 @@ static int cascf_readdir(const char *path, void *buf, fuse_fill_dir_t filler, of return 0; } -static int cascf_open(const char *path, struct fuse_file_info *fi) +static int cascfs_open(const char *path, struct fuse_file_info *fi) { LOG_VERBOSE << path; @@ -302,7 +307,7 @@ static int cascf_open(const char *path, struct fuse_file_info *fi) return 0; } -static int cascf_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) +static int cascfs_read(const char *path, char *buf, size_t size, FUSE_OFF_T offset, struct fuse_file_info *fi) { LOG_VERBOSE << path << " at " << offset << " size " << size; @@ -335,25 +340,12 @@ static int cascf_read(const char *path, char *buf, size_t size, off_t offset, st } } -static struct fuse_operations cascf_oper = { - .getattr = cascf_getattr, - .open = cascf_open, - .read = cascf_read, - .readdir = cascf_readdir, -}; - -struct TFileTreeRootPub : public TFileTreeRoot -{ -public: - CASC_FILE_TREE FileTree; -}; - -void cascf_populate(HANDLE hStorage) +void cascfs_populate(HANDLE hStorage) { auto hs = IsValidCascStorageHandle(hStorage); cfFileTree.m_hStorage = hStorage; - PLOG_INFO << "Building file tree.."; + LOG_DEBUG << "Building file tree.."; CASC_FIND_DATA findData; HANDLE handle = CascFindFirstFile(hStorage, "*", &findData, NULL); @@ -390,20 +382,31 @@ void cascf_populate(HANDLE hStorage) cfFileTree.GenerateNodeHashMap(cfFileTree.GetRootNode()); } -int cascf_mount(const std::string& mountPoint, HANDLE hStorage) +static struct fuse_operations cascf_oper; + +int cascfs_mount(const std::string& mountPoint, HANDLE hStorage) { - cascf_populate(hStorage); + cascf_oper.getattr = cascfs_getattr; + cascf_oper.open = cascfs_open; + cascf_oper.read = cascfs_read; + cascf_oper.readdir = cascfs_readdir; + + cascfs_populate(hStorage); LOG_DEBUG << "Preparing to mount.."; + +#ifndef WIN32 if (!pathExists(mountPoint)) { LOG_FATAL << "Path doesn't exist or is not accessible" << mountPoint; return -2; } +#endif auto fChan = fuse_mount(mountPoint.c_str(), NULL); if (fChan != NULL) { auto fHandle = fuse_new(fChan, NULL, &cascf_oper, sizeof(cascf_oper), NULL); if (fHandle != NULL) { + LOG_INFO << "cascfs " << static_cast(fHandle) << " mounted at " << mountPoint; struct fuse_session *se = fuse_get_session(fHandle); if (fuse_set_signal_handlers(se) == 0) { LOG_DEBUG << "Entering CASC-FS loop.."; @@ -416,6 +419,10 @@ int cascf_mount(const std::string& mountPoint, HANDLE hStorage) fuse_unmount(mountPoint.c_str(), fChan); fuse_destroy(fHandle); } + else { + LOG_FATAL << "fuse_new failed " << static_cast(fHandle); + return -2; + } } else { LOG_FATAL << "Couldn't mount to " << mountPoint; diff --git a/src/stormex.cc b/src/stormex.cc index 06d0dec..13b01e7 100644 --- a/src/stormex.cc +++ b/src/stormex.cc @@ -301,7 +301,7 @@ int main(int argc, char* argv[]) StorageExplorer stExplorer; int tmp; - PLOG_INFO << "Opening storage.."; + LOG_DEBUG << "Opening storage.."; if ((tmp = stExplorer.openStorage(appCtx.m_base.storageSrc)) != 0) { PLOG_FATAL << "Failed to open the storage: " << appCtx.m_base.storageSrc << " E(" << tmp << ")"; exit(-1); @@ -310,7 +310,7 @@ int main(int argc, char* argv[]) try { if (appCtx.m_mount.mountPoint.length()) { - return cascf_mount(appCtx.m_mount.mountPoint, stExplorer.getHandle()); + return cascfs_mount(appCtx.m_mount.mountPoint, stExplorer.getHandle()); } auto fResults = enumerateFiles(stExplorer);