diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..7e854f1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,145 @@ +# Generated from CLion Inspection settings +--- +Checks: '-*, +bugprone-argument-comment, +bugprone-assert-side-effect, +bugprone-bad-signal-to-kill-thread, +bugprone-branch-clone, +bugprone-copy-constructor-init, +bugprone-dangling-handle, +bugprone-dynamic-static-initializers, +bugprone-fold-init-type, +bugprone-forward-declaration-namespace, +bugprone-forwarding-reference-overload, +bugprone-inaccurate-erase, +bugprone-incorrect-roundings, +bugprone-integer-division, +bugprone-lambda-function-name, +bugprone-macro-parentheses, +bugprone-macro-repeated-side-effects, +bugprone-misplaced-operator-in-strlen-in-alloc, +bugprone-misplaced-pointer-arithmetic-in-alloc, +bugprone-misplaced-widening-cast, +bugprone-move-forwarding-reference, +bugprone-multiple-statement-macro, +bugprone-no-escape, +bugprone-parent-virtual-call, +bugprone-posix-return, +bugprone-reserved-identifier, +bugprone-sizeof-container, +bugprone-sizeof-expression, +bugprone-spuriously-wake-up-functions, +bugprone-string-constructor, +bugprone-string-integer-assignment, +bugprone-string-literal-with-embedded-nul, +bugprone-suspicious-enum-usage, +bugprone-suspicious-include, +bugprone-suspicious-memset-usage, +bugprone-suspicious-missing-comma, +bugprone-suspicious-semicolon, +bugprone-suspicious-string-compare, +bugprone-suspicious-memory-comparison, +bugprone-suspicious-realloc-usage, +bugprone-swapped-arguments, +bugprone-terminating-continue, +bugprone-throw-keyword-missing, +bugprone-too-small-loop-variable, +bugprone-undefined-memory-manipulation, +bugprone-undelegated-constructor, +bugprone-unhandled-self-assignment, +bugprone-unused-raii, +bugprone-unused-return-value, +bugprone-use-after-move, +bugprone-virtual-near-miss, +cert-dcl21-cpp, +cert-dcl58-cpp, +cert-err34-c, +cert-err52-cpp, +cert-err60-cpp, +cert-flp30-c, +cert-msc50-cpp, +cert-msc51-cpp, +cert-str34-c, +cppcoreguidelines-interfaces-global-init, +cppcoreguidelines-narrowing-conversions, +cppcoreguidelines-pro-type-member-init, +cppcoreguidelines-pro-type-static-cast-downcast, +cppcoreguidelines-slicing, +google-default-arguments, +google-runtime-operator, +hicpp-exception-baseclass, +hicpp-multiway-paths-covered, +misc-misplaced-const, +misc-new-delete-overloads, +misc-non-copyable-objects, +misc-throw-by-value-catch-by-reference, +misc-unconventional-assign-operator, +misc-uniqueptr-reset-release, +modernize-avoid-bind, +modernize-concat-nested-namespaces, +modernize-deprecated-headers, +modernize-deprecated-ios-base-aliases, +modernize-loop-convert, +modernize-make-shared, +modernize-make-unique, +modernize-pass-by-value, +modernize-raw-string-literal, +modernize-redundant-void-arg, +modernize-replace-auto-ptr, +modernize-replace-disallow-copy-and-assign-macro, +modernize-replace-random-shuffle, +modernize-return-braced-init-list, +modernize-shrink-to-fit, +modernize-unary-static-assert, +modernize-use-auto, +modernize-use-bool-literals, +modernize-use-emplace, +modernize-use-equals-default, +modernize-use-equals-delete, +modernize-use-nodiscard, +modernize-use-noexcept, +modernize-use-nullptr, +modernize-use-override, +modernize-use-transparent-functors, +modernize-use-uncaught-exceptions, +mpi-buffer-deref, +mpi-type-mismatch, +openmp-use-default-none, +performance-faster-string-find, +performance-for-range-copy, +performance-implicit-conversion-in-loop, +performance-inefficient-algorithm, +performance-inefficient-string-concatenation, +performance-inefficient-vector-operation, +performance-move-const-arg, +performance-move-constructor-init, +performance-no-automatic-move, +performance-noexcept-move-constructor, +performance-trivially-destructible, +performance-type-promotion-in-math-fn, +performance-unnecessary-copy-initialization, +performance-unnecessary-value-param, +portability-simd-intrinsics, +readability-avoid-const-params-in-decls, +readability-const-return-type, +readability-container-size-empty, +readability-convert-member-functions-to-static, +readability-delete-null-pointer, +readability-deleted-default, +readability-inconsistent-declaration-parameter-name, +readability-make-member-function-const, +readability-misleading-indentation, +readability-misplaced-array-index, +readability-non-const-parameter, +readability-redundant-control-flow, +readability-redundant-declaration, +readability-redundant-function-ptr-dereference, +readability-redundant-smartptr-get, +readability-redundant-string-cstr, +readability-redundant-string-init, +readability-simplify-subscript-expr, +readability-static-accessed-through-instance, +readability-static-definition-in-anonymous-namespace, +readability-string-compare, +readability-uniqueptr-delete-release, +readability-use-anyofallof' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d53906..5118eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -47,11 +47,11 @@ sgfscrypt.sys sgfscrypt.inf # ===== 项目私有目录/文件 ===== -driver/ -refer/ -overlay/ -check/ -note.txt +/driver/ +/refer/ +/overlay/ +/check/ +/note.txt # ===== Windows 系统文件 ===== Thumbs.db diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c23400..a297b68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,44 @@ project(sega_mount) set(CMAKE_CXX_STANDARD 20) -add_executable(sega_mount - src/main.cpp +# ===== 公共编译设置 ===== +if (WIN32) + add_compile_options(/utf-8) + add_compile_definitions(WINVER=0x0A00 _WIN32_WINNT=0x0A00 NTDDI_VERSION=0x0A000000) +endif () + +# ===== FetchContent: ImGui (Win32 + DirectX11 后端) ===== +include(FetchContent) +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.92.9b + GIT_SHALLOW TRUE + CONFIGURE_COMMAND "" # ImGui 无 CMakeLists.txt,跳过自动配置 + BUILD_COMMAND "" +) +FetchContent_MakeAvailable(imgui) + +# 手动构建 ImGui 静态库(ImGui 仓库不含 CMakeLists.txt) +add_library(imgui STATIC + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_win32.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_dx11.cpp +) +target_include_directories(imgui PUBLIC + ${imgui_SOURCE_DIR} + ${imgui_SOURCE_DIR}/backends +) +if (WIN32) + target_link_libraries(imgui PUBLIC d3d11 d3dcompiler dxgi) +endif () + +# ===== 公共源文件(cmd 和 gui 共享) ===== +set(SEGA_CORE_SOURCES + src/config.cpp src/driver_service.cpp src/fscrypt.cpp src/icf_parser.cpp @@ -18,16 +54,48 @@ add_executable(sega_mount src/utils/strings.cpp ) -target_include_directories(sega_mount PRIVATE src) +set(SEGA_WIN_LIBS bcrypt virtdisk) +# ===== 目标1: sega_mount_cmd (命令行版) ===== +add_executable(sega_mount_cmd + src/cmd/main.cpp + ${SEGA_CORE_SOURCES} +) +target_include_directories(sega_mount_cmd PRIVATE src) if (WIN32) - target_link_libraries(sega_mount PRIVATE bcrypt virtdisk) - # 源文件含中文注释,指定UTF-8编码避免MSVC解析错误 - target_compile_options(sega_mount PRIVATE /utf-8) - # 指定Windows SDK最低版本为Win10,启用virtdisk.h等结构体定义 - target_compile_definitions(sega_mount PRIVATE - WINVER=0x0A00 - _WIN32_WINNT=0x0A00 - NTDDI_VERSION=0x0A000000 - ) + target_link_libraries(sega_mount_cmd PRIVATE ${SEGA_WIN_LIBS}) +endif () +set_target_properties(sega_mount_cmd PROPERTIES OUTPUT_NAME sega_mount_cmd) + +# ===== 目标2: sega_mount_gui (GUI版, ImGui + Win32 + DirectX11) ===== +add_executable(sega_mount_gui + src/gui/main.cpp + src/gui/theme.cpp + src/gui/dx11_backend.cpp + src/gui/gui_utils.cpp + src/gui/ui.cpp + ${SEGA_CORE_SOURCES} +) +target_include_directories(sega_mount_gui PRIVATE src) +if (WIN32) + target_link_libraries(sega_mount_gui PRIVATE ${SEGA_WIN_LIBS} imgui comdlg32) + # WinMain 入口需要 Windows 子系统(不弹控制台窗口) + set_target_properties(sega_mount_gui PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS") +endif () +set_target_properties(sega_mount_gui PROPERTIES OUTPUT_NAME sega_mount_gui) + +# ===== 单元测试: 挂载/卸载 OPT 容器 ===== +add_executable(test_mount_opt + tests/test_mount_opt.cpp + src/driver_service.cpp + src/fscrypt.cpp + src/utils/aes.cpp + src/utils/bcrypt_util.cpp + src/utils/files.cpp + src/utils/log.cpp + src/utils/strings.cpp +) +target_include_directories(test_mount_opt PRIVATE src) +if (WIN32) + target_link_libraries(test_mount_opt PRIVATE bcrypt) endif () diff --git a/src/cmd/main.cpp b/src/cmd/main.cpp new file mode 100644 index 0000000..ad89f90 --- /dev/null +++ b/src/cmd/main.cpp @@ -0,0 +1,100 @@ +#define LOG_MODULE "MAIN" +#include "config.h" +#include "driver_service.h" +#include "icf_mount.h" +#include "utils/strings.h" +#include "utils/files.h" +#include "utils/log.h" +#include +#include + +static void print_usage() { + std::fputs( + "SEGA MOUNT\n" + "Usage:\n" + " sega_mount -m [config.ini] Mount\n" + " sega_mount -u [config.ini] Unmount\n", + stdout + ); + std::fflush(stdout); +} + +int main(int argc, char *argv[]) { + // 设置控制台为UTF-8代码页,配合/utf-8编译选项避免中文乱码 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + if (argc < 2) { + print_usage(); + return 1; + } + + // 解析命令:-m 挂载,-u 卸载 + std::string cmd = argv[1]; + bool is_mount = cmd == "-m"; + bool is_unmount = cmd == "-u"; + if (!is_mount && !is_unmount) { + LOG_ERROR("Unknown command: {}", cmd); + print_usage(); + return 1; + } + + // 确定config.ini路径:优先命令行参数,否则使用当前运行目录 + if (argc >= 3) { + g_config_path_w = utils::to_wstring(argv[2]); + } else { + g_config_path_w = std::format(L"{}\\config.ini", utils::get_cwd()); + } + + // 解析config.ini 到全局 g_cfg + std::string cfg_err; + if (!parse_config(g_config_path_w, g_cfg, &cfg_err)) { + LOG_ERROR("{}", cfg_err); + return 1; + } + g_config_path = utils::wstr_to_str(g_config_path_w); + + if (g_cfg.icf_path.empty()) { + LOG_ERROR("ICF_PATH is empty in config"); + return 1; + } + + LOG_INFO("Config loaded from {}", g_config_path); + LOG_INFO(" ICF_PATH: {}", utils::wstr_to_str(g_cfg.icf_path)); + LOG_INFO(" IMAGE_DIR: {}", utils::wstr_to_str(g_cfg.image_dir)); + LOG_INFO(" OVERLAY_DIR: {}", utils::wstr_to_str(g_cfg.overlay_dir)); + LOG_INFO(" MOUNT_LETTER: {}", static_cast(g_cfg.mount_letter)); + LOG_INFO(" APP_LINK: {}", utils::wstr_to_str(g_cfg.app_link)); + LOG_INFO(" OPT_LINK: {}", utils::wstr_to_str(g_cfg.opt_link)); + LOG_INFO(" APP_KEY: {}", g_cfg.app_key.empty() ? "(use built-in table)" : "(override)"); + LOG_INFO(" OPT_KEY: {}", g_cfg.opt_key.empty() ? "(use built-in table)" : "(override)"); + + // 确保sgfscrypt服务已安装并运行 + std::string svc_err; + if (!driver::ensure_running(&svc_err)) { + LOG_ERROR("Failed to start driver service: {}", svc_err); + LOG_ERROR("Hint: run this program as Administrator"); + return 1; + } + + if (is_mount) { + auto result = icf_mount::mount_icf_apps(g_cfg); + if (!result.success) { + LOG_ERROR("Mount failed: {}", result.error); + return 1; + } + LOG_INFO("Mount Success: {} APPs, {} OPTs mounted", + result.apps.size(), result.opts.size()); + return 0; + } + + // is_unmount + auto result = icf_mount::unmount_icf_apps(g_cfg); + if (!result.success) { + LOG_ERROR("Unmount failed: {}", result.error); + return 1; + } + LOG_INFO("Unmount Success: {} APPs, {} OPTs unmounted", + result.apps.size(), result.opts.size()); + return 0; +} diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..4a8dc93 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,69 @@ +#include "config.h" +#include "utils/strings.h" +#include + +// 全局实例定义 +Config g_cfg; +std::string g_config_path; +std::wstring g_config_path_w; + +bool parse_config(const std::wstring &path, Config &cfg, std::string *error) { + if (GetFileAttributesW(path.c_str()) == INVALID_FILE_ATTRIBUTES) { + if (error) *error = "Config file not found: " + utils::wstr_to_str(path); + return false; + } + + wchar_t buf[512]; + + auto read_w = [&](const wchar_t *key) -> std::wstring { + DWORD len = GetPrivateProfileStringW(L"CONFIG", key, L"", buf, _countof(buf), path.c_str()); + return {buf, len}; + }; + + cfg.icf_path = read_w(L"ICF_PATH"); + cfg.image_dir = read_w(L"IMAGE_DIR"); + cfg.overlay_dir = read_w(L"OVERLAY_DIR"); + + std::wstring letter = read_w(L"MOUNT_LETTER"); + if (!letter.empty()) { + wchar_t ch = letter[0]; + if (ch >= L'a' && ch <= L'z') ch -= L'a' - L'A'; + cfg.mount_letter = ch; + } + + cfg.app_link = read_w(L"APP_LINK"); + cfg.opt_link = read_w(L"OPT_LINK"); + + auto read_a = [&](const wchar_t *key) -> std::string { + DWORD len = GetPrivateProfileStringW(L"KEYS", key, L"", buf, _countof(buf), path.c_str()); + return utils::wstr_to_str(std::wstring(buf, len)); + }; + + cfg.app_key = read_a(L"APP_KEY"); + cfg.app_iv = read_a(L"APP_IV"); + cfg.opt_key = read_a(L"OPT_KEY"); + cfg.opt_iv = read_a(L"OPT_IV"); + + return true; +} + +bool save_config(const std::wstring &path, const Config &cfg, std::string *error) { + auto write_w = [&](const wchar_t *key, const std::wstring &val) { + WritePrivateProfileStringW(L"CONFIG", key, val.c_str(), path.c_str()); + }; + auto write_a = [&](const wchar_t *key, const std::string &val) { + WritePrivateProfileStringW(L"KEYS", key, utils::to_wstring(val).c_str(), path.c_str()); + }; + + write_w(L"ICF_PATH", cfg.icf_path); + write_w(L"IMAGE_DIR", cfg.image_dir); + write_w(L"OVERLAY_DIR", cfg.overlay_dir); + write_w(L"MOUNT_LETTER", std::wstring(1, cfg.mount_letter)); + write_w(L"APP_LINK", cfg.app_link); + write_w(L"OPT_LINK", cfg.opt_link); + write_a(L"APP_KEY", cfg.app_key); + write_a(L"APP_IV", cfg.app_iv); + write_a(L"OPT_KEY", cfg.opt_key); + write_a(L"OPT_IV", cfg.opt_iv); + return true; +} diff --git a/src/config.h b/src/config.h index 0ed439e..b94db98 100644 --- a/src/config.h +++ b/src/config.h @@ -16,3 +16,16 @@ struct Config { std::string opt_key; // OPT密钥hex std::string opt_iv; // OPT IV hex }; + +// 全局配置实例,cmd 和 gui 共用 +extern Config g_cfg; + +// 全局配置文件路径(UTF-8 用于显示,宽字符用于 API 调用) +extern std::string g_config_path; +extern std::wstring g_config_path_w; + +// 解析 config.ini 到 cfg,失败返回 false 并设置 error +bool parse_config(const std::wstring &path, Config &cfg, std::string *error); + +// 保存 cfg 到 config.ini +bool save_config(const std::wstring &path, const Config &cfg, std::string *error); diff --git a/src/driver_service.cpp b/src/driver_service.cpp index cab3654..d1d0a15 100644 --- a/src/driver_service.cpp +++ b/src/driver_service.cpp @@ -11,45 +11,64 @@ namespace driver { bool ensure_running(std::string *error) { SC_HANDLE hScm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); if (!hScm) { - if (error) *error = "OpenSCManager failed: " + std::to_string(GetLastError()); + if (error) *error = "OpenSCManager failed: " + utils::last_error_str(); return false; } // Try to open existing service - SC_HANDLE hSvc = OpenServiceW(hScm, SERVICE_NAME, - SERVICE_ALL_ACCESS); + SC_HANDLE hSvc = OpenServiceW(hScm, SERVICE_NAME,SERVICE_ALL_ACCESS); if (!hSvc) { DWORD last = GetLastError(); if (last != ERROR_SERVICE_DOES_NOT_EXIST) { - if (error) *error = "OpenService failed: " + std::to_string(last); + if (error) *error = "OpenService failed: " + utils::last_error_str(last); CloseServiceHandle(hScm); return false; } - // Service does not exist - create it - std::wstring driver_path = utils::get_exe_dir() + L"\\" + DRIVER_FILENAME; + // Service does not exist - copy driver to system directory and create service + std::wstring src_path = utils::get_exe_dir() + L"\\" + DRIVER_FILENAME; + + // Get system drivers directory (typically C:\Windows\System32\drivers) + wchar_t sys_dir[MAX_PATH] = {}; + UINT sys_len = GetSystemDirectoryW(sys_dir, MAX_PATH); + if (sys_len == 0 || sys_len > MAX_PATH) { + if (error) *error = "GetSystemDirectory failed: " + utils::last_error_str(); + CloseServiceHandle(hScm); + return false; + } + std::wstring dst_path = std::wstring(sys_dir) + L"\\drivers\\" + DRIVER_FILENAME; + + // Copy driver file to system directory (overwrite if exists) + if (!CopyFileW(src_path.c_str(), dst_path.c_str(), FALSE)) { + if (error) *error = "Copy driver to system directory failed: " + utils::last_error_str(); + CloseServiceHandle(hScm); + return false; + } + LOG_INFO("Driver copied to {}", utils::wstr_to_str(dst_path)); + hSvc = CreateServiceW( hScm, SERVICE_NAME, SERVICE_NAME, SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, - driver_path.c_str(), - nullptr, nullptr, nullptr, nullptr, nullptr); + dst_path.c_str(), + nullptr, nullptr, nullptr, nullptr, nullptr + ); if (!hSvc) { - if (error) *error = "CreateService failed: " + std::to_string(GetLastError()); + if (error) *error = "CreateService failed: " + utils::last_error_str(); CloseServiceHandle(hScm); return false; } - LOG_INFO("Service installed: {}", utils::wstr_to_str(driver_path)); + LOG_INFO("Service installed: {}", utils::wstr_to_str(dst_path)); } // Query current state SERVICE_STATUS status{}; if (!QueryServiceStatus(hSvc, &status)) { - if (error) *error = "QueryServiceStatus failed: " + std::to_string(GetLastError()); + if (error) *error = "QueryServiceStatus failed: " + utils::last_error_str(); CloseServiceHandle(hSvc); CloseServiceHandle(hScm); return false; @@ -65,7 +84,7 @@ namespace driver { if (!StartServiceW(hSvc, 0, nullptr)) { DWORD last = GetLastError(); if (last != ERROR_SERVICE_ALREADY_RUNNING) { - if (error) *error = "StartService failed: " + std::to_string(last); + if (error) *error = "StartService failed: " + utils::last_error_str(last); CloseServiceHandle(hSvc); CloseServiceHandle(hScm); return false; diff --git a/src/fscrypt.cpp b/src/fscrypt.cpp index 78c2dbb..62bd176 100644 --- a/src/fscrypt.cpp +++ b/src/fscrypt.cpp @@ -10,8 +10,24 @@ #include namespace fscrypt { + // ===== 全局密钥覆盖 ===== + // 由 set_key() 设置,空字符串表示对应类型用内置密钥表 + static std::string g_app_key_hex; + static std::string g_app_iv_hex; + static std::string g_opt_key_hex; + static std::string g_opt_iv_hex; + + // 设置全局密钥覆盖(在挂载前调用一次) + void set_key(const std::string &app_key_hex, const std::string &app_iv_hex, + const std::string &opt_key_hex, const std::string &opt_iv_hex) { + g_app_key_hex = app_key_hex; + g_app_iv_hex = app_iv_hex; + g_opt_key_hex = opt_key_hex; + g_opt_iv_hex = opt_iv_hex; + } + // 解析解密后的BootId,提取容器类型、数据偏移、扇区大小等信息 - bool parse_boot_id(const uint8_t *decrypted_bootid, size_t size, BootIdInfo *info, std::string *error) { + static bool parse_boot_id(const uint8_t *decrypted_bootid, size_t size, BootIdInfo *info, std::string *error) { if (size < 0x38) { if (error) *error = "Decrypted BootId is too small"; return false; @@ -27,14 +43,14 @@ namespace fscrypt { info->data_offset = read_u64_le(decrypted_bootid + 32); info->data_size = read_u64_le(decrypted_bootid + 40); info->sector_size = read_u64_le(decrypted_bootid + 48); - info->total_sectors = info->sector_size * info->data_size; + info->total_sectors = info->data_size * info->sector_size; info->encrypted_offset = (info->data_size * (info->data_offset - info->sector_size)) << 8; return true; } // 根据BootId容器类型识别容器名称和镜像类型(OS/APP/OPTION) - bool recognize_container(ContainerType type, const char **container_name, const char **image_type, - std::string *error) { + static bool recognize_container(ContainerType type, const char **container_name, const char **image_type, + std::string *error) { switch (static_cast(type)) { case 0: *container_name = "NTFS Container"; @@ -54,10 +70,10 @@ namespace fscrypt { } } - // 查找AES密钥和IV:优先用config.ini override,为空则查内置密钥表 + // 查找AES密钥和IV:若已通过set_key设置则用之,否则查内置密钥表 // 规则:取文件名前4字符作为GameID,.app用AppKey/AppIV,.opt用OptKey/OptIV - bool lookup_key_iv(const std::wstring &container_path, uint8_t out_key[16], uint8_t out_iv[16], - std::string *error, const KeyOverride *override_keys) { + static bool lookup_key_iv(const std::wstring &container_path, uint8_t out_key[16], uint8_t out_iv[16], + std::string *error) { // 提取文件名 std::wstring filename = container_path; size_t slash = filename.find_last_of(L"\\/"); @@ -75,27 +91,23 @@ namespace fscrypt { } // 判断扩展名:.app 或 .opt - bool is_app = filename.size() >= 4 && - _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".app") == 0; - bool is_opt = filename.size() >= 4 && - _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".opt") == 0; + bool is_app = filename.size() >= 4 && _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".app") == 0; + bool is_opt = filename.size() >= 4 && _wcsicmp(filename.substr(filename.size() - 4).c_str(), L".opt") == 0; if (!is_app && !is_opt) { if (error) *error = "Filename must end with .app or .opt"; return false; } - // 优先使用config.ini指定的密钥(override) - if (override_keys) { - const std::string &key_hex = is_app ? override_keys->app_key_hex : override_keys->opt_key_hex; - const std::string &iv_hex = is_app ? override_keys->app_iv_hex : override_keys->opt_iv_hex; - if (!key_hex.empty() && !iv_hex.empty()) { - if (!utils::hex_to_bytes(key_hex, out_key, 16) || - !utils::hex_to_bytes(iv_hex, out_iv, 16)) { - if (error) *error = std::format("Invalid hex in config override: {}/{}", key_hex, iv_hex); - return false; - } - return true; + // 优先使用set_key设置的全局密钥 + const std::string &key_hex = is_app ? g_app_key_hex : g_opt_key_hex; + const std::string &iv_hex = is_app ? g_app_iv_hex : g_opt_iv_hex; + if (!key_hex.empty() && !iv_hex.empty()) { + if (!utils::hex_to_bytes(key_hex, out_key, 16) || + !utils::hex_to_bytes(iv_hex, out_iv, 16)) { + if (error) *error = std::format("Invalid hex in set_key: {}/{}", key_hex, iv_hex); + return false; } + return true; } // 从编译期密钥表查找GameID,直接拷贝字节数组 @@ -115,23 +127,30 @@ namespace fscrypt { // 挂载容器:读取BootId、解密、识别类型,向驱动发送MOUNT IOCTL - MountResult mount_container(const std::wstring &container_path, const std::wstring &tag, - const KeyOverride *override_keys) { + // 密钥来源:若已通过set_key设置则用之,否则查内置密钥表 + MountResult mount_container(const std::wstring &container_path, const std::wstring &tag) { MountResult result = {}; // Open Fscrypt driver - HANDLE hDriver = CreateFileW(L"\\\\.\\Fscrypt", GENERIC_READ | GENERIC_WRITE, - 0, nullptr, OPEN_EXISTING, 0x80, nullptr); + HANDLE hDriver = CreateFileW( + L"\\\\.\\Fscrypt", + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + OPEN_EXISTING, + 0x80, + nullptr + ); if (hDriver == INVALID_HANDLE_VALUE) { - result.error = std::format("Open Fscrypt Driver Failed: {}", GetLastError()); + result.error = std::format("Open Fscrypt Driver Failed: {}", utils::last_error_str()); return result; } - // Look up AES key and IV from embedded keys.json based on filename + // Look up AES key and IV: use set_key override if set, else built-in table uint8_t key[16]; uint8_t app_iv[16]; std::string err; - if (!lookup_key_iv(container_path, key, app_iv, &err, override_keys)) { + if (!lookup_key_iv(container_path, key, app_iv, &err)) { result.error = std::format("Lookup key/IV failed: {}", err); CloseHandle(hDriver); return result; @@ -186,70 +205,74 @@ namespace fscrypt { LOG_INFO("{} ({})", container_name, image_type); fclose(container_file); - // Prepare mount IOCTL input buffer (total 0x280 = 640 bytes) - // Layout from fscrypt_link.exe sub_140005620 (Buffer is wchar_t[], offsets in bytes): - // 0x000: int32 code = 640 - // 0x004: wchar_t path[258] (NT path: \??\...) - // 0x208: uint64_t unk_208 = 0x200000 - // 0x210: uint64_t unk_210 = 0 - // 0x218: uint64_t unk_218 = 1 - // 0x220: uint8_t key[16] (AES key from key.bin / JSON) - // 0x230: uint8_t iv[16] (AES IV, calculated or from JSON) - // 0x240: wchar_t tag[32] + // ===== MOUNT IOCTL 输入缓冲区 (IOCTL 0x22E008, 0x280 = 640 bytes) ===== + // 通过IDA逆向 sgfscrypt.sys 的 fscrypt_mount_with_tag / fscrypt_init_device_extension 得到: + // + // 0x000: uint32 buffer_size 声明的缓冲区大小。>=0x240 不读tag(自动编号),>=0x280 读tag + // 0x004: wchar_t path[258] 容器文件 NT 路径,如 \??\C:\xxx\SDGB.app + // 0x208: uint64 header_size 容器头部跳过字节数。data_offset=0时 disk_size = file_size - header_size + // 0x210: uint64 data_offset 数据区起始偏移。非0时 disk_size = data_offset,为0则自动计算 + // 0x218: uint32 encrypted 加密标志,非0则用 key/IV 初始化 AES-128-CBC 读写加解密 + // 0x21C: uint32 reserved 驱动读取但未使用,填0 + // 0x220: uint8 aes_key[16] AES-128 密钥 + // 0x230: uint8 aes_iv[16] AES-128 IV + // 0x240: wchar_t tag[32] 设备标签,生成 \\.\FscryptDisk_ + // + // 输出缓冲区: wchar_t tag[] — 驱动格式化后的 tag 字符串(含null终止),长度 = wcslen(tag)*2 + 2 #pragma pack(push, 1) struct MountInBuffer { - int code; - wchar_t path[258]; - uint64_t unk_208; - uint64_t unk_210; - uint64_t unk_218; - uint8_t key[16]; - uint8_t iv[16]; - wchar_t tag[32]; + uint32_t buffer_size; // 0x000 + wchar_t path[258]; // 0x004 + uint64_t header_size; // 0x208 + uint64_t data_offset; // 0x210 + uint32_t encrypted; // 0x218 + uint32_t reserved; // 0x21C + uint8_t aes_key[16]; // 0x220 + uint8_t aes_iv[16]; // 0x230 + wchar_t tag[32]; // 0x240 }; #pragma pack(pop) static_assert(sizeof(MountInBuffer) == 0x280, "MountInBuffer must be 0x280"); MountInBuffer in_buf{}; memset(&in_buf, 0, sizeof(in_buf)); - in_buf.code = 640; + in_buf.buffer_size = sizeof(in_buf); // 0x280,走完整模式(带tag) // Build NT path: \??\ std::wstring nt_path = std::format(L"\\??\\{}", utils::normalize_path(container_path)); wcsncpy_s(in_buf.path, nt_path.c_str(), 257); - in_buf.unk_208 = 0x200000; - in_buf.unk_210 = 0; - in_buf.unk_218 = 1; + in_buf.header_size = 0x200000; // 跳过容器头部 2MB + in_buf.data_offset = 0; // 0 = 自动计算: disk_size = file_size - header_size + in_buf.encrypted = 1; // 启用 AES-128-CBC 读写加解密 - // Copy AES key and IV from JSON lookup (key at 0x220, iv at 0x230) - memcpy(in_buf.key, key, 16); - memcpy(in_buf.iv, app_iv, 16); + // Copy AES key and IV (key at 0x220, iv at 0x230) + memcpy(in_buf.aes_key, key, 16); + memcpy(in_buf.aes_iv, app_iv, 16); // Copy tag wcsncpy_s(in_buf.tag, tag.c_str(), 31); - // Send mount IOCTL - uint8_t out_buf[1024] = {}; + // Send mount IOCTL — 驱动在输出缓冲区返回格式化的 NT 路径: \??\FscryptDisk_ + // bytes_returned = wcslen(out) * 2 + 2 (含 null 终止符) + wchar_t out_buf[512] = {}; DWORD bytes_returned = 0; if (!DeviceIoControl(hDriver, IOCTL_FSCRYPT_MOUNT, &in_buf, sizeof(in_buf), out_buf, sizeof(out_buf), &bytes_returned, nullptr)) { - result.error = std::format("DeviceIoControl failed, Error = {}", GetLastError()); + result.error = std::format("DeviceIoControl failed: {}", utils::last_error_str()); CloseHandle(hDriver); return result; } - // Query symlink to verify mount - std::wstring device_path = std::format(L"\\\\.\\FscryptDisk_{}", tag); - HANDLE hCheck = CreateFileW(device_path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (hCheck != INVALID_HANDLE_VALUE) { - CloseHandle(hCheck); + // 从输出缓冲区取驱动返回的 NT 路径,验证挂载成功 + if (bytes_returned >= sizeof(wchar_t) && out_buf[0] != 0) { + result.nt_target = out_buf; // 含 \??\FscryptDisk_\ . result.success = true; + LOG_INFO("Mounted: {}", utils::wstr_to_str(result.nt_target)); } else { - LOG_ERROR("Query Symbol Link Failed"); + result.error = "Mount IOCTL succeeded but output buffer is empty"; result.success = false; } @@ -258,38 +281,53 @@ namespace fscrypt { } // 卸载容器:向驱动发送UNMOUNT IOCTL + // ===== UNMOUNT IOCTL 输入缓冲区 (IOCTL 0x22E00C, 0x204 = 516 bytes) ===== + // 通过IDA逆向 sgfscrypt.sys 的 fscrypt_find_device_by_tag 得到: + // 驱动用 RtlCompareUnicodeString 将 input.path(跳过前4字节code) 与设备tag做不区分大小写比较 + // 找到后从链表摘除,调用 fscrypt_detach_device 清理: + // FSCTL_LOCK_VOLUME(0x90018) → FSCTL_DISMOUNT_VOLUME(0x90020) → IoDeleteSymbolicLink + // + // 0x000: uint32 buffer_size 声明的缓冲区大小,必须 >= 8 + // 0x004: wchar_t path[258] NT路径,如 \??\FscryptDisk_APP_1\ . bool unmount_container(const std::wstring &tag) { - HANDLE hDriver = CreateFileW(L"\\\\.\\Fscrypt", GENERIC_READ | GENERIC_WRITE, - 0, nullptr, OPEN_EXISTING, 0x80, nullptr); + HANDLE hDriver = CreateFileW( + L"\\\\.\\Fscrypt", + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + OPEN_EXISTING, + 0x80, + nullptr + ); if (hDriver == INVALID_HANDLE_VALUE) { - LOG_ERROR("Open Fscrypt Driver Failed: {}", GetLastError()); + LOG_ERROR("Open Fscrypt Driver Failed: {}", utils::last_error_str()); return false; } struct UnmountInBuffer { - int code; - wchar_t path[258]; + uint32_t buffer_size; // 0x000 + wchar_t path[258]; // 0x004 }; UnmountInBuffer in_buf{}; memset(&in_buf, 0, sizeof(in_buf)); - in_buf.code = 516; // 0x204 + in_buf.buffer_size = sizeof(in_buf); // 0x204 - // Build path: \??\FscryptDisk_ (no trailing backslash in comment) + // Build path: \??\FscryptDisk_\ (带尾部反斜杠,驱动用此字符串做Unicode比较匹配设备tag) std::wstring nt_path = std::format(L"\\??\\FscryptDisk_{}\\", tag); wcsncpy_s(in_buf.path, nt_path.c_str(), 257); - uint8_t out_buf[1024] = {}; + // UNMOUNT 无输出缓冲区,驱动不返回任何数据 DWORD bytes_returned = 0; if (DeviceIoControl(hDriver, IOCTL_FSCRYPT_UNMOUNT, &in_buf, sizeof(in_buf), - out_buf, sizeof(out_buf), + nullptr, 0, &bytes_returned, nullptr)) { CloseHandle(hDriver); return true; } - LOG_ERROR("UNMOUNT failed: Error = {}", GetLastError()); + LOG_ERROR("UNMOUNT failed: {}", utils::last_error_str()); CloseHandle(hDriver); return false; } @@ -298,9 +336,15 @@ namespace fscrypt { bool link_container(const std::wstring &tag, const std::wstring &link_path) { // Verify device exists: \\.\FscryptDisk_ std::wstring device_path = std::format(L"\\\\.\\FscryptDisk_{}", tag); - HANDLE hCheck = CreateFileW(device_path.c_str(), 0, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); + HANDLE hCheck = CreateFileW( + device_path.c_str(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr + ); if (hCheck == INVALID_HANDLE_VALUE) { LOG_ERROR("LINK failed: Non-existent device"); return false; @@ -315,44 +359,47 @@ namespace fscrypt { for (auto &c: target) if (c == L'/') c = L'\\'; while (target.size() > 3 && target.back() == L'\\') target.pop_back(); - // 创建重解析点 - // REPARSE_DATA_BUFFER结构(共16字节头部): - // 0x00: ReparseTag (4字节) - // 0x04: ReparseDataLength (2字节) - // 0x06: Reserved (2字节) - // 0x08: SubstituteNameOffset (2字节) - // 0x0A: SubstituteNameLength (2字节) - // 0x0C: PrintNameOffset (2字节) - // 0x0E: PrintNameLength (2字节) - // 0x10: PathBuffer (变长) - size_t sub_len = source.size(); - size_t print_len = target.size(); - size_t buf_size = 16 + (sub_len + 1) * 2 + (print_len + 1) * 2; + // 创建重解析点(Junction),使用本地定义的 mount point reparse 结构体 + // PathBuffer 布局: [substitute_name\0 print_name\0] +#pragma pack(push, 1) + struct MountPointReparseBuffer { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; // 变长,实际大小由 buf_size 决定 + }; + struct ReparseDataBuffer { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + MountPointReparseBuffer MountPointReparseBuffer; + }; +#pragma pack(pop) + + size_t sub_chars = source.size(); + size_t print_chars = target.size(); + size_t header_size = offsetof(ReparseDataBuffer, MountPointReparseBuffer.PathBuffer); + size_t buf_size = header_size + (sub_chars + 1 + print_chars + 1) * sizeof(WCHAR); std::vector reparse_buf(buf_size, 0); - // ReparseTag - *reinterpret_cast(reparse_buf.data()) = IO_REPARSE_TAG_MOUNT_POINT; - // ReparseDataLength - *reinterpret_cast(reparse_buf.data() + 4) = static_cast(buf_size - 8); - // Reserved - *reinterpret_cast(reparse_buf.data() + 6) = 0; - // SubstituteNameOffset - *reinterpret_cast(reparse_buf.data() + 8) = 0; - // SubstituteNameLength - *reinterpret_cast(reparse_buf.data() + 10) = static_cast(sub_len * 2); - // PrintNameOffset - *reinterpret_cast(reparse_buf.data() + 12) = static_cast((sub_len + 1) * 2); - // PrintNameLength - *reinterpret_cast(reparse_buf.data() + 14) = static_cast(print_len * 2); + auto *rdb = reinterpret_cast(reparse_buf.data()); - // Copy substitute name (source) - memcpy(reparse_buf.data() + 16, source.c_str(), sub_len * 2); - // null terminator - *reinterpret_cast(reparse_buf.data() + 16 + sub_len * 2) = 0; - // Copy print name (target) - memcpy(reparse_buf.data() + 16 + (sub_len + 1) * 2, target.c_str(), print_len * 2); - // null terminator - *reinterpret_cast(reparse_buf.data() + 16 + (sub_len + 1) * 2 + print_len * 2) = 0; + rdb->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + rdb->ReparseDataLength = static_cast(buf_size - 8); // 减去 8 字节头部(Tag+DataLength+Reserved) + rdb->Reserved = 0; + + auto &mp = rdb->MountPointReparseBuffer; + mp.SubstituteNameOffset = 0; + mp.SubstituteNameLength = static_cast(sub_chars * sizeof(WCHAR)); + mp.PrintNameOffset = static_cast((sub_chars + 1) * sizeof(WCHAR)); + mp.PrintNameLength = static_cast(print_chars * sizeof(WCHAR)); + + // Copy substitute name (source) + null, print name (target) + null + memcpy(mp.PathBuffer, source.c_str(), sub_chars * sizeof(WCHAR)); + mp.PathBuffer[sub_chars] = 0; + memcpy(mp.PathBuffer + sub_chars + 1, target.c_str(), print_chars * sizeof(WCHAR)); + mp.PathBuffer[sub_chars + 1 + print_chars] = 0; // 递归创建目录及其父目录(CreateDirectoryW无法递归创建父目录) namespace fs = std::filesystem; @@ -365,12 +412,17 @@ namespace fscrypt { } // Open the directory - HANDLE hDir = CreateFileW(link_path.c_str(), GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, - FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + HANDLE hDir = CreateFileW( + link_path.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, + nullptr + ); if (hDir == INVALID_HANDLE_VALUE) { - LOG_ERROR("CreateFileW failed for {}: {}", utils::wstr_to_str(link_path), GetLastError()); + LOG_ERROR("CreateFileW failed for {}: {}", utils::wstr_to_str(link_path), utils::last_error_str()); return false; } @@ -379,7 +431,7 @@ namespace fscrypt { reparse_buf.data(), static_cast(buf_size), nullptr, 0, &bytes_returned, nullptr); if (!ok) { - LOG_ERROR("FSCTL_SET_REPARSE_POINT failed: {}", GetLastError()); + LOG_ERROR("FSCTL_SET_REPARSE_POINT failed: {}", utils::last_error_str()); } CloseHandle(hDir); @@ -403,7 +455,7 @@ namespace fscrypt { // mount point重解析点可直接用RemoveDirectoryW移除(会删除junction而非目标内容) if (!RemoveDirectoryW(path.c_str())) { - LOG_ERROR("RemoveDirectory failed for {}: {}", utils::wstr_to_str(path), GetLastError()); + LOG_ERROR("RemoveDirectory failed for {}: {}", utils::wstr_to_str(path), utils::last_error_str()); return false; } LOG_INFO("Removed link {}", utils::wstr_to_str(path)); diff --git a/src/fscrypt.h b/src/fscrypt.h index 7367d52..99c689c 100644 --- a/src/fscrypt.h +++ b/src/fscrypt.h @@ -9,8 +9,24 @@ namespace fscrypt { // AES密钥和IV已统一至 utils/crypto_constants.h 的 crypto::BOOT_KEY / BOOT_IV - // IOCTL码(FSCTL_SET_REPARSE_POINT和IO_REPARSE_TAG_MOUNT_POINT - // 已在winioctl.h/winnt.h中定义) + // ===== sgfscrypt.sys 驱动 IOCTL 码 (通过IDA逆向分析得到) ===== + // + // 控制设备 \\.\Fscrypt — 容器挂载/卸载管理: + // 0x226040 Ping/IsAlive 检测驱动是否运行,输出 0x103 + // 0x22E000 MOUNT(带盘符) 输入含盘符字符,创建带盘符的磁盘设备 + // 0x22E004 UNMOUNT(按盘符) 输入8字节,按盘符查找并卸载 + // 0x22E008 MOUNT(带Tag) 输入含路径/密钥/IV/Tag,创建无盘符磁盘设备 ★本项目使用 + // 0x22E00C UNMOUNT(按Tag) 输入含NT路径,按Tag字符串匹配卸载 ★本项目使用 + // + // 磁盘设备 \\.\FscryptDisk_ — 响应Windows标准磁盘IOCTL (驱动透明处理): + // 0x70000 GET_DRIVE_GEOMETRY 0x7405C GET_LENGTH_INFO + // 0x70024 IS_WRITABLE 0x74800 CHECK_VERIFY + // 0x2D0800 STORAGE_CHECK_VERIFY2 读写数据时驱动自动做 AES-128-CBC 加解密 + // + // (FSCTL_SET_REPARSE_POINT 和 IO_REPARSE_TAG_MOUNT_POINT 已在 winioctl.h/winnt.h 中定义) + constexpr DWORD IOCTL_FSCRYPT_PING = 0x226040; + constexpr DWORD IOCTL_FSCRYPT_MOUNT_DRIVE = 0x22E000; + constexpr DWORD IOCTL_FSCRYPT_UNMOUNT_DRIVE = 0x22E004; constexpr DWORD IOCTL_FSCRYPT_MOUNT = 0x22E008; constexpr DWORD IOCTL_FSCRYPT_UNMOUNT = 0x22E00C; @@ -51,25 +67,15 @@ namespace fscrypt { const char **image_type, std::string *error = nullptr); - // 密钥覆盖配置(从config.ini读取,hex字符串,空表示用内置密钥表) - struct KeyOverride { - std::string app_key_hex; - std::string app_iv_hex; - std::string opt_key_hex; - std::string opt_iv_hex; - }; - - // 查找AES密钥和IV:优先用override,为空则查内置密钥表 - // 规则:取文件名前4字符作为GameID,.app用AppKey/AppIV,.opt用OptKey/OptIV - bool lookup_key_iv(const std::wstring &container_path, - uint8_t out_key[16], uint8_t out_iv[16], - std::string *error = nullptr, - const KeyOverride *override_keys = nullptr); + // 设置全局密钥覆盖(hex字符串,空则对应类型用内置密钥表) + // 应在mount_container之前调用一次,之后所有挂载均使用此密钥 + void set_key(const std::string &app_key_hex, const std::string &app_iv_hex, + const std::string &opt_key_hex, const std::string &opt_iv_hex); // 挂载容器:读取BootId、解密、识别类型,向驱动发送MOUNT IOCTL + // 密钥来源:若已通过set_key设置则用之,否则查内置密钥表 MountResult mount_container(const std::wstring &container_path, - const std::wstring &tag, - const KeyOverride *override_keys = nullptr); + const std::wstring &tag); // 卸载容器:向驱动发送UNMOUNT IOCTL bool unmount_container(const std::wstring &tag); diff --git a/src/gui/dx11_backend.cpp b/src/gui/dx11_backend.cpp new file mode 100644 index 0000000..1ccdbdc --- /dev/null +++ b/src/gui/dx11_backend.cpp @@ -0,0 +1,94 @@ +#include "dx11_backend.h" +#include "gui_common.h" +#include "imgui.h" + +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +bool CreateDeviceD3D(HWND hWnd) { + DXGI_SWAP_CHAIN_DESC sd; + ZeroMemory(&sd, sizeof(sd)); + sd.BufferCount = 2; + sd.BufferDesc.Width = 0; + sd.BufferDesc.Height = 0; + sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.BufferDesc.RefreshRate.Numerator = 60; + sd.BufferDesc.RefreshRate.Denominator = 1; + sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.OutputWindow = hWnd; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + + UINT createDeviceFlags = 0; + D3D_FEATURE_LEVEL featureLevel; + const D3D_FEATURE_LEVEL featureLevelArray[2] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0}; + HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, + createDeviceFlags, featureLevelArray, 2, + D3D11_SDK_VERSION, &sd, &g_pSwapChain, + &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); + if (res == DXGI_ERROR_UNSUPPORTED) + res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, + createDeviceFlags, featureLevelArray, 2, + D3D11_SDK_VERSION, &sd, &g_pSwapChain, + &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); + if (res != S_OK) + return false; + + CreateRenderTarget(); + return true; +} + +void CleanupDeviceD3D() { + CleanupRenderTarget(); + if (g_pSwapChain) { + g_pSwapChain->Release(); + g_pSwapChain = nullptr; + } + if (g_pd3dDeviceContext) { + g_pd3dDeviceContext->Release(); + g_pd3dDeviceContext = nullptr; + } + if (g_pd3dDevice) { + g_pd3dDevice->Release(); + g_pd3dDevice = nullptr; + } +} + +void CreateRenderTarget() { + ID3D11Texture2D *pBackBuffer; + g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); + g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView); + pBackBuffer->Release(); +} + +void CleanupRenderTarget() { + if (g_mainRenderTargetView) { + g_mainRenderTargetView->Release(); + g_mainRenderTargetView = nullptr; + } +} + +LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + return true; + + switch (msg) { + case WM_SIZE: + if (wParam == SIZE_MINIMIZED) + return 0; + g_ResizeWidth = (UINT) LOWORD(lParam); + g_ResizeHeight = (UINT) HIWORD(lParam); + return 0; + case WM_SYSCOMMAND: + if ((wParam & 0xfff0) == SC_KEYMENU) + return 0; + break; + case WM_DESTROY: + ::PostQuitMessage(0); + return 0; + default: ; + } + return ::DefWindowProcW(hWnd, msg, wParam, lParam); +} diff --git a/src/gui/dx11_backend.h b/src/gui/dx11_backend.h new file mode 100644 index 0000000..1abb0dc --- /dev/null +++ b/src/gui/dx11_backend.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +// DX11 + Win32 后端初始化与清理 +bool CreateDeviceD3D(HWND hWnd); + +void CleanupDeviceD3D(); + +void CreateRenderTarget(); + +void CleanupRenderTarget(); + +LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); diff --git a/src/gui/gui_common.h b/src/gui/gui_common.h new file mode 100644 index 0000000..0323c0a --- /dev/null +++ b/src/gui/gui_common.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include +#include + +// ===== 操作状态 ===== +inline bool g_mounting = false; +inline bool g_unmounting = false; + +// ===== 日志捕获 ===== +inline std::vector g_log_lines; +inline std::mutex g_log_mutex; +inline bool g_auto_scroll = true; +inline bool g_log_capture_enabled = false; + +// ===== DX11 全局变量 ===== +inline ID3D11Device *g_pd3dDevice = nullptr; +inline ID3D11DeviceContext *g_pd3dDeviceContext = nullptr; +inline IDXGISwapChain *g_pSwapChain = nullptr; +inline bool g_SwapChainOccluded = false; +inline UINT g_ResizeWidth = 0, g_ResizeHeight = 0; +inline ID3D11RenderTargetView *g_mainRenderTargetView = nullptr; + +// ===== 驱动状态 ===== +inline bool g_driver_ready = false; diff --git a/src/gui/gui_utils.cpp b/src/gui/gui_utils.cpp new file mode 100644 index 0000000..0c68f62 --- /dev/null +++ b/src/gui/gui_utils.cpp @@ -0,0 +1,48 @@ +#include "gui_utils.h" +#include "gui_common.h" +#include "config.h" +#include "utils/strings.h" +#include +#include +#include +#include + +void load_config_file(const std::wstring &path) { + g_config_path_w = path; + g_config_path = utils::wstr_to_str(path); + std::string err; + if (parse_config(path, g_cfg, &err)) { + g_log_lines.push_back(std::format("[INFO] Config loaded: {}", g_config_path)); + } else { + g_log_lines.push_back(std::format("[ERROR] {}", err)); + } +} + +std::string open_file_dialog(const char *filter_desc, const char *filter_ext) { + wchar_t buf[MAX_PATH] = {}; + OPENFILENAMEW ofn = {}; + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = buf; + ofn.nMaxFile = MAX_PATH; + ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST; + + std::wstring wfilter = utils::to_wstring(std::string(filter_desc) + '\0' + filter_ext + '\0'); + ofn.lpstrFilter = wfilter.c_str(); + + if (GetOpenFileNameW(&ofn)) + return utils::wstr_to_str(buf); + return {}; +} + +std::string open_folder_dialog() { + BROWSEINFOW bi = {}; + bi.hwndOwner = nullptr; + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + LPITEMIDLIST pidl = SHBrowseForFolderW(&bi); + if (!pidl) return {}; + + wchar_t buf[MAX_PATH] = {}; + if (!SHGetPathFromIDListW(pidl, buf)) return {}; + CoTaskMemFree(pidl); + return utils::wstr_to_str(buf); +} diff --git a/src/gui/gui_utils.h b/src/gui/gui_utils.h new file mode 100644 index 0000000..c269494 --- /dev/null +++ b/src/gui/gui_utils.h @@ -0,0 +1,11 @@ +#pragma once +#include + +// 加载配置文件并更新全局 g_cfg / g_config_path +void load_config_file(const std::wstring &path); + +// 打开文件选择对话框,返回 UTF-8 路径,取消则返回空 +std::string open_file_dialog(const char *filter_desc, const char *filter_ext); + +// 打开目录选择对话框,返回 UTF-8 路径,取消则返回空 +std::string open_folder_dialog(); diff --git a/src/gui/main.cpp b/src/gui/main.cpp new file mode 100644 index 0000000..3024d5f --- /dev/null +++ b/src/gui/main.cpp @@ -0,0 +1,148 @@ +// SEGA Mount GUI — ImGui + Win32 + DirectX11 +#define LOG_MODULE "GUI" +#include "gui_common.h" +#include "dx11_backend.h" +#include "gui_utils.h" +#include "ui.h" +#include "theme.h" +#include "imgui.h" +#include "imgui_impl_win32.h" +#include "imgui_impl_dx11.h" +#include "driver_service.h" +#include "utils/files.h" +#include +#include +#include + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) { + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + // 默认加载当前目录的 config.ini + std::wstring default_config = std::format(L"{}\\config.ini", utils::get_cwd()); + load_config_file(default_config); + + // 启动时自动检测驱动 + { + std::string svc_err; + g_driver_ready = driver::ensure_running(&svc_err); + if (g_driver_ready) + g_log_lines.emplace_back("[INFO] Driver service ready"); + else + g_log_lines.push_back(std::format("[ERROR] Driver: {}", svc_err)); + } + + // 初始化 ImGui DPI 感知 + ImGui_ImplWin32_EnableDpiAwareness(); + float main_scale = ImGui_ImplWin32_GetDpiScaleForMonitor( + ::MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY)); + + // 创建 Win32 窗口 + WNDCLASSEXW wc = { + sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, + hInstance, nullptr, nullptr, nullptr, nullptr, + L"SEGA Mount", nullptr + }; + ::RegisterClassExW(&wc); + HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"SEGA Mount GUI", + WS_OVERLAPPEDWINDOW, 100, 100, + (int)(900 * main_scale), (int)(600 * main_scale), + nullptr, nullptr, wc.hInstance, nullptr); + + // 初始化 D3D11 + if (!CreateDeviceD3D(hwnd)) { + CleanupDeviceD3D(); + ::UnregisterClassW(wc.lpszClassName, wc.hInstance); + return 1; + } + + ::ShowWindow(hwnd, SW_SHOWDEFAULT); + ::UpdateWindow(hwnd); + + // 初始化 ImGui + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + (void) io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + // 应用暗色主题(CinderImGui 风格) + apply_dark_theme(); + + // DPI 缩放 + ImGuiStyle &style = ImGui::GetStyle(); + style.ScaleAllSizes(main_scale); + style.FontScaleDpi = main_scale; + + // 加载中文字体 + io.Fonts->AddFontFromFileTTF(R"(c:\Windows\Fonts\msyh.ttc)", 0, nullptr, + io.Fonts->GetGlyphRangesChineseFull()); + + ImGui_ImplWin32_Init(hwnd); + ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); + + // 启用日志捕获 + g_log_capture_enabled = true; + + // 主循环 + bool done = false; + while (!done) { + MSG msg; + while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + if (msg.message == WM_QUIT) + done = true; + } + if (done) + break; + + if (g_SwapChainOccluded && g_pSwapChain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED) { + ::Sleep(10); + continue; + } + g_SwapChainOccluded = false; + + if (g_ResizeWidth != 0 && g_ResizeHeight != 0) { + CleanupRenderTarget(); + g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0); + g_ResizeWidth = g_ResizeHeight = 0; + CreateRenderTarget(); + } + + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + + // 窗口最小化时 DisplaySize 为 0×0,跳过渲染避免 ClipRect 断言失败 + ImGuiIO &frame_io = ImGui::GetIO(); + if (frame_io.DisplaySize.x <= 0.0f || frame_io.DisplaySize.y <= 0.0f) { + ::Sleep(10); + continue; + } + + ImGui::NewFrame(); + + DrawUI(); + + ImGui::Render(); + constexpr float clear_color[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr); + g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + + HRESULT hr = g_pSwapChain->Present(1, 0); + g_SwapChainOccluded = (hr == DXGI_STATUS_OCCLUDED); + } + + // 清理 + g_log_capture_enabled = false; + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + CleanupDeviceD3D(); + ::DestroyWindow(hwnd); + ::UnregisterClassW(wc.lpszClassName, wc.hInstance); + + return 0; +} diff --git a/src/gui/theme.cpp b/src/gui/theme.cpp new file mode 100644 index 0000000..ff37698 --- /dev/null +++ b/src/gui/theme.cpp @@ -0,0 +1,62 @@ +#include "theme.h" +#include "imgui.h" + +// 从 CinderImGui::Options::darkTheme() 提取,适配原生 ImGui +// ModalWindowDarkening → ModalWindowDimBg (ImGui 1.73+ 重命名) +void apply_dark_theme() { + ImGuiStyle &style = ImGui::GetStyle(); + + style.WindowMinSize = ImVec2(160, 20); + style.FramePadding = ImVec2(4, 2); + style.ItemSpacing = ImVec2(6, 2); + style.ItemInnerSpacing = ImVec2(2, 4); + style.Alpha = 0.95f; + style.WindowRounding = 4.0f; + style.FrameRounding = 2.0f; + style.IndentSpacing = 6.0f; + style.ColumnsMinSpacing = 50.0f; + style.GrabMinSize = 14.0f; + style.GrabRounding = 16.0f; + style.ScrollbarSize = 12.0f; + style.ScrollbarRounding = 16.0f; + + ImVec4 *c = style.Colors; + c[ImGuiCol_Text] = ImVec4(0.86f, 0.93f, 0.89f, 0.78f); + c[ImGuiCol_TextDisabled] = ImVec4(0.86f, 0.93f, 0.89f, 0.28f); + c[ImGuiCol_WindowBg] = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + c[ImGuiCol_Border] = ImVec4(0.31f, 0.31f, 1.00f, 0.00f); + c[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + c[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f); + c[ImGuiCol_FrameBgActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.20f, 0.22f, 0.27f, 0.75f); + c[ImGuiCol_TitleBgActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_MenuBarBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.47f); + c[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.09f, 0.15f, 0.16f, 1.00f); + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_CheckMark] = ImVec4(0.71f, 0.22f, 0.27f, 1.00f); + c[ImGuiCol_SliderGrab] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f); + c[ImGuiCol_SliderGrabActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_Button] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f); + c[ImGuiCol_ButtonHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.86f); + c[ImGuiCol_ButtonActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_Header] = ImVec4(0.92f, 0.18f, 0.29f, 0.76f); + c[ImGuiCol_HeaderHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.86f); + c[ImGuiCol_HeaderActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_Separator] = ImVec4(0.14f, 0.16f, 0.19f, 1.00f); + c[ImGuiCol_SeparatorHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f); + c[ImGuiCol_SeparatorActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.77f, 0.83f, 0.04f); + c[ImGuiCol_ResizeGripHovered] = ImVec4(0.92f, 0.18f, 0.29f, 0.78f); + c[ImGuiCol_ResizeGripActive] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_PlotLines] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f); + c[ImGuiCol_PlotLinesHovered] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_PlotHistogram] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f); + c[ImGuiCol_PlotHistogramHovered] = ImVec4(0.92f, 0.18f, 0.29f, 1.00f); + c[ImGuiCol_TextSelectedBg] = ImVec4(0.92f, 0.18f, 0.29f, 0.43f); + c[ImGuiCol_PopupBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.9f); + c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.73f); +} diff --git a/src/gui/theme.h b/src/gui/theme.h new file mode 100644 index 0000000..3da1b2c --- /dev/null +++ b/src/gui/theme.h @@ -0,0 +1,4 @@ +#pragma once + +// 应用 CinderImGui 风格的暗色主题(红色强调) +void apply_dark_theme(); diff --git a/src/gui/ui.cpp b/src/gui/ui.cpp new file mode 100644 index 0000000..6f38621 --- /dev/null +++ b/src/gui/ui.cpp @@ -0,0 +1,245 @@ +#include "ui.h" +#include "gui_common.h" +#include "gui_utils.h" +#include "icf_mount.h" +#include "utils/strings.h" +#include "imgui.h" +#include + +void DrawUI() { + ImGuiViewport *viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + ImGui::Begin("Main", nullptr, flags); + + // ===== 顶栏:配置文件路径 ===== + ImGui::Text("Config:"); + ImGui::SameLine(); + ImGui::InputText("##config_path", g_config_path.data(), g_config_path.capacity() + 1, + ImGuiInputTextFlags_CallbackResize, + [](ImGuiInputTextCallbackData *data) -> int { + auto *s = static_cast(data->UserData); + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { + s->resize(data->BufTextLen); + data->Buf = s->data(); + data->BufSize = static_cast(s->capacity() + 1); + data->BufDirty = true; + } + return 0; + }, + &g_config_path); + ImGui::SameLine(); + if (ImGui::Button("Load")) { + g_config_path_w = utils::to_wstring(g_config_path); + load_config_file(g_config_path_w); + } + ImGui::SameLine(); + if (ImGui::Button("Save")) { + g_config_path_w = utils::to_wstring(g_config_path); + std::string err; + save_config(g_config_path_w, g_cfg, &err); + g_log_lines.emplace_back("[INFO] Config saved"); + } + + ImGui::Separator(); + + // ===== 配置编辑区 ===== + if (ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) { + // 3列布局: label(110) | input(自适应) | button(30) + ImGui::Columns(3, nullptr, false); + float col_label = 110.0f; + float col_button = 30.0f; + float col_input = ImGui::GetWindowWidth() - col_label - col_button - 40.0f; + ImGui::SetColumnWidth(0, col_label); + ImGui::SetColumnWidth(1, col_input); + ImGui::SetColumnWidth(2, col_button); + + // ICF_PATH (file) + static char icf_path_buf[512]; + strncpy_s(icf_path_buf, utils::wstr_to_str(g_cfg.icf_path).c_str(), sizeof(icf_path_buf) - 1); + ImGui::TextUnformatted("ICF Path"); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##icf_path", icf_path_buf, sizeof(icf_path_buf))) + g_cfg.icf_path = utils::to_wstring(icf_path_buf); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + if (ImGui::Button("...##icf_browse")) { + auto sel = open_file_dialog("ICF Files", "*.icf"); + if (!sel.empty()) { + strncpy_s(icf_path_buf, sel.c_str(), sizeof(icf_path_buf) - 1); + g_cfg.icf_path = utils::to_wstring(sel); + } + } + ImGui::NextColumn(); + + // IMAGE_DIR (folder) + static char image_dir_buf[512]; + strncpy_s(image_dir_buf, utils::wstr_to_str(g_cfg.image_dir).c_str(), sizeof(image_dir_buf) - 1); + ImGui::TextUnformatted("Image Dir"); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##image_dir", image_dir_buf, sizeof(image_dir_buf))) + g_cfg.image_dir = utils::to_wstring(image_dir_buf); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + if (ImGui::Button("...##img_browse")) { + auto sel = open_folder_dialog(); + if (!sel.empty()) { + strncpy_s(image_dir_buf, sel.c_str(), sizeof(image_dir_buf) - 1); + g_cfg.image_dir = utils::to_wstring(sel); + } + } + ImGui::NextColumn(); + + // OVERLAY_DIR (folder) + static char overlay_dir_buf[512]; + strncpy_s(overlay_dir_buf, utils::wstr_to_str(g_cfg.overlay_dir).c_str(), sizeof(overlay_dir_buf) - 1); + ImGui::TextUnformatted("Overlay Dir"); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##overlay_dir", overlay_dir_buf, sizeof(overlay_dir_buf))) + g_cfg.overlay_dir = utils::to_wstring(overlay_dir_buf); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + if (ImGui::Button("...##ovr_browse")) { + auto sel = open_folder_dialog(); + if (!sel.empty()) { + strncpy_s(overlay_dir_buf, sel.c_str(), sizeof(overlay_dir_buf) - 1); + g_cfg.overlay_dir = utils::to_wstring(sel); + } + } + ImGui::NextColumn(); + + // MOUNT_LETTER + ImGui::TextUnformatted("Mount Letter"); + ImGui::NextColumn(); + char letter_str[2] = {static_cast(g_cfg.mount_letter), 0}; + ImGui::PushItemWidth(40); + if (ImGui::InputText("##mount_letter", letter_str, 2)) + if (letter_str[0]) g_cfg.mount_letter = static_cast(letter_str[0]); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + ImGui::NextColumn(); // no browse button for letter + + // APP_LINK (folder) + static char app_link_buf[512]; + strncpy_s(app_link_buf, utils::wstr_to_str(g_cfg.app_link).c_str(), sizeof(app_link_buf) - 1); + ImGui::TextUnformatted("App Link"); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##app_link", app_link_buf, sizeof(app_link_buf))) + g_cfg.app_link = utils::to_wstring(app_link_buf); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + if (ImGui::Button("...##app_browse")) { + auto sel = open_folder_dialog(); + if (!sel.empty()) { + strncpy_s(app_link_buf, sel.c_str(), sizeof(app_link_buf) - 1); + g_cfg.app_link = utils::to_wstring(sel); + } + } + ImGui::NextColumn(); + + // OPT_LINK (folder) + static char opt_link_buf[512]; + strncpy_s(opt_link_buf, utils::wstr_to_str(g_cfg.opt_link).c_str(), sizeof(opt_link_buf) - 1); + ImGui::TextUnformatted("Opt Link"); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1); + if (ImGui::InputText("##opt_link", opt_link_buf, sizeof(opt_link_buf))) + g_cfg.opt_link = utils::to_wstring(opt_link_buf); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + if (ImGui::Button("...##opt_browse")) { + auto sel = open_folder_dialog(); + if (!sel.empty()) { + strncpy_s(opt_link_buf, sel.c_str(), sizeof(opt_link_buf) - 1); + g_cfg.opt_link = utils::to_wstring(sel); + } + } + ImGui::NextColumn(); + + ImGui::Columns(1); + } + + ImGui::Separator(); + + // ===== 操作区 ===== + ImGui::Text("Driver: "); + ImGui::SameLine(); + if (g_driver_ready) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::Text("Ready"); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Not Running"); + ImGui::PopStyleColor(); + } + + ImGui::SameLine(); + if (ImGui::Button(g_driver_ready ? "Mount" : "Mount (need driver)")) { + if (g_driver_ready && !g_mounting) { + g_mounting = true; + g_log_lines.emplace_back("[INFO] Starting mount..."); + auto result = icf_mount::mount_icf_apps(g_cfg); + if (result.success) { + g_log_lines.push_back(std::format("[INFO] Mount OK: {} APPs, {} OPTs", + result.apps.size(), result.opts.size())); + } else { + g_log_lines.push_back(std::format("[ERROR] Mount failed: {}", result.error)); + } + g_mounting = false; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Unmount")) { + if (g_driver_ready && !g_unmounting) { + g_unmounting = true; + g_log_lines.emplace_back("[INFO] Starting unmount..."); + auto result = icf_mount::unmount_icf_apps(g_cfg); + if (result.success) { + g_log_lines.push_back(std::format("[INFO] Unmount OK: {} APPs, {} OPTs", + result.apps.size(), result.opts.size())); + } else { + g_log_lines.push_back(std::format("[ERROR] Unmount failed: {}", result.error)); + } + g_unmounting = false; + } + } + + ImGui::Separator(); + + // ===== 日志面板 ===== + ImGui::Checkbox("Auto-scroll", &g_auto_scroll); + ImGui::SameLine(); + if (ImGui::Button("Clear Log")) + g_log_lines.clear(); + + ImGui::BeginChild("LogPanel", ImVec2(-1, -1), ImGuiChildFlags_Borders); + { + std::lock_guard lock(g_log_mutex); + for (const auto &line: g_log_lines) { + if (line.find("[ERROR]") != std::string::npos) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + else if (line.find("[WARN]") != std::string::npos) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); + else + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f)); + ImGui::TextUnformatted(line.c_str()); + ImGui::PopStyleColor(); + } + if (g_auto_scroll && !g_log_lines.empty()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + + ImGui::End(); +} diff --git a/src/gui/ui.h b/src/gui/ui.h new file mode 100644 index 0000000..a65ef5e --- /dev/null +++ b/src/gui/ui.h @@ -0,0 +1,4 @@ +#pragma once + +// 主界面绘制 +void DrawUI(); diff --git a/src/icf_mount.cpp b/src/icf_mount.cpp index a690f5c..90ebc59 100644 --- a/src/icf_mount.cpp +++ b/src/icf_mount.cpp @@ -65,12 +65,8 @@ namespace icf_mount { // APP/OPT文件所在目录:优先使用image_dir,否则使用当前目录 std::wstring dir = cfg.image_dir.empty() ? utils::get_cwd() : cfg.image_dir; - // 构造密钥override(config.ini [KEYS] section,空则用内置密钥表) - fscrypt::KeyOverride key_override; - key_override.app_key_hex = cfg.app_key; - key_override.app_iv_hex = cfg.app_iv; - key_override.opt_key_hex = cfg.opt_key; - key_override.opt_iv_hex = cfg.opt_iv; + // 设置全局密钥覆盖(config.ini [KEYS] section,空则用内置密钥表) + fscrypt::set_key(cfg.app_key, cfg.app_iv, cfg.opt_key, cfg.opt_iv); // 依次挂载每个APP容器 bool all_ok = true; @@ -87,7 +83,7 @@ namespace icf_mount { LOG_INFO("Mount part {}: {}", entry->part_index, entry->filename); - auto mount_res = fscrypt::mount_container(app_path, tag, &key_override); + auto mount_res = fscrypt::mount_container(app_path, tag); mount_entry.success = mount_res.success; mount_entry.error = mount_res.error; @@ -140,7 +136,7 @@ namespace icf_mount { std::wstring mount_point = std::format(L"{}:\\", cfg.mount_letter); DeleteVolumeMountPointW(mount_point.c_str()); // 清理可能残留的旧挂载点 if (!SetVolumeMountPointW(mount_point.c_str(), vhd_info.volume_guid.c_str())) { - LOG_ERROR("Assign drive {} failed: Error = {}", drive, GetLastError()); + LOG_ERROR("Assign drive {} failed: {}", drive, utils::last_error_str()); all_ok = false; } else { LOG_INFO("Overlay mounted to {}:", drive); @@ -189,7 +185,7 @@ namespace icf_mount { LOG_INFO("Mount OPT {}: {}", entry->version, entry->filename); - auto mount_res = fscrypt::mount_container(opt_path, opt_tag, &key_override); + auto mount_res = fscrypt::mount_container(opt_path, opt_tag); mount_entry.success = mount_res.success; mount_entry.error = mount_res.error; diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index c329d55..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#define LOG_MODULE "MAIN" -#include "config.h" -#include "driver_service.h" -#include "icf_mount.h" -#include "utils/strings.h" -#include "utils/files.h" -#include "utils/log.h" -#include -#include - -// 解析config.ini,使用Windows系统API GetPrivateProfileStringW -static bool parse_config(const std::wstring &path, Config &cfg, std::string *error) { - if (GetFileAttributesW(path.c_str()) == INVALID_FILE_ATTRIBUTES) { - *error = "Config file not found: " + utils::wstr_to_str(path); - return false; - } - - wchar_t buf[512]; - - // [CONFIG] section - auto read_w = [&](const wchar_t *key) -> std::wstring { - DWORD len = GetPrivateProfileStringW(L"CONFIG", key, L"", buf, _countof(buf), path.c_str()); - return {buf, len}; - }; - - cfg.icf_path = read_w(L"ICF_PATH"); - cfg.image_dir = read_w(L"IMAGE_DIR"); - cfg.overlay_dir = read_w(L"OVERLAY_DIR"); - - std::wstring letter = read_w(L"MOUNT_LETTER"); - if (!letter.empty()) { - wchar_t ch = letter[0]; - if (ch >= L'a' && ch <= L'z') ch -= L'a' - L'A'; - cfg.mount_letter = ch; - } - - cfg.app_link = read_w(L"APP_LINK"); - cfg.opt_link = read_w(L"OPT_LINK"); - - // [KEYS] section - auto read_a = [&](const wchar_t *key) -> std::string { - DWORD len = GetPrivateProfileStringW(L"KEYS", key, L"", buf, _countof(buf), path.c_str()); - return utils::wstr_to_str(std::wstring(buf, len)); - }; - - cfg.app_key = read_a(L"APP_KEY"); - cfg.app_iv = read_a(L"APP_IV"); - cfg.opt_key = read_a(L"OPT_KEY"); - cfg.opt_iv = read_a(L"OPT_IV"); - - return true; -} - -static void print_usage() { - std::fputs( - "SEGA MOUNT\n" - "Usage:\n" - " sega_mount -m [config.ini] Mount\n" - " sega_mount -u [config.ini] Unmount\n", - stdout - ); - std::fflush(stdout); -} - -int main(int argc, char *argv[]) { - // 设置控制台为UTF-8代码页,配合/utf-8编译选项避免中文乱码 - SetConsoleOutputCP(CP_UTF8); - SetConsoleCP(CP_UTF8); - - if (argc < 2) { - print_usage(); - return 1; - } - - // 解析命令:-m 挂载,-u 卸载 - std::string cmd = argv[1]; - bool is_mount = cmd == "-m"; - bool is_unmount = cmd == "-u"; - if (!is_mount && !is_unmount) { - LOG_ERROR("Unknown command: {}", cmd); - print_usage(); - return 1; - } - - // 确定config.ini路径:优先命令行参数,否则使用当前运行目录 - std::wstring config_path; - if (argc >= 3) { - config_path = utils::to_wstring(argv[2]); - } else { - config_path = std::format(L"{}\\config.ini", utils::get_cwd()); - } - - // 解析config.ini - Config cfg; - std::string cfg_err; - if (!parse_config(config_path, cfg, &cfg_err)) { - LOG_ERROR("{}", cfg_err); - return 1; - } - - if (cfg.icf_path.empty()) { - LOG_ERROR("ICF_PATH is empty in config"); - return 1; - } - - LOG_INFO("Config loaded from {}", utils::wstr_to_str(config_path)); - LOG_INFO(" ICF_PATH: {}", utils::wstr_to_str(cfg.icf_path)); - LOG_INFO(" IMAGE_DIR: {}", utils::wstr_to_str(cfg.image_dir)); - LOG_INFO(" OVERLAY_DIR: {}", utils::wstr_to_str(cfg.overlay_dir)); - LOG_INFO(" MOUNT_LETTER: {}", static_cast(cfg.mount_letter)); - LOG_INFO(" APP_LINK: {}", utils::wstr_to_str(cfg.app_link)); - LOG_INFO(" OPT_LINK: {}", utils::wstr_to_str(cfg.opt_link)); - LOG_INFO(" APP_KEY: {}", cfg.app_key.empty() ? "(use built-in table)" : "(override)"); - LOG_INFO(" OPT_KEY: {}", cfg.opt_key.empty() ? "(use built-in table)" : "(override)"); - - // 确保sgfscrypt服务已安装并运行 - std::string svc_err; - if (!driver::ensure_running(&svc_err)) { - LOG_ERROR("Failed to start driver service: {}", svc_err); - LOG_ERROR("Hint: run this program as Administrator"); - return 1; - } - - if (is_mount) { - auto result = icf_mount::mount_icf_apps(cfg); - if (!result.success) { - LOG_ERROR("Mount failed: {}", result.error); - return 1; - } - LOG_INFO("Mount Success: {} APPs, {} OPTs mounted", - result.apps.size(), result.opts.size()); - return 0; - } - - // is_unmount - auto result = icf_mount::unmount_icf_apps(cfg); - if (!result.success) { - LOG_ERROR("Unmount failed: {}", result.error); - return 1; - } - LOG_INFO("Unmount Success: {} APPs, {} OPTs unmounted", - result.apps.size(), result.opts.size()); - return 0; -} diff --git a/src/utils/strings.cpp b/src/utils/strings.cpp index 4c98b42..b67f7d0 100644 --- a/src/utils/strings.cpp +++ b/src/utils/strings.cpp @@ -65,4 +65,51 @@ namespace utils { } return s; } + + // 将 Windows 错误码转为可读字符串: "5: Access is denied." + std::string last_error_str(DWORD code) { + // 用 FormatMessage 取系统英文描述文本 + LPWSTR msg_buf = nullptr; + DWORD msg_len = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, code, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + reinterpret_cast(&msg_buf), 0, nullptr); + + std::string desc; + if (msg_len > 0 && msg_buf) { + // 直接用 WideCharToMultiByte 转 UTF-8 + int utf8_len = WideCharToMultiByte( + CP_UTF8, + 0, + msg_buf, + static_cast(msg_len), + nullptr, + 0, + nullptr, + nullptr + ); + if (utf8_len > 0) { + desc.resize(utf8_len); + WideCharToMultiByte( + CP_UTF8, + 0, + msg_buf, + static_cast(msg_len), + desc.data(), + utf8_len, + nullptr, + nullptr + ); + } + // 去掉末尾的换行 + while (!desc.empty() && (desc.back() == '\r' || desc.back() == '\n' || desc.back() == ' ')) + desc.pop_back(); + } + LocalFree(msg_buf); + + // 组合: "5: Access is denied." + if (!desc.empty()) + return std::format("{}: {}", code, desc); + return std::format("{}", code); + } } diff --git a/src/utils/strings.h b/src/utils/strings.h index cbfbc25..0a04576 100644 --- a/src/utils/strings.h +++ b/src/utils/strings.h @@ -25,4 +25,8 @@ namespace utils { // 将字节数组格式化为十六进制小写字符串 std::string bytes_to_hex(const uint8_t *data, size_t len); + + // 将 Windows GetLastError() 的错误码转为可读字符串 + // 格式: "5 (ERROR_ACCESS_DENIED)" — 数字 + 符号名 + std::string last_error_str(DWORD code = GetLastError()); } diff --git a/tests/test_mount_opt.cpp b/tests/test_mount_opt.cpp new file mode 100644 index 0000000..b2f0808 --- /dev/null +++ b/tests/test_mount_opt.cpp @@ -0,0 +1,157 @@ +// 单元测试:挂载/卸载/链接/取消链接 OPT 容器 +// 需要管理员权限运行(驱动安装+挂载都需要) +// +// 用法: +// test_mount_opt mount 挂载容器,验证设备可访问 +// test_mount_opt unmount 卸载容器,验证设备已消失 +// test_mount_opt link 为已挂载的卷创建目录链接(Junction) +// test_mount_opt unlink 移除目录链接(Junction) +// +// 示例: +// test_mount_opt mount "C:\Users\aiyah\Desktop\SDGA_A011_20260716114938_0.opt" OPT_TEST +// test_mount_opt link OPT_TEST "C:\mnt\opt_test" +// test_mount_opt unlink "C:\mnt\opt_test" +// test_mount_opt unmount OPT_TEST +#define LOG_MODULE "TEST" +#include "fscrypt.h" +#include "driver_service.h" +#include "utils/strings.h" +#include "utils/log.h" +#include +#include +#include +#include + +static void print_usage() { + puts("Usage:"); + puts(" test_mount_opt mount Mount container"); + puts(" test_mount_opt unmount Unmount container"); + puts(" test_mount_opt link Link device to directory"); + puts(" test_mount_opt unlink Remove directory link"); +} + +// 确保驱动服务运行 +static bool ensure_driver() { + std::string svc_err; + if (!driver::ensure_running(&svc_err)) { + LOG_ERROR("driver::ensure_running failed: {}", svc_err); + LOG_ERROR("Hint: run this program as Administrator"); + return false; + } + LOG_INFO("Driver service ready"); + return true; +} + +// 验证设备 \\.\FscryptDisk_ 是否可访问 +static bool check_device(const std::wstring &tag, bool should_exist) { + std::wstring device_path = std::format(L"\\\\.\\FscryptDisk_{}", tag); + HANDLE hCheck = CreateFileW(device_path.c_str(), 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + bool exists = (hCheck != INVALID_HANDLE_VALUE); + if (exists) CloseHandle(hCheck); + + if (should_exist && !exists) { + LOG_ERROR("Device {} not accessible: {}", utils::wstr_to_str(device_path), GetLastError()); + return false; + } + if (!should_exist && exists) { + LOG_ERROR("Device {} still exists after unmount!", utils::wstr_to_str(device_path)); + return false; + } + if (should_exist) { + LOG_INFO("Device {} accessible", utils::wstr_to_str(device_path)); + } else { + LOG_INFO("Device {} removed", utils::wstr_to_str(device_path)); + } + return true; +} + +// ---- 子命令实现 ---- + +static int cmd_mount(int argc, char *argv[]) { + if (argc < 4) { print_usage(); return 1; } + std::wstring opt_path = utils::to_wstring(argv[2]); + std::wstring tag = utils::to_wstring(argv[3]); + + if (!ensure_driver()) return 1; + + LOG_INFO("Mounting '{}' as tag '{}' ...", utils::wstr_to_str(opt_path), utils::wstr_to_str(tag)); + auto result = fscrypt::mount_container(opt_path, tag); + if (!result.success) { + LOG_ERROR("Mount failed: {}", result.error); + return 1; + } + LOG_INFO("Mount succeeded"); + + if (!check_device(tag, true)) return 1; + + LOG_INFO("=== MOUNT OK ==="); + return 0; +} + +static int cmd_unmount(int argc, char *argv[]) { + if (argc < 3) { print_usage(); return 1; } + std::wstring tag = utils::to_wstring(argv[2]); + + if (!ensure_driver()) return 1; + + LOG_INFO("Unmounting tag '{}' ...", utils::wstr_to_str(tag)); + if (!fscrypt::unmount_container(tag)) { + LOG_ERROR("Unmount failed"); + return 1; + } + LOG_INFO("Unmount succeeded"); + + if (!check_device(tag, false)) return 1; + + LOG_INFO("=== UNMOUNT OK ==="); + return 0; +} + +static int cmd_link(int argc, char *argv[]) { + if (argc < 4) { print_usage(); return 1; } + std::wstring tag = utils::to_wstring(argv[2]); + std::wstring link_dir = utils::to_wstring(argv[3]); + + LOG_INFO("Linking tag '{}' -> '{}' ...", utils::wstr_to_str(tag), utils::wstr_to_str(link_dir)); + if (!fscrypt::link_container(tag, link_dir)) { + LOG_ERROR("Link failed"); + return 1; + } + LOG_INFO("=== LINK OK ==="); + return 0; +} + +static int cmd_unlink(int argc, char *argv[]) { + if (argc < 3) { print_usage(); return 1; } + std::wstring link_dir = utils::to_wstring(argv[2]); + + LOG_INFO("Unlinking '{}' ...", utils::wstr_to_str(link_dir)); + if (!fscrypt::unlink_container(link_dir)) { + LOG_ERROR("Unlink failed"); + return 1; + } + LOG_INFO("=== UNLINK OK ==="); + return 0; +} + +int main(int argc, char *argv[]) { + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + if (argc < 2) { + print_usage(); + return 1; + } + + std::string cmd = argv[1]; + if (cmd == "mount") return cmd_mount(argc, argv); + if (cmd == "unmount") return cmd_unmount(argc, argv); + if (cmd == "link") return cmd_link(argc, argv); + if (cmd == "unlink") return cmd_unlink(argc, argv); + + LOG_ERROR("Unknown command: {}", cmd); + print_usage(); + return 1; +}