优化fscrypt,添加GUI目标

This commit is contained in:
MoeGrid
2026-08-01 16:32:29 +08:00
parent 3d316146ee
commit bc4714234f
24 changed files with 1507 additions and 319 deletions
+145
View File
@@ -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'
+5 -5
View File
@@ -47,11 +47,11 @@ sgfscrypt.sys
sgfscrypt.inf
# ===== 项目私有目录/文件 =====
driver/
refer/
overlay/
check/
note.txt
/driver/
/refer/
/overlay/
/check/
/note.txt
# ===== Windows 系统文件 =====
Thumbs.db
+80 -12
View File
@@ -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 ()
+100
View File
@@ -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 <windows.h>
#include <string>
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<char>(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;
}
+69
View File
@@ -0,0 +1,69 @@
#include "config.h"
#include "utils/strings.h"
#include <windows.h>
// 全局实例定义
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;
}
+13
View File
@@ -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);
+31 -12
View File
@@ -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;
+171 -119
View File
@@ -10,8 +10,24 @@
#include <filesystem>
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<uint8_t>(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_<tag>
//
// 输出缓冲区: 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: \??\<normalized_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_<tag>
// 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_<tag>\ .
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_<tag> (no trailing backslash in comment)
// Build path: \??\FscryptDisk_<tag>\ (带尾部反斜杠,驱动用此字符串做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_<tag>
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<uint8_t> reparse_buf(buf_size, 0);
// ReparseTag
*reinterpret_cast<DWORD *>(reparse_buf.data()) = IO_REPARSE_TAG_MOUNT_POINT;
// ReparseDataLength
*reinterpret_cast<WORD *>(reparse_buf.data() + 4) = static_cast<WORD>(buf_size - 8);
// Reserved
*reinterpret_cast<WORD *>(reparse_buf.data() + 6) = 0;
// SubstituteNameOffset
*reinterpret_cast<WORD *>(reparse_buf.data() + 8) = 0;
// SubstituteNameLength
*reinterpret_cast<WORD *>(reparse_buf.data() + 10) = static_cast<WORD>(sub_len * 2);
// PrintNameOffset
*reinterpret_cast<WORD *>(reparse_buf.data() + 12) = static_cast<WORD>((sub_len + 1) * 2);
// PrintNameLength
*reinterpret_cast<WORD *>(reparse_buf.data() + 14) = static_cast<WORD>(print_len * 2);
auto *rdb = reinterpret_cast<ReparseDataBuffer *>(reparse_buf.data());
// Copy substitute name (source)
memcpy(reparse_buf.data() + 16, source.c_str(), sub_len * 2);
// null terminator
*reinterpret_cast<WCHAR *>(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<WCHAR *>(reparse_buf.data() + 16 + (sub_len + 1) * 2 + print_len * 2) = 0;
rdb->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
rdb->ReparseDataLength = static_cast<USHORT>(buf_size - 8); // 减去 8 字节头部(Tag+DataLength+Reserved)
rdb->Reserved = 0;
auto &mp = rdb->MountPointReparseBuffer;
mp.SubstituteNameOffset = 0;
mp.SubstituteNameLength = static_cast<USHORT>(sub_chars * sizeof(WCHAR));
mp.PrintNameOffset = static_cast<USHORT>((sub_chars + 1) * sizeof(WCHAR));
mp.PrintNameLength = static_cast<USHORT>(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<DWORD>(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));
+24 -18
View File
@@ -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_<tag> — 响应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);
+94
View File
@@ -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);
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <d3d11.h>
#include <windows.h>
// DX11 + Win32 后端初始化与清理
bool CreateDeviceD3D(HWND hWnd);
void CleanupDeviceD3D();
void CreateRenderTarget();
void CleanupRenderTarget();
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <d3d11.h>
// ===== 操作状态 =====
inline bool g_mounting = false;
inline bool g_unmounting = false;
// ===== 日志捕获 =====
inline std::vector<std::string> 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;
+48
View File
@@ -0,0 +1,48 @@
#include "gui_utils.h"
#include "gui_common.h"
#include "config.h"
#include "utils/strings.h"
#include <windows.h>
#include <commdlg.h>
#include <shlobj.h>
#include <format>
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);
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
// 加载配置文件并更新全局 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();
+148
View File
@@ -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 <tchar.h>
#include <windows.h>
#include <format>
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;
}
+62
View File
@@ -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);
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
// 应用 CinderImGui 风格的暗色主题(红色强调)
void apply_dark_theme();
+245
View File
@@ -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 <format>
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<std::string *>(data->UserData);
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
s->resize(data->BufTextLen);
data->Buf = s->data();
data->BufSize = static_cast<int>(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<char>(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<wchar_t>(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<std::mutex> 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();
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
// 主界面绘制
void DrawUI();
+5 -9
View File
@@ -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;
-144
View File
@@ -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 <windows.h>
#include <string>
// 解析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<char>(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;
}
+47
View File
@@ -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<LPWSTR>(&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<int>(msg_len),
nullptr,
0,
nullptr,
nullptr
);
if (utf8_len > 0) {
desc.resize(utf8_len);
WideCharToMultiByte(
CP_UTF8,
0,
msg_buf,
static_cast<int>(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);
}
}
+4
View File
@@ -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());
}
+157
View File
@@ -0,0 +1,157 @@
// 单元测试:挂载/卸载/链接/取消链接 OPT 容器
// 需要管理员权限运行(驱动安装+挂载都需要)
//
// 用法:
// test_mount_opt mount <opt_path> <tag> 挂载容器,验证设备可访问
// test_mount_opt unmount <tag> 卸载容器,验证设备已消失
// test_mount_opt link <tag> <link_dir> 为已挂载的卷创建目录链接(Junction)
// test_mount_opt unlink <link_dir> 移除目录链接(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 <windows.h>
#include <cstdio>
#include <string>
#include <format>
static void print_usage() {
puts("Usage:");
puts(" test_mount_opt mount <opt_path> <tag> Mount container");
puts(" test_mount_opt unmount <tag> Unmount container");
puts(" test_mount_opt link <tag> <link_dir> Link device to directory");
puts(" test_mount_opt unlink <link_dir> 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_<tag> 是否可访问
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;
}