From 0dc9961e0287cba0a15bd06ff5c1d0ff329f8253 Mon Sep 17 00:00:00 2001 From: Talv Date: Wed, 31 Jul 2019 15:01:24 +0200 Subject: [PATCH] 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);