Add full implementation: core engine, UI, build fixes, and compilation

- Implement all core modules: disk I/O, partition tables, filesystem
  formatting, recovery, imaging, diagnostics, security, and maintenance
- Implement all UI tabs with full widget layouts and backend integration
- Fix MSVC compilation: NOMINMAX, WIN32_LEAN_AND_MEAN, missing includes
  (winioctl.h, bcrypt.h, shellapi.h, cwctype), type mismatches, and
  POSIX macro conflicts
- Add Guid implementation (Types.cpp), move DiskAccessMode to Types.h
- Add CMake presets with embedded MSVC/SDK environment for Git Bash builds
- Add build scripts, key generation, icon resources, and windeployqt
- Include pre-built hwdiag library and third-party integration
This commit is contained in:
DigiJ
2026-03-11 22:48:12 -07:00
parent 179bb85c4f
commit 8656efda63
104 changed files with 33270 additions and 334 deletions

14
.gitignore vendored
View File

@@ -9,3 +9,17 @@ build/
*.opendb
CMakeUserPresets.json
compile_commands.json
puzzle.md
# Build-time generated files
resources/garbage.xtx
# Secret module source — only pre-compiled .lib is committed
src/ui/dialogs/AstroChicken.*
src/ui/dialogs/Vohaul.*
src/ui/dialogs/Arnoid.*
src/ui/tabs/StarGenerator.*
src/core/security/OratDecoder.*
third_party/hwdiag/internal/
third_party/hwdiag/build/
third_party/hwdiag/hwdiag_impl.cpp

View File

@@ -7,12 +7,19 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Platform defines — NOMINMAX prevents windows.h from defining min/max macros
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
# Find Qt6
find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
# CMake helpers
include(cmake/CompilerWarnings.cmake)
include(cmake/Version.cmake)
include(cmake/GenerateKey.cmake)
# Tests option (declared before third_party so FetchContent sees it)
option(SPW_BUILD_TESTS "Build tests" ON)
# Third-party dependencies
add_subdirectory(third_party)
@@ -21,7 +28,6 @@ add_subdirectory(third_party)
add_subdirectory(src)
# Tests
option(SPW_BUILD_TESTS "Build tests" ON)
if(SPW_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)

View File

@@ -1,11 +1,34 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 25,
"patch": 0
},
"configurePresets": [
{
"name": "msvc-base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_PREFIX_PATH": "C:/Qt/6.10.0/msvc2022_64",
"CMAKE_MAKE_PROGRAM": "C:/Qt/Tools/Ninja/ninja.exe"
},
"environment": {
"MSVC_VER": "14.44.35207",
"WINSDK_VER": "10.0.26100.0",
"VCDIR": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/$env{MSVC_VER}",
"SDKDIR": "C:/Program Files (x86)/Windows Kits/10",
"PATH": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/$env{MSVC_VER}/bin/Hostx64/x64;C:/Program Files (x86)/Windows Kits/10/bin/$env{WINSDK_VER}/x64;C:/Qt/Tools/Ninja;C:/Program Files/CMake/bin;C:/Qt/6.10.0/msvc2022_64/bin;C:/Windows/System32;C:/Windows;C:/Program Files/Git/cmd;C:/Program Files/Git/usr/bin",
"INCLUDE": "$env{VCDIR}/include;$env{SDKDIR}/Include/$env{WINSDK_VER}/ucrt;$env{SDKDIR}/Include/$env{WINSDK_VER}/um;$env{SDKDIR}/Include/$env{WINSDK_VER}/shared;$env{SDKDIR}/Include/$env{WINSDK_VER}/winrt;$env{SDKDIR}/Include/$env{WINSDK_VER}/cppwinrt",
"LIB": "$env{VCDIR}/lib/x64;$env{SDKDIR}/Lib/$env{WINSDK_VER}/ucrt/x64;$env{SDKDIR}/Lib/$env{WINSDK_VER}/um/x64"
}
},
{
"name": "default",
"displayName": "Default (Debug)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"inherits": "msvc-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
@@ -13,8 +36,7 @@
{
"name": "release",
"displayName": "Release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"inherits": "msvc-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
@@ -22,8 +44,7 @@
{
"name": "relwithdebinfo",
"displayName": "Release with Debug Info",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"inherits": "msvc-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
@@ -32,11 +53,29 @@
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
"configurePreset": "default",
"environment": {
"MSVC_VER": "14.44.35207",
"WINSDK_VER": "10.0.26100.0",
"VCDIR": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207",
"SDKDIR": "C:/Program Files (x86)/Windows Kits/10",
"PATH": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64;C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x64;C:/Qt/Tools/Ninja;C:/Program Files/CMake/bin;C:/Qt/6.10.0/msvc2022_64/bin;C:/Windows/System32;C:/Windows;C:/Program Files/Git/cmd;C:/Program Files/Git/usr/bin",
"INCLUDE": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/include;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/ucrt;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/um;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/shared;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/winrt;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/cppwinrt",
"LIB": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/lib/x64;C:/Program Files (x86)/Windows Kits/10/Lib/10.0.26100.0/ucrt/x64;C:/Program Files (x86)/Windows Kits/10/Lib/10.0.26100.0/um/x64"
}
},
{
"name": "release",
"configurePreset": "release"
"configurePreset": "release",
"environment": {
"MSVC_VER": "14.44.35207",
"WINSDK_VER": "10.0.26100.0",
"VCDIR": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207",
"SDKDIR": "C:/Program Files (x86)/Windows Kits/10",
"PATH": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/bin/Hostx64/x64;C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x64;C:/Qt/Tools/Ninja;C:/Program Files/CMake/bin;C:/Qt/6.10.0/msvc2022_64/bin;C:/Windows/System32;C:/Windows;C:/Program Files/Git/cmd;C:/Program Files/Git/usr/bin",
"INCLUDE": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/include;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/ucrt;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/um;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/shared;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/winrt;C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0/cppwinrt",
"LIB": "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.44.35207/lib/x64;C:/Program Files (x86)/Windows Kits/10/Lib/10.0.26100.0/ucrt/x64;C:/Program Files (x86)/Windows Kits/10/Lib/10.0.26100.0/um/x64"
}
}
],
"testPresets": [

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# Setec Partition Wizard For Windows
A comprehensive disk recovery, repair, flashing, and formatting tool for Windows.
## Features
- **Partition Management** — Create, delete, resize, move, merge, and split partitions
- **Formatting** — NTFS, FAT32/16/12, exFAT, ext2/3/4, Btrfs, XFS, HFS+, APFS (read), ReFS, and legacy filesystems (HPFS, Minix, AmigaFFS, BeOS BFS, and more)
- **Partition Tables** — Full MBR, GPT, and Apple Partition Map support
- **Recovery** — Deleted partition recovery, file carving, MBR/GPT repair
- **Imaging** — Disk/USB/SD card imaging, ISO flashing, disk cloning
- **Diagnostics** — S.M.A.R.T. monitoring, benchmarks, surface scan
- **Security Keys** — FIDO2/WebAuthn programming, encrypted vaults, boot authentication keys
- **Maintenance** — Secure erase (DoD 5220.22-M, Gutmann), boot repair
## Building
```bash
cmake --preset release
cmake --build --preset release
```
Requires:
- CMake 3.25+
- Qt 6
- MSVC (Visual Studio 2022+)
- Windows 10/11 x64
## License
Copyright (c) 2026 Setec
Don't forget to look UP UP at space,

104
build.bat Normal file
View File

@@ -0,0 +1,104 @@
@echo off
setlocal EnableDelayedExpansion
:: ============================================================
:: Setec Partition Wizard — Build Script
:: Manually sets MSVC x64 environment (no vcvars dependency)
:: ============================================================
set "MSVC_VER=14.44.35207"
set "WINSDK_VER=10.0.26100.0"
set "VSDIR=C:\Program Files\Microsoft Visual Studio\2022\Professional"
set "VCDIR=%VSDIR%\VC\Tools\MSVC\%MSVC_VER%"
set "SDKDIR=C:\Program Files (x86)\Windows Kits\10"
:: ---- PATH ----
set "PATH=%VCDIR%\bin\Hostx64\x64"
set "PATH=%PATH%;%SDKDIR%\bin\%WINSDK_VER%\x64"
set "PATH=%PATH%;C:\Qt\Tools\Ninja"
set "PATH=%PATH%;C:\Program Files\CMake\bin"
set "PATH=%PATH%;C:\Qt\6.10.0\msvc2022_64\bin"
set "PATH=%PATH%;C:\Windows\System32;C:\Windows"
set "PATH=%PATH%;C:\Program Files\Git\cmd"
set "PATH=%PATH%;C:\Program Files\Git\usr\bin"
:: ---- INCLUDE ----
set "INCLUDE=%VCDIR%\include"
set "INCLUDE=%INCLUDE%;%SDKDIR%\Include\%WINSDK_VER%\ucrt"
set "INCLUDE=%INCLUDE%;%SDKDIR%\Include\%WINSDK_VER%\um"
set "INCLUDE=%INCLUDE%;%SDKDIR%\Include\%WINSDK_VER%\shared"
set "INCLUDE=%INCLUDE%;%SDKDIR%\Include\%WINSDK_VER%\winrt"
set "INCLUDE=%INCLUDE%;%SDKDIR%\Include\%WINSDK_VER%\cppwinrt"
:: ---- LIB ----
set "LIB=%VCDIR%\lib\x64"
set "LIB=%LIB%;%SDKDIR%\Lib\%WINSDK_VER%\ucrt\x64"
set "LIB=%LIB%;%SDKDIR%\Lib\%WINSDK_VER%\um\x64"
:: ---- Other vars MSVC needs ----
set "LIBPATH=%VCDIR%\lib\x64"
set "Platform=x64"
set "VisualStudioVersion=17.0"
set "VSCMD_ARG_HOST_ARCH=x64"
set "VSCMD_ARG_TGT_ARCH=x64"
:: ---- Verify compiler ----
echo === Setec Partition Wizard Build ===
cl /? >nul 2>&1
if errorlevel 1 (
echo ERROR: cl.exe not found. Check MSVC_VER.
exit /b 1
)
echo Compiler: cl.exe OK
echo.
:: ---- Change to project dir ----
cd /d "%~dp0"
:: ---- Parse arguments ----
set "PRESET=default"
set "ACTION=all"
if not "%1"=="" set "PRESET=%1"
if not "%2"=="" set "ACTION=%2"
if "%ACTION%"=="configure" goto :configure
if "%ACTION%"=="build" goto :build
if "%ACTION%"=="all" goto :all
if "%ACTION%"=="clean" goto :clean
echo Usage: build.bat [preset] [configure^|build^|all^|clean]
exit /b 1
:all
:configure
echo --- CMake Configure (preset: %PRESET%) ---
cmake --preset %PRESET%
if errorlevel 1 (
echo.
echo CONFIGURE FAILED
exit /b 1
)
echo.
if "%ACTION%"=="configure" goto :done
:build
echo --- CMake Build (preset: %PRESET%) ---
cmake --build --preset %PRESET%
if errorlevel 1 (
echo.
echo BUILD FAILED
exit /b 1
)
echo.
goto :done
:clean
echo --- Clean (preset: %PRESET%) ---
if exist "build\%PRESET%" rmdir /s /q "build\%PRESET%"
echo Cleaned build\%PRESET%
goto :done
:done
echo === Done ===
exit /b 0

33
cmake/GenerateKey.cmake Normal file
View File

@@ -0,0 +1,33 @@
# GenerateKey.cmake — Build-time 1337-bit key generation
# Compiles and runs the keygen tool to produce:
# 1. generated/EmbeddedKey.h (compiled into the app)
# 2. resources/garbage.xtx (distributed read-only alongside the app)
set(KEYGEN_SOURCE "${CMAKE_SOURCE_DIR}/tools/keygen.cpp")
set(KEYGEN_BINARY "${CMAKE_BINARY_DIR}/tools/keygen${CMAKE_EXECUTABLE_SUFFIX}")
set(GENERATED_DIR "${CMAKE_BINARY_DIR}/generated")
set(GENERATED_KEY_HEADER "${GENERATED_DIR}/EmbeddedKey.h")
set(GARBAGE_XTX "${CMAKE_SOURCE_DIR}/resources/garbage.xtx")
file(MAKE_DIRECTORY ${GENERATED_DIR})
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/tools")
# Step 1: Compile keygen tool (runs on host at configure/build time)
add_executable(spw_keygen EXCLUDE_FROM_ALL "${KEYGEN_SOURCE}")
if(WIN32)
target_link_libraries(spw_keygen PRIVATE bcrypt)
endif()
set_target_properties(spw_keygen PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools"
)
# Step 2: Run keygen to produce header + garbage.xtx
add_custom_command(
OUTPUT "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}"
COMMAND spw_keygen "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}"
DEPENDS spw_keygen "${KEYGEN_SOURCE}"
COMMENT "Generating 1337-bit cryptographic key and garbage.xtx..."
VERBATIM
)
add_custom_target(generate_key DEPENDS "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}")

198
docs/build.md Normal file
View File

@@ -0,0 +1,198 @@
# Setec Partition Wizard — Build Documentation
> Last updated: 2026-03-11
## Project Overview
**Setec Partition Wizard** is a comprehensive C++17/Qt6 professional disk utility for Windows. It provides partition management, disk cloning, image creation/restore, ISO flashing, file/partition recovery, boot repair, S.M.A.R.T. diagnostics, benchmarking, surface scanning, secure erase, FIDO2 security keys, encrypted vaults, and boot authentication — covering everything that commercial tools like Partition Magic, Acronis, and EaseUS used to offer.
---
## Build Requirements
| Component | Required Version | Location on this system |
|-----------|-----------------|------------------------|
| MSVC (cl.exe) | 2022 (toolset 14.44.35207) | `C:\Program Files\Microsoft Visual Studio\2022\Professional\` |
| Windows SDK | 10.0.26100.0 | `C:\Program Files (x86)\Windows Kits\10\` |
| CMake | 3.25+ | `C:\Program Files\CMake\bin\cmake.exe` |
| Ninja | any | `C:\Qt\Tools\Ninja\ninja.exe` |
| Qt | 6.10.0 (msvc2022_64) | `C:\Qt\6.10.0\msvc2022_64\` |
| Python | 3.x (for icon generation) | `C:\Python314\python.exe` |
### Optional Tools
| Tool | Purpose | Location |
|------|---------|----------|
| w64devkit (GCC 15.2.0) | Alternative compiler | `C:\w64devkit\bin\` |
| Clang (via Qt llvm-mingw) | Alternative compiler | `C:\Qt\Tools\llvm-mingw1706_64\bin\` |
| VS Build Tools | Headless builds | `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\` |
---
## Architecture
```
SetecPartitionWizard/
├── CMakeLists.txt # Root build - finds Qt6, includes cmake/, adds src/
├── CMakePresets.json # Presets with embedded MSVC/SDK environment
├── cmake/
│ ├── CompilerWarnings.cmake # /W4 /permissive- /utf-8 flags
│ ├── Version.cmake # SPW_VERSION_* defines
│ └── GenerateKey.cmake # Builds spw_keygen, generates EmbeddedKey.h + garbage.xtx
├── src/
│ ├── core/ # spw_core static library (28 .cpp files)
│ │ ├── common/ # Types.h, Result.h, Error.h, Constants.h, Logging
│ │ ├── disk/ # RawDiskHandle, VolumeHandle, DiskEnumerator, DiskGeometry,
│ │ │ # SmartReader, PartitionTable, FilesystemDetector, FilesystemInfo
│ │ ├── filesystem/ # FormatEngine (NTFS/FAT/ext/exFAT/swap/Btrfs/XFS)
│ │ ├── operations/ # Operation base, OperationQueue, PartitionOperations (7 op types)
│ │ ├── recovery/ # PartitionRecovery, FileRecovery (MFT/FAT/ext/carving), BootRepair
│ │ ├── diagnostics/ # Benchmark (seq/random R/W, QD1/QD32), SurfaceScan
│ │ ├── imaging/ # Checksums (SHA-256/MD5/CRC32), DiskCloner, ImageCreator,
│ │ │ # ImageRestorer, IsoFlasher (ISO9660 parser + UEFI detection)
│ │ ├── maintenance/ # SecureErase (Zero/DoD-3/7/Gutmann/Random/Custom)
│ │ └── security/ # EncryptedVault (AES-256-XTS/CBC/GCM), Fido2Manager (CTAP2),
│ │ # BootAuthenticator (HMAC-SHA256), OratDecoder
│ ├── ui/ # spw_ui static library
│ │ ├── MainWindow.cpp/h # Tab container, F5=secret menu, Ctrl+R=refresh
│ │ ├── tabs/ # DiskPartitionTab, RecoveryTab, ImagingTab, DiagnosticsTab,
│ │ │ # SecurityTab, MaintenanceTab, StarGenerator
│ │ ├── dialogs/ # AstroChicken, Arnoid, Vohaul (secret menu chain)
│ │ └── widgets/ # DiskMapWidget (visual partition map)
│ └── app/ # SetecPartitionWizard.exe
│ ├── main.cpp # Entry point, single-instance lock
│ └── SingleInstance.cpp/h
├── third_party/ # GTest via FetchContent, hwdiag (secret pentesting module)
├── tests/ # Unit tests
├── tools/ # spw_keygen (build-time key generator)
├── resources/
│ ├── resources.qrc # Qt resource file
│ ├── garbage.xtx # Generated riddle file
│ └── icons/ # app.ico + toolbar PNGs
├── scripts/
│ ├── repair_path.ps1 # PowerShell: repair PATH/INCLUDE/LIB environment
│ └── install_tools.ps1 # PowerShell: install/repair dev tools via winget/choco
└── docs/
├── build.md # This file
└── tool_compilers.md # Complete tool inventory with install instructions
```
### Library Dependencies
**spw_core** links against:
- `Qt6::Core` — QString, QThread, signals/slots
- `setupapi` — SetupDi* device enumeration
- `wbemuuid` — WMI (IWbemServices) for disk info
- `ole32`, `oleaut32` — COM initialization for WMI
- `bcrypt` — SHA-256, AES-256, PBKDF2, HMAC (Windows CNG)
- `ntdll` — LZNT1 compression (RtlCompressBuffer/RtlDecompressBuffer)
- `virtdisk` — VHD mount/unmount (AttachVirtualDisk)
- `hid` — HID device enumeration for FIDO2 USB tokens
**spw_ui** links against:
- `Qt6::Widgets` — QMainWindow, QTabWidget, all UI widgets
- `spw_core` — backend logic
---
## Build Instructions
### Option A: From Developer Command Prompt (Recommended)
```cmd
"C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat"
cd C:\Users\mdavi\SetecPartitionWizard
cmake --preset default
cmake --build build/default
```
### Option B: From Git Bash (requires environment setup)
```bash
# Set MSVC environment (INCLUDE/LIB must use Windows-style paths with semicolons)
export MSVC_VER="14.44.35207"
export WINSDK_VER="10.0.26100.0"
export VCDIR="C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/$MSVC_VER"
export SDKDIR="C:/Program Files (x86)/Windows Kits/10"
# PATH needs MSYS-style /c/ paths
export PATH="/c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/$MSVC_VER/bin/Hostx64/x64:/c/Program Files (x86)/Windows Kits/10/bin/$WINSDK_VER/x64:/c/Qt/Tools/Ninja:/c/Program Files/CMake/bin:/c/Qt/6.10.0/msvc2022_64/bin:/c/Windows/System32:/c/Windows:/c/Program Files/Git/cmd:/c/Program Files/Git/usr/bin"
# INCLUDE and LIB need Windows-style C:/ paths with semicolons
export INCLUDE="$VCDIR/include;$SDKDIR/Include/$WINSDK_VER/ucrt;$SDKDIR/Include/$WINSDK_VER/um;$SDKDIR/Include/$WINSDK_VER/shared;$SDKDIR/Include/$WINSDK_VER/winrt;$SDKDIR/Include/$WINSDK_VER/cppwinrt"
export LIB="$VCDIR/lib/x64;$SDKDIR/Lib/$WINSDK_VER/ucrt/x64;$SDKDIR/Lib/$WINSDK_VER/um/x64"
cmake --preset default
cmake --build build/default
```
### Option C: Using CMake Build Presets
The `CMakePresets.json` has embedded MSVC environment in both configure and build presets:
```bash
cmake --preset default
cmake --build --preset default
```
**Note:** The build preset includes INCLUDE/LIB/PATH so Ninja finds cl.exe and all headers.
### Option D: Run repair_path.ps1 first (fixes system-wide)
```powershell
# In PowerShell (Admin)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.\scripts\repair_path.ps1
# Then restart terminal and build normally
```
---
## Build Troubleshooting History
### Issue: windows.h not found
**Symptom:** `fatal error C1083: Cannot open include file: 'windows.h'`
**Cause:** MSVC's cl.exe relies on the `INCLUDE` environment variable to find system headers. When building from Git Bash, this variable is either not set or gets mangled by MSYS path conversion.
**Fix:** CMakePresets.json now embeds INCLUDE/LIB in both configurePresets and buildPresets. Alternatively, use `repair_path.ps1` to set them system-wide.
### Issue: INCLUDE env var works for configure but not build
**Symptom:** `cmake --preset default` succeeds (cl.exe detected), but `cmake --build build/default` fails with missing headers.
**Cause:** CMake preset `environment` in configurePresets only applies during the configure step. Ninja inherits the shell's environment, not CMake's. The build presets need their own `environment` block.
**Fix:** Added `environment` with INCLUDE/LIB/PATH to buildPresets in CMakePresets.json.
### Issue: Git Bash PATH with spaces
**Symptom:** Commands in directories with spaces fail.
**Cause:** Git Bash/MSYS uses `/c/` style paths. PATH entries must use `/c/Program Files/...` not `C:\Program Files\...`.
**Fix:** PATH uses MSYS-style, INCLUDE/LIB use Windows-style.
### Issue: F5 key conflict
**Symptom:** F5 triggered "Refresh Disks" instead of the secret AstroChicken menu.
**Cause:** `QKeySequence::Refresh` maps to F5 on Windows, intercepting keyPressEvent.
**Fix:** Changed refresh shortcut to `QKeySequence(Qt::CTRL | Qt::Key_R)`. F5 now correctly triggers the secret menu via keyPressEvent.
### Issue: hwdiag CRT mismatch (LNK2038)
**Symptom:** Linker error about mismatched RuntimeLibrary (MDd vs MD).
**Cause:** hwdiag was built as Release (/MD) but main project is Debug (/MDd).
**Fix:** Build hwdiag in Debug mode to match.
### Issue: SPW_BUILD_TESTS option ordering
**Symptom:** GTest never fetched, tests don't build.
**Cause:** `option(SPW_BUILD_TESTS)` was declared AFTER `add_subdirectory(third_party)`.
**Fix:** Moved option declaration before add_subdirectory.
---
## Current Build Status (2026-03-11)
**NOT YET COMPILING.** All ~88 source files (41 .cpp, 47 .h) are written but have never been compiled together successfully. Expected issues:
- Type mismatches between files written by different agents
- Missing includes
- Struct field name inconsistencies
- Interface mismatches between headers and implementations
The MSVC environment issue (INCLUDE/LIB not reaching Ninja) must be resolved first before code-level errors can be addressed. Use `repair_path.ps1` or build from Developer Command Prompt.
---
## Key Design Decisions
1. **No exceptions** — Uses `Result<T>` monadic error handling throughout
2. **No OpenSSL** — All crypto via Windows BCrypt (CNG) API
3. **GParted-style operation queue** — Changes are queued, previewed, then applied atomically
4. **RAII disk handles** — RawDiskHandle and VolumeHandle auto-close on destruction
5. **Removable-only safety** — IsoFlasher refuses to write to fixed disks
6. **Admin required** — Raw disk I/O requires elevation; app checks and prompts
7. **Secret menu** — F5 triggers hidden pentesting module disguised as `libspw_hwdiag` static library

299
docs/tool_compilers.md Normal file
View File

@@ -0,0 +1,299 @@
# SetecPartitionWizard -- Tool & Compiler Inventory
> System scan performed **2026-03-11** on `mdavi` / Windows 10 (build 26200).
## Quick Status
| Tool | Status | Version | Location |
|------|--------|---------|----------|
| MSVC (cl.exe) | FOUND | 14.44.35207 | `C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\` |
| VS Build Tools | FOUND | 17.14.14 | `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\` |
| Windows SDK | FOUND | 10.0.26100.0 | `C:\Program Files (x86)\Windows Kits\10\` |
| CMake | FOUND | (standalone) | `C:\Program Files\CMake\bin\cmake.exe` |
| CMake (Qt) | FOUND | (Qt-bundled) | `C:\Qt\Tools\CMake_64\bin\cmake.exe` |
| CMake (VS) | FOUND | (VS-bundled) | `...\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe` |
| Ninja | FOUND | (Qt-bundled) | `C:\Qt\Tools\Ninja\ninja.exe` |
| Qt 6.10.0 (MSVC) | FOUND | 6.10.0 | `C:\Qt\6.10.0\msvc2022_64\` |
| Qt 6.10.0 (MinGW) | FOUND | 6.10.0 | `C:\Qt\6.10.0\mingw_64\` |
| Qt 6.10.0 (llvm-mingw) | FOUND | 6.10.0 | `C:\Qt\6.10.0\llvm-mingw_64\` |
| Qt 6.10.0 (ARM64) | FOUND | 6.10.0 | `C:\Qt\6.10.0\msvc2022_arm64\` |
| Qt 6.9.2 (MSVC) | FOUND | 6.9.2 | `C:\Qt\6.9.2\msvc2022_64\` |
| Clang (Qt llvm-mingw) | FOUND | 17.x | `C:\Qt\Tools\llvm-mingw1706_64\bin\clang.exe` |
| Clang (standalone LLVM) | NOT FOUND | -- | Expected at `C:\Program Files\LLVM\bin\` |
| clang-cl.exe | NOT FOUND | -- | Not in standalone LLVM or VS LLVM toolset |
| lld-link.exe | NOT FOUND | -- | Not found anywhere |
| GCC (w64devkit) | FOUND | 15.2.0 | `C:\w64devkit\bin\gcc.exe` |
| make (w64devkit) | FOUND | -- | `C:\w64devkit\bin\make.exe` |
| nmake | FOUND | -- | `...\MSVC\14.44.35207\bin\Hostx64\x64\nmake.exe` |
| Python 3.14 | FOUND | 3.14.0rc2 | `C:\Python314\python.exe` |
| Python 3.13 | FOUND | 3.13.7 | `C:\Users\mdavi\AppData\Local\Programs\Python\Python313\python.exe` |
| Git | FOUND | 2.53.0 | `C:\Program Files\Git\cmd\git.exe` |
| Go | FOUND | -- | `C:\Program Files\Go\bin\go.exe` |
| CUDA | FOUND | v13.1 | `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1\` |
| pkg-config | NOT FOUND | -- | Not installed |
| Chocolatey | FOUND | -- | `C:\ProgramData\chocolatey\` |
| GitHub CLI | FOUND | -- | `C:\Program Files\GitHub CLI\` |
| WinGet LLVM-MinGW | FOUND | 20260311 | `...\WinGet\Packages\...\llvm-mingw-20260311-ucrt-x86_64\bin\` |
---
## Detailed Notes Per Tool
### 1. MSVC / Visual Studio
**Status:** FOUND -- two installations detected.
| Installation | Edition | Version | Path |
|---|---|---|---|
| VS 2022 Professional | Professional | 17.14.14 (toolset 14.44.35207) | `C:\Program Files\Microsoft Visual Studio\2022\Professional\` |
| VS 2022 Build Tools | Build Tools | 17.14.14 (toolset 14.44.35207) | `C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\` |
**Key files:**
- `cl.exe`: `C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\cl.exe`
- `link.exe`: same directory
- `nmake.exe`: same directory
- `vcvarsall.bat`: `C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat`
- `vcvars64.bat`: `C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat`
**Cannot be CLI-installed.** Use the Visual Studio Installer:
1. Run `"C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe"`
2. Or download from: https://visualstudio.microsoft.com/downloads/
3. Required workloads:
- "Desktop development with C++"
- Individual components: "MSVC v143 - VS 2022 C++ x64/x86 build tools (Latest)"
- Individual components: "C++ CMake tools for Windows"
- Individual components: "Windows 10/11 SDK (10.0.26100.0)"
### 2. Windows SDK
**Status:** FOUND -- version 10.0.26100.0
**Location:** `C:\Program Files (x86)\Windows Kits\10\`
**Include directories:**
- `...\Include\10.0.26100.0\ucrt\`
- `...\Include\10.0.26100.0\um\`
- `...\Include\10.0.26100.0\shared\`
**Library directories:**
- `...\Lib\10.0.26100.0\ucrt\x64\`
- `...\Lib\10.0.26100.0\um\x64\`
**Binary directory:**
- `...\bin\10.0.26100.0\x64\` (contains `rc.exe`, `mt.exe`, `signtool.exe`)
**Multiple SDK bin versions found** (older ones likely residual):
- 10.0.14393.0, 10.0.15063.0, 10.0.16299.0, 10.0.17134.0, 10.0.26100.0
**Cannot be CLI-installed.** Installed via the Visual Studio Installer as an individual component, or standalone from:
https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
### 3. CMake
**Status:** FOUND at three locations.
| Location | Notes |
|---|---|
| `C:\Program Files\CMake\bin\cmake.exe` | **Standalone install (preferred)** |
| `C:\Qt\Tools\CMake_64\bin\cmake.exe` | Qt-bundled CMake |
| `...\VS 2022\...\CMake\bin\cmake.exe` | VS-bundled CMake |
**CLI install/repair:**
```powershell
winget install Kitware.CMake --override '/FORCE /VERYSILENT /NORESTART /ADD_CMAKE_TO_PATH=System'
```
**Manual download:** https://cmake.org/download/
### 4. Ninja
**Status:** FOUND at `C:\Qt\Tools\Ninja\ninja.exe` (Qt-bundled).
**CLI install (standalone):**
```powershell
winget install Ninja-build.Ninja
```
**Manual download:** https://github.com/nicean/ninja/releases
### 5. Qt Framework
**Status:** FOUND -- multiple kits installed.
**Qt 6.10.0 kits:**
| Kit | Path |
|---|---|
| msvc2022_64 (PRIMARY) | `C:\Qt\6.10.0\msvc2022_64\` |
| msvc2022_arm64 | `C:\Qt\6.10.0\msvc2022_arm64\` |
| mingw_64 | `C:\Qt\6.10.0\mingw_64\` |
| llvm-mingw_64 | `C:\Qt\6.10.0\llvm-mingw_64\` |
**Qt 6.9.2 kits (older):**
| Kit | Path |
|---|---|
| msvc2022_64 | `C:\Qt\6.9.2\msvc2022_64\` |
| msvc2022_arm64 | `C:\Qt\6.9.2\msvc2022_arm64\` |
| Source | `C:\Qt\6.9.2\Src\` |
**Qt Tools:**
- CMake: `C:\Qt\Tools\CMake_64\`
- Ninja: `C:\Qt\Tools\Ninja\`
- MinGW 13.1.0: `C:\Qt\Tools\mingw1310_64\`
- LLVM-MinGW 17.06: `C:\Qt\Tools\llvm-mingw1706_64\`
- Qt Creator: `C:\Qt\Tools\QtCreator\`
- Qt Design Studio: `C:\Qt\Tools\QtDesignStudio-4.8.0-preview\`
- OpenSSL v3: `C:\Qt\Tools\OpenSSLv3\`
- Qt Installer Framework: `C:\Qt\Tools\QtInstallerFramework\`
**Key CMake config:**
- `Qt6Config.cmake`: `C:\Qt\6.10.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake`
**CMake variables to set:**
```
Qt6_DIR=C:\Qt\6.10.0\msvc2022_64\lib\cmake\Qt6
CMAKE_PREFIX_PATH=C:\Qt\6.10.0\msvc2022_64
```
**Cannot be CLI-installed.** Use the Qt Online Installer:
1. Download from: https://www.qt.io/download-qt-installer
2. Sign in with Qt account (free for open-source use)
3. Select: Qt 6.10.0 > MSVC 2022 64-bit
4. Under "Additional Libraries", select any modules your project uses
5. Under "Developer and Designer Tools", ensure CMake and Ninja are checked
### 6. Clang / LLVM
**Status:** PARTIALLY FOUND
| Tool | Status | Location |
|---|---|---|
| clang.exe (Qt llvm-mingw) | FOUND | `C:\Qt\Tools\llvm-mingw1706_64\bin\clang.exe` |
| clang.exe (WinGet) | FOUND | `...\WinGet\...\llvm-mingw-20260311-ucrt-x86_64\bin\` |
| clang.exe (standalone) | NOT FOUND | Expected `C:\Program Files\LLVM\bin\` |
| clang-cl.exe | NOT FOUND | Not found in any location |
| lld-link.exe | NOT FOUND | Not found in any location |
**Note:** The Qt llvm-mingw distribution targets MinGW (GNU) ABI, not MSVC ABI.
For MSVC-compatible Clang (`clang-cl.exe`), install standalone LLVM:
```powershell
winget install LLVM.LLVM --override '/FORCE /VERYSILENT /NORESTART'
```
**Manual download:** https://github.com/llvm/llvm-project/releases
- Choose: `LLVM-XX.X.X-win64.exe`
- During install, select "Add LLVM to the system PATH"
### 7. GCC / MinGW / w64devkit
**Status:** FOUND
| Tool | Version | Location |
|---|---|---|
| gcc.exe | 15.2.0 | `C:\w64devkit\bin\gcc.exe` |
| g++.exe | 15.2.0 | `C:\w64devkit\bin\g++.exe` |
| make.exe | -- | `C:\w64devkit\bin\make.exe` |
| MinGW (Qt) | 13.1.0 | `C:\Qt\Tools\mingw1310_64\bin\` |
**w64devkit** is a self-contained GCC toolchain. Download/update from:
https://github.com/skeeto/w64devkit/releases
**Warning:** Do not mix w64devkit/MinGW-built libraries with MSVC-built libraries.
The SetecPartitionWizard project uses MSVC -- use w64devkit only for standalone
C/C++ utilities, not for building the main Qt application.
### 8. Python
**Status:** FOUND -- two installations.
| Version | Location | Notes |
|---|---|---|
| 3.14.0rc2 | `C:\Python314\python.exe` | Pre-release, manual install |
| 3.13.7 | `C:\Users\mdavi\AppData\Local\Programs\Python\Python313\python.exe` | Standard install, pip available |
**WindowsApps alias detected:** `python` and `python3` in PATH resolve to the
Microsoft Store redirector at `C:\Users\mdavi\AppData\Local\Microsoft\WindowsApps\`.
This may interfere with the real Python installations.
**Fix:** Settings > Apps > Advanced app settings > App execution aliases >
Turn off `python.exe` and `python3.exe`.
**CLI install:**
```powershell
winget install Python.Python.3.13
```
### 9. Git
**Status:** FOUND
- Version: 2.53.0.windows.1
- Location: `C:\Program Files\Git\cmd\git.exe`
**CLI install/update:**
```powershell
winget install Git.Git --override '/VERYSILENT /NORESTART'
```
### 10. Build Helpers
| Tool | Status | Location |
|---|---|---|
| nmake.exe | FOUND | `...\MSVC\14.44.35207\bin\Hostx64\x64\nmake.exe` |
| make.exe | FOUND | `C:\w64devkit\bin\make.exe` (GNU Make) |
| pkg-config | NOT FOUND | Not installed anywhere |
| MSBuild | FOUND (implicit) | Part of VS 2022 Professional |
**To install pkg-config:**
```powershell
choco install pkgconfiglite -y
# or
winget install bloodrock.pkg-config-lite
```
---
## Additional Tools Found (Not Project-Critical)
| Tool | Location |
|---|---|
| Go | `C:\Program Files\Go\bin\go.exe` |
| CUDA v13.1 | `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1\` |
| .NET SDK | `C:\Program Files\dotnet\` |
| GitHub Desktop | `C:\Users\mdavi\AppData\Local\GitHubDesktop\` |
| LM Studio | `C:\Users\mdavi\.lmstudio\bin\` |
| Claude CLI | `C:\Users\mdavi\.local\bin\claude.exe` |
| Metasploit | `C:\metasploit-framework\bin\` |
---
## Recommended Build Command
For the SetecPartitionWizard project using MSVC + Qt 6.10.0 + CMake + Ninja:
```powershell
# Option A: From a VS Developer PowerShell (vcvars already sourced)
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="C:/Qt/6.10.0/msvc2022_64"
cmake --build build
# Option B: From a regular PowerShell (after running repair_path.ps1)
# The INCLUDE, LIB, Qt6_DIR, and CMAKE_PREFIX_PATH env vars are set permanently.
cmake -S . -B build -G Ninja -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl
cmake --build build
```
---
## PATH Issues Detected
1. **Duplicate entries:** The User PATH contains many duplicate entries (the entire System PATH
appears to be duplicated in User PATH). Run `repair_path.ps1` to clean this up.
2. **WindowsApps Python alias:** The Store alias for `python.exe` shadows real Python installs.
Disable via App execution aliases in Settings.
3. **Missing from PATH:** CMake (`C:\Program Files\CMake\bin`) is in the System PATH but
MSVC, Ninja, Qt, and w64devkit are not in either User or System PATH.
4. **Quoted paths:** Some User PATH entries have literal single-quote characters around them
(e.g., `'C:\Users\mdavi\AppData\...\Scripts'`), which may cause resolution failures.

BIN
resources/icons/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

15
resources/resources.qrc Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/">
<file>styles/default.qss</file>
<file>icons/toolbar/refresh.png</file>
<file>icons/toolbar/create.png</file>
<file>icons/toolbar/delete.png</file>
<file>icons/toolbar/resize.png</file>
<file>icons/toolbar/format.png</file>
<file>icons/toolbar/clone.png</file>
<file>icons/toolbar/flash.png</file>
<file>icons/toolbar/apply.png</file>
<file>icons/toolbar/undo.png</file>
</qresource>
</RCC>

286
scripts/install_tools.ps1 Normal file
View File

@@ -0,0 +1,286 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Installs or repairs development tools for the SetecPartitionWizard project
using winget (primary), choco (fallback), and pip.
.DESCRIPTION
For each tool, the script checks whether it is already installed and functional.
If not, it attempts installation via winget, then falls back to Chocolatey.
Python packages are installed via pip after Python is confirmed working.
Run from an elevated PowerShell prompt for best results (some winget/choco
installs require admin).
.NOTES
Generated 2026-03-11 by Goju PATH repair agent.
Tools that CANNOT be CLI-installed (MSVC, Qt, Windows SDK) are documented
in docs/tool_compilers.md with manual install instructions.
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Continue'
# ─────────────────────────────────────────────────────────────
# Helper: test if a command is available
# ─────────────────────────────────────────────────────────────
function Test-CommandExists {
param([string]$Command)
$null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
}
function Write-Step {
param([string]$Name, [string]$Status, [string]$Detail = "")
$color = switch ($Status) {
"FOUND" { "Green" }
"INSTALL" { "Cyan" }
"SKIP" { "DarkGray" }
"FAIL" { "Red" }
"OK" { "Green" }
default { "White" }
}
Write-Host " [$Status] $Name" -ForegroundColor $color -NoNewline
if ($Detail) { Write-Host " -- $Detail" -ForegroundColor DarkGray } else { Write-Host "" }
}
# ─────────────────────────────────────────────────────────────
# Check for package managers
# ─────────────────────────────────────────────────────────────
Write-Host "`n=== SetecPartitionWizard Tool Installer ===" -ForegroundColor Cyan
$HasWinget = Test-CommandExists "winget"
$HasChoco = Test-CommandExists "choco"
if ($HasWinget) { Write-Step "winget" "FOUND" } else { Write-Step "winget" "FAIL" "Not available -- winget installs will be skipped" }
if ($HasChoco) { Write-Step "choco" "FOUND" } else { Write-Step "choco" "SKIP" "Not available -- choco fallback disabled" }
# ─────────────────────────────────────────────────────────────
# 1. CMake
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- CMake ---" -ForegroundColor Yellow
$cmakeExe = "C:\Program Files\CMake\bin\cmake.exe"
if (Test-Path $cmakeExe) {
$ver = & $cmakeExe --version 2>&1 | Select-Object -First 1
Write-Step "CMake" "FOUND" $ver
}
else {
Write-Step "CMake" "INSTALL" "Installing via winget..."
if ($HasWinget) {
winget install Kitware.CMake --accept-package-agreements --accept-source-agreements --override '/FORCE /VERYSILENT /NORESTART /ADD_CMAKE_TO_PATH=System'
}
elseif ($HasChoco) {
choco install cmake --installargs '"ADD_CMAKE_TO_PATH=System"' -y --force
}
else {
Write-Step "CMake" "FAIL" "No package manager available. Download from https://cmake.org/download/"
}
}
# ─────────────────────────────────────────────────────────────
# 2. Ninja
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- Ninja ---" -ForegroundColor Yellow
$ninjaExe = "C:\Qt\Tools\Ninja\ninja.exe"
if (Test-Path $ninjaExe) {
$ver = & $ninjaExe --version 2>&1
Write-Step "Ninja" "FOUND" "v$ver (Qt-bundled)"
}
elseif (Test-CommandExists "ninja") {
Write-Step "Ninja" "FOUND" "in PATH"
}
else {
Write-Step "Ninja" "INSTALL" "Installing via winget..."
if ($HasWinget) {
winget install Ninja-build.Ninja --accept-package-agreements --accept-source-agreements
}
elseif ($HasChoco) {
choco install ninja -y
}
else {
Write-Step "Ninja" "FAIL" "Download from https://github.com/nicean/ninja/releases"
}
}
# ─────────────────────────────────────────────────────────────
# 3. Git
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- Git ---" -ForegroundColor Yellow
if (Test-CommandExists "git") {
$ver = git --version 2>&1
Write-Step "Git" "FOUND" $ver
}
else {
Write-Step "Git" "INSTALL" "Installing via winget..."
if ($HasWinget) {
winget install Git.Git --accept-package-agreements --accept-source-agreements --override '/VERYSILENT /NORESTART'
}
elseif ($HasChoco) {
choco install git -y --force
}
}
# ─────────────────────────────────────────────────────────────
# 4. Python
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- Python ---" -ForegroundColor Yellow
$python314 = "C:\Python314\python.exe"
$python313 = "C:\Users\mdavi\AppData\Local\Programs\Python\Python313\python.exe"
if (Test-Path $python314) {
$ver = & $python314 --version 2>&1
Write-Step "Python 3.14" "FOUND" $ver
}
else {
Write-Step "Python 3.14" "SKIP" "Not found at C:\Python314 -- install manually (pre-release)"
}
if (Test-Path $python313) {
$ver = & $python313 --version 2>&1
Write-Step "Python 3.13" "FOUND" $ver
}
else {
Write-Step "Python 3.13" "INSTALL" "Installing via winget..."
if ($HasWinget) {
winget install Python.Python.3.13 --accept-package-agreements --accept-source-agreements
}
elseif ($HasChoco) {
choco install python313 -y
}
}
# Disable WindowsApps python alias (common source of confusion)
Write-Host "`n TIP: If 'python' opens the Microsoft Store, disable the alias:" -ForegroundColor DarkGray
Write-Host " Settings > Apps > Advanced app settings > App execution aliases" -ForegroundColor DarkGray
Write-Host " Turn off 'python.exe' and 'python3.exe' aliases`n" -ForegroundColor DarkGray
# ─────────────────────────────────────────────────────────────
# 5. LLVM / Clang (standalone)
# ─────────────────────────────────────────────────────────────
Write-Host "--- LLVM / Clang ---" -ForegroundColor Yellow
$llvmPaths = @(
"C:\Program Files\LLVM\bin\clang.exe",
"C:\Qt\Tools\llvm-mingw1706_64\bin\clang.exe"
)
$foundLlvm = $false
foreach ($p in $llvmPaths) {
if (Test-Path $p) {
$ver = & $p --version 2>&1 | Select-Object -First 1
Write-Step "Clang" "FOUND" "$ver ($p)"
$foundLlvm = $true
break
}
}
if (-not $foundLlvm) {
Write-Step "Clang" "INSTALL" "Installing standalone LLVM via winget..."
if ($HasWinget) {
winget install LLVM.LLVM --accept-package-agreements --accept-source-agreements --override '/FORCE /VERYSILENT /NORESTART'
}
elseif ($HasChoco) {
choco install llvm -y --force
}
else {
Write-Step "Clang" "FAIL" "Download from https://github.com/llvm/llvm-project/releases"
}
}
# ─────────────────────────────────────────────────────────────
# 6. GitHub CLI
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- GitHub CLI ---" -ForegroundColor Yellow
if (Test-CommandExists "gh") {
$ver = gh --version 2>&1 | Select-Object -First 1
Write-Step "GitHub CLI" "FOUND" $ver
}
else {
Write-Step "GitHub CLI" "INSTALL" "Installing via winget..."
if ($HasWinget) {
winget install GitHub.cli --accept-package-agreements --accept-source-agreements
}
elseif ($HasChoco) {
choco install gh -y
}
}
# ─────────────────────────────────────────────────────────────
# 7. Python packages (pip)
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- Python packages (pip) ---" -ForegroundColor Yellow
# Find the best available python
$PythonExe = $null
if (Test-Path $python314) { $PythonExe = $python314 }
elseif (Test-Path $python313) { $PythonExe = $python313 }
elseif (Test-CommandExists "python") { $PythonExe = "python" }
if ($PythonExe) {
$pipPackages = @(
"Pillow", # Icon/image generation for the app
"jinja2", # Template engine (useful for code generation)
"pyyaml" # YAML parsing
)
foreach ($pkg in $pipPackages) {
Write-Host " Checking $pkg..." -NoNewline
$installed = & $PythonExe -m pip show $pkg 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " already installed" -ForegroundColor Green
}
else {
Write-Host " installing..." -ForegroundColor Cyan
& $PythonExe -m pip install --user $pkg
}
}
}
else {
Write-Step "pip packages" "SKIP" "No Python found"
}
# ─────────────────────────────────────────────────────────────
# 8. MANUAL-ONLY TOOLS (cannot be CLI-installed)
# ─────────────────────────────────────────────────────────────
Write-Host "`n--- Manual-install tools (verification only) ---" -ForegroundColor Yellow
# MSVC
$clExe = "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\cl.exe"
if (Test-Path $clExe) {
Write-Step "MSVC cl.exe" "FOUND" "v14.44.35207 (VS 2022 Professional)"
}
else {
Write-Step "MSVC cl.exe" "FAIL" "Not found -- install via Visual Studio Installer (see docs/tool_compilers.md)"
}
# Windows SDK
$rcExe = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\rc.exe"
if (Test-Path $rcExe) {
Write-Step "Windows SDK" "FOUND" "10.0.26100.0"
}
else {
Write-Step "Windows SDK" "FAIL" "Not found -- install via Visual Studio Installer (see docs/tool_compilers.md)"
}
# Qt
$qtBinDir = "C:\Qt\6.10.0\msvc2022_64\bin"
if (Test-Path "$qtBinDir\qmake.exe") {
Write-Step "Qt 6.10.0" "FOUND" "msvc2022_64 at $qtBinDir"
}
else {
Write-Step "Qt 6.10.0" "FAIL" "Not found -- install via Qt Online Installer (see docs/tool_compilers.md)"
}
# w64devkit
if (Test-Path "C:\w64devkit\bin\gcc.exe") {
$ver = & "C:\w64devkit\bin\gcc.exe" --version 2>&1 | Select-Object -First 1
Write-Step "w64devkit" "FOUND" $ver
}
else {
Write-Step "w64devkit" "SKIP" "Not found at C:\w64devkit -- download from https://github.com/skeeto/w64devkit/releases"
}
Write-Host "`n=== Tool installation complete ===" -ForegroundColor Green
Write-Host "Run .\repair_path.ps1 next to ensure all paths are registered.`n"

265
scripts/repair_path.ps1 Normal file
View File

@@ -0,0 +1,265 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Repairs the Windows PATH and sets environment variables for the
SetecPartitionWizard C++17/Qt6 build environment.
.DESCRIPTION
This script permanently adds missing dev-tool directories to the User PATH
(via [Environment]::SetEnvironmentVariable) and sets INCLUDE, LIB, Qt6_DIR,
CMAKE_PREFIX_PATH, and other variables needed by CMake/Ninja/MSVC builds.
It does NOT remove any existing PATH entries -- it only appends missing ones.
Run from an elevated or normal PowerShell prompt. After running, open a NEW
terminal for the changes to take effect.
.NOTES
Generated 2026-03-11 by Goju PATH repair agent.
Machine: mdavi / MSYS_NT-10.0-26200
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ─────────────────────────────────────────────────────────────
# 1. DISCOVERED TOOL PATHS (from system scan 2026-03-11)
# ─────────────────────────────────────────────────────────────
# MSVC 2022 Professional 17.14.14, toolset 14.44.35207
$MsvcVersion = "14.44.35207"
$VsRoot = "C:\Program Files\Microsoft Visual Studio\2022\Professional"
$MsvcBin = "$VsRoot\VC\Tools\MSVC\$MsvcVersion\bin\Hostx64\x64"
$MsvcInclude = "$VsRoot\VC\Tools\MSVC\$MsvcVersion\include"
$MsvcLib = "$VsRoot\VC\Tools\MSVC\$MsvcVersion\lib\x64"
$VcVarsAll = "$VsRoot\VC\Auxiliary\Build\vcvarsall.bat"
$VcVars64 = "$VsRoot\VC\Auxiliary\Build\vcvars64.bat"
# Windows SDK 10.0.26100.0
$SdkVersion = "10.0.26100.0"
$SdkRoot = "C:\Program Files (x86)\Windows Kits\10"
$SdkBin = "$SdkRoot\bin\$SdkVersion\x64"
$SdkIncludeUcrt = "$SdkRoot\Include\$SdkVersion\ucrt"
$SdkIncludeUm = "$SdkRoot\Include\$SdkVersion\um"
$SdkIncludeShared = "$SdkRoot\Include\$SdkVersion\shared"
$SdkLibUcrt = "$SdkRoot\Lib\$SdkVersion\ucrt\x64"
$SdkLibUm = "$SdkRoot\Lib\$SdkVersion\um\x64"
# CMake 3.x (standalone install)
$CmakeBin = "C:\Program Files\CMake\bin"
# Ninja (Qt-bundled)
$NinjaBin = "C:\Qt\Tools\Ninja"
# Qt 6.10.0 MSVC 2022 x64 (primary build kit)
$QtRoot = "C:\Qt\6.10.0\msvc2022_64"
$QtBin = "$QtRoot\bin"
$QtCmake = "$QtRoot\lib\cmake\Qt6"
# Qt Tools CMake (separate from standalone CMake)
$QtCmakeBin = "C:\Qt\Tools\CMake_64\bin"
# Clang/LLVM via Qt llvm-mingw 17.06
$LlvmMingwBin = "C:\Qt\Tools\llvm-mingw1706_64\bin"
# w64devkit (GCC 15.2.0, make, etc.)
$W64DevkitBin = "C:\w64devkit\bin"
# Python 3.14 (primary) and 3.13 (secondary)
$Python314 = "C:\Python314"
$Python313 = "C:\Users\mdavi\AppData\Local\Programs\Python\Python313"
$Python313Scripts = "C:\Users\mdavi\AppData\Local\Programs\Python\Python313\Scripts"
# Git for Windows
$GitCmd = "C:\Program Files\Git\cmd"
# Go (found on system)
$GoBin = "C:\Program Files\Go\bin"
# Chocolatey
$ChocoBin = "C:\ProgramData\chocolatey\bin"
# VS Code
$VsCodeBin = "C:\Users\mdavi\AppData\Local\Programs\Microsoft VS Code\bin"
# GitHub CLI
$GhCliBin = "C:\Program Files\GitHub CLI"
# WinGet LLVM-MinGW (UCRT, installed 2026-03-11)
$WingetLlvmMingw = "C:\Users\mdavi\AppData\Local\Microsoft\WinGet\Packages\MartinStorsjo.LLVM-MinGW.UCRT_Microsoft.Winget.Source_8wekyb3d8bbwe\llvm-mingw-20260311-ucrt-x86_64\bin"
# ─────────────────────────────────────────────────────────────
# 2. DEFINE DESIRED PATH ORDER (highest priority first)
# ─────────────────────────────────────────────────────────────
# Priority rationale:
# - MSVC cl.exe and SDK tools first (primary compiler)
# - CMake and Ninja next (build system)
# - Qt bin (for windeployqt, moc, uic, rcc)
# - Python, Git, and other helpers later
$DevToolPaths = @(
$MsvcBin, # cl.exe, link.exe, nmake.exe
$SdkBin, # rc.exe, mt.exe, signtool.exe
$CmakeBin, # cmake.exe, ctest.exe, cpack.exe
$NinjaBin, # ninja.exe
$QtBin, # windeployqt.exe, moc.exe, uic.exe, rcc.exe
$QtCmakeBin, # Qt-bundled cmake (fallback)
$LlvmMingwBin, # clang.exe, clang++.exe (Qt llvm-mingw)
$W64DevkitBin, # gcc.exe, g++.exe, make.exe
$Python314, # python.exe 3.14
$Python313, # python.exe 3.13
$Python313Scripts, # pip.exe, etc.
$GitCmd, # git.exe
$GoBin, # go.exe
$ChocoBin, # choco.exe
$GhCliBin, # gh.exe
$VsCodeBin, # code.exe
$WingetLlvmMingw # winget-installed llvm-mingw
)
# ─────────────────────────────────────────────────────────────
# 3. READ CURRENT USER PATH AND APPEND MISSING ENTRIES
# ─────────────────────────────────────────────────────────────
Write-Host "`n=== SetecPartitionWizard PATH Repair ===" -ForegroundColor Cyan
Write-Host "Scanning current User PATH for missing dev-tool entries...`n"
$CurrentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (-not $CurrentUserPath) { $CurrentUserPath = "" }
# Backup current PATH
$BackupFile = "$env:USERPROFILE\path_backup_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
$CurrentUserPath | Out-File -FilePath $BackupFile -Encoding UTF8
Write-Host " Backed up current User PATH to: $BackupFile" -ForegroundColor DarkGray
# Normalize: split, trim, remove empty, deduplicate (case-insensitive)
$ExistingEntries = $CurrentUserPath -split ';' |
ForEach-Object { $_.Trim().Trim("'").Trim('"').TrimEnd('\') } |
Where-Object { $_ -ne '' }
$ExistingSet = [System.Collections.Generic.HashSet[string]]::new(
[StringComparer]::OrdinalIgnoreCase
)
foreach ($e in $ExistingEntries) { [void]$ExistingSet.Add($e) }
$Added = @()
$AlreadyPresent = @()
foreach ($dir in $DevToolPaths) {
$normalized = $dir.TrimEnd('\')
if ($ExistingSet.Contains($normalized)) {
$AlreadyPresent += $normalized
}
elseif (Test-Path $normalized) {
$Added += $normalized
[void]$ExistingSet.Add($normalized)
}
else {
Write-Host " [SKIP] Not found on disk: $normalized" -ForegroundColor Yellow
}
}
if ($AlreadyPresent.Count -gt 0) {
Write-Host "`n Already in User PATH:" -ForegroundColor Green
$AlreadyPresent | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGreen }
}
if ($Added.Count -gt 0) {
Write-Host "`n Adding to User PATH:" -ForegroundColor Cyan
$Added | ForEach-Object { Write-Host " $_" -ForegroundColor White }
# Build new PATH: existing entries + new entries
$NewPath = (($ExistingEntries + $Added) | Select-Object -Unique) -join ';'
# Safety: check total length
if ($NewPath.Length -gt 30000) {
Write-Warning "New PATH is $($NewPath.Length) chars -- approaching the 32767 limit!"
}
[Environment]::SetEnvironmentVariable("Path", $NewPath, "User")
Write-Host "`n User PATH updated permanently." -ForegroundColor Green
}
else {
Write-Host "`n No new PATH entries needed -- all dev tools already present." -ForegroundColor Green
}
# ─────────────────────────────────────────────────────────────
# 4. SET INCLUDE AND LIB FOR MSVC + WINDOWS SDK
# ─────────────────────────────────────────────────────────────
# Note: These are typically set by vcvars64.bat at session start.
# Setting them permanently in User env makes them available to
# CMake/Ninja even outside a Developer Command Prompt.
Write-Host "`n=== Setting INCLUDE and LIB ===" -ForegroundColor Cyan
$IncludePaths = @(
$MsvcInclude,
$SdkIncludeUcrt,
$SdkIncludeUm,
$SdkIncludeShared
) -join ';'
$LibPaths = @(
$MsvcLib,
$SdkLibUcrt,
$SdkLibUm
) -join ';'
[Environment]::SetEnvironmentVariable("INCLUDE", $IncludePaths, "User")
Write-Host " INCLUDE = $IncludePaths" -ForegroundColor DarkGray
[Environment]::SetEnvironmentVariable("LIB", $LibPaths, "User")
Write-Host " LIB = $LibPaths" -ForegroundColor DarkGray
# ─────────────────────────────────────────────────────────────
# 5. SET Qt AND CMAKE VARIABLES
# ─────────────────────────────────────────────────────────────
Write-Host "`n=== Setting Qt6 / CMake variables ===" -ForegroundColor Cyan
[Environment]::SetEnvironmentVariable("Qt6_DIR", $QtCmake, "User")
Write-Host " Qt6_DIR = $QtCmake"
[Environment]::SetEnvironmentVariable("CMAKE_PREFIX_PATH", $QtRoot, "User")
Write-Host " CMAKE_PREFIX_PATH = $QtRoot"
[Environment]::SetEnvironmentVariable("QT_ROOT", $QtRoot, "User")
Write-Host " QT_ROOT = $QtRoot"
# ─────────────────────────────────────────────────────────────
# 6. VCVARS HELPER FUNCTION (for session-level MSVC setup)
# ─────────────────────────────────────────────────────────────
Write-Host "`n=== vcvars64 helper ===" -ForegroundColor Cyan
Write-Host @"
The MSVC compiler (cl.exe) works best when vcvars64.bat has been sourced
in the current session. The permanent INCLUDE/LIB vars above cover most
CMake/Ninja use cases, but if you need the full VS environment, run:
cmd /k "`"$VcVars64`" & powershell"
Or add this function to your PowerShell profile ($PROFILE):
function Enter-VsDevShell {
Import-Module "`"$VsRoot\Common7\Tools\Microsoft.VisualStudio.DevShell.dll`""
Enter-VsDevShell -VsInstallPath "`"$VsRoot`" -DevCmdArguments `"-arch=amd64`"
}
"@ -ForegroundColor DarkGray
# ─────────────────────────────────────────────────────────────
# 7. SUMMARY
# ─────────────────────────────────────────────────────────────
Write-Host "=== Done ===" -ForegroundColor Green
Write-Host "Open a NEW terminal for all changes to take effect.`n"
# Show final User PATH for verification
Write-Host "Final User PATH entries:" -ForegroundColor Cyan
$FinalPath = [Environment]::GetEnvironmentVariable("Path", "User")
$FinalPath -split ';' | Where-Object { $_ -ne '' } | ForEach-Object {
$marker = if (Test-Path $_) { "[OK]" } else { "[!!]" }
$color = if (Test-Path $_) { "Green" } else { "Red" }
Write-Host " $marker $_" -ForegroundColor $color
}

View File

@@ -7,9 +7,12 @@ set(APP_HEADERS
SingleInstance.h
)
qt_add_resources(APP_RESOURCES ${CMAKE_SOURCE_DIR}/resources/resources.qrc)
add_executable(SetecPartitionWizard WIN32
${APP_SOURCES}
${APP_HEADERS}
${APP_RESOURCES}
${CMAKE_SOURCE_DIR}/resources/setec.rc
)

View File

@@ -4,6 +4,7 @@
#include "ui/MainWindow.h"
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QStandardPaths>
@@ -11,6 +12,7 @@
#ifdef _WIN32
#include <Windows.h>
#include <shellapi.h>
#endif
static bool isRunningAsAdmin()

View File

@@ -1,24 +1,48 @@
set(CORE_SOURCES
# Common
common/Types.cpp
common/Logging.cpp
# Disk (stubs — will be implemented in Phase 2)
# disk/RawDiskHandle.cpp
# disk/VolumeHandle.cpp
# disk/DiskEnumerator.cpp
# disk/PartitionTable.cpp
# disk/DiskGeometry.cpp
# disk/VolumeManager.cpp
# Disk I/O and enumeration
disk/RawDiskHandle.cpp
disk/VolumeHandle.cpp
disk/DiskEnumerator.cpp
disk/DiskGeometry.cpp
disk/SmartReader.cpp
disk/PartitionTable.cpp
disk/FilesystemDetector.cpp
disk/FilesystemInfo.cpp
# Filesystem (stubs — will be implemented in Phase 4)
# filesystem/FilesystemFactory.cpp
# filesystem/FilesystemDetector.cpp
# filesystem/NtfsDriver.cpp
# filesystem/Fat32Driver.cpp
# Filesystem
filesystem/FormatEngine.cpp
# Operations (stubs — will be implemented in Phase 3)
# operations/OperationQueue.cpp
# operations/OperationRunner.cpp
# Operations
operations/OperationQueue.cpp
operations/PartitionOperations.cpp
# Recovery
recovery/PartitionRecovery.cpp
recovery/FileRecovery.cpp
recovery/BootRepair.cpp
# Diagnostics
diagnostics/Benchmark.cpp
diagnostics/SurfaceScan.cpp
# Imaging
imaging/Checksums.cpp
imaging/DiskCloner.cpp
imaging/ImageCreator.cpp
imaging/ImageRestorer.cpp
imaging/IsoFlasher.cpp
# Maintenance
maintenance/SecureErase.cpp
# Security
security/EncryptedVault.cpp
security/Fido2Manager.cpp
security/BootAuthenticator.cpp
)
set(CORE_HEADERS
@@ -28,12 +52,43 @@ set(CORE_HEADERS
common/Constants.h
common/Logging.h
common/Version.h
common/Obfuscate.h
disk/RawDiskHandle.h
disk/VolumeHandle.h
disk/DiskEnumerator.h
disk/DiskGeometry.h
disk/SmartReader.h
disk/PartitionTable.h
disk/FilesystemDetector.h
disk/FilesystemInfo.h
filesystem/FormatEngine.h
operations/Operation.h
operations/OperationQueue.h
operations/PartitionOperations.h
recovery/PartitionRecovery.h
recovery/FileRecovery.h
recovery/BootRepair.h
diagnostics/Benchmark.h
diagnostics/SurfaceScan.h
imaging/Checksums.h
imaging/DiskCloner.h
imaging/ImageCreator.h
imaging/ImageRestorer.h
imaging/IsoFlasher.h
maintenance/SecureErase.h
security/EncryptedVault.h
security/Fido2Manager.h
security/BootAuthenticator.h
)
add_library(spw_core STATIC ${CORE_SOURCES} ${CORE_HEADERS})
# Depend on build-time key generation
add_dependencies(spw_core generate_key)
target_include_directories(spw_core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/..
${CMAKE_BINARY_DIR}/generated
)
target_link_libraries(spw_core PUBLIC
@@ -47,5 +102,9 @@ if(WIN32)
wbemuuid
ole32
oleaut32
bcrypt
ntdll
virtdisk
hid
)
endif()

View File

@@ -18,7 +18,7 @@ constexpr uint64_t DEFAULT_ALIGNMENT_SECTORS_512 = DEFAULT_ALIGNMENT_BYTES / SEC
constexpr uint16_t MBR_SIGNATURE = 0xAA55;
constexpr uint32_t MBR_SIZE = 512;
constexpr int MBR_MAX_PRIMARY_PARTITIONS = 4;
constexpr uint8_t MBR_PARTITION_ENTRY_OFFSET = 446;
constexpr uint32_t MBR_PARTITION_ENTRY_OFFSET = 446;
constexpr uint8_t MBR_PARTITION_ENTRY_SIZE = 16;
// GPT constants
@@ -42,7 +42,7 @@ constexpr uint16_t HFSX_MAGIC = 0x4858; // "HX"
constexpr uint32_t APFS_MAGIC = 0x4253584E; // "NXSB" (little-endian)
constexpr uint16_t FAT_SIGNATURE = 0xAA55;
constexpr uint32_t REFS_MAGIC = 0x53465265; // "ReFS"
constexpr uint16_t HPFS_SUPER_MAGIC = 0xF995E849;
constexpr uint32_t HPFS_SUPER_MAGIC = 0xF995E849;
constexpr uint16_t MINIX_SUPER_MAGIC = 0x137F;
constexpr uint16_t MINIX2_SUPER_MAGIC = 0x2468;
constexpr uint32_t UFS_MAGIC = 0x00011954;

View File

@@ -0,0 +1,63 @@
#pragma once
// Compile-time string obfuscation for sensitive UI strings.
// All pentesting menu text is stored XOR-encrypted and decoded at runtime.
#include <array>
#include <cstdint>
#include <string>
namespace spw
{
namespace obf
{
// Compile-time XOR key derived from line number + counter
constexpr uint8_t xor_key(size_t idx, uint8_t seed)
{
return static_cast<uint8_t>((seed ^ 0xA7) + idx * 0x6D + (idx >> 2) * 0x3B);
}
template <size_t N>
struct ObfString
{
uint8_t data[N] = {};
uint8_t seed = 0;
constexpr ObfString(const char (&str)[N], uint8_t s) : seed(s)
{
for (size_t i = 0; i < N; i++)
{
data[i] = static_cast<uint8_t>(str[i]) ^ xor_key(i, seed);
}
}
std::string decode() const
{
std::string result(N - 1, '\0');
for (size_t i = 0; i < N - 1; i++)
{
result[i] = static_cast<char>(data[i] ^ xor_key(i, seed));
}
return result;
}
QString qdecode() const
{
return QString::fromStdString(decode());
}
};
// Macro: OBF("string") creates a compile-time encrypted string
// The __LINE__ is used as the seed so each usage gets a different key
#define OBF(str) ([]() { \
constexpr ::spw::obf::ObfString<sizeof(str)> _obf(str, (uint8_t)(__LINE__ ^ 0x55)); \
return _obf; \
}())
// Convenience: OBFS returns std::string, OBFQ returns QString
#define OBFS(str) OBF(str).decode()
#define OBFQ(str) OBF(str).qdecode()
} // namespace obf
} // namespace spw

81
src/core/common/Types.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include "Types.h"
#include <cstring>
#include <cstdio>
#include <random>
namespace spw
{
bool Guid::operator==(const Guid& other) const
{
return std::memcmp(data, other.data, 16) == 0;
}
bool Guid::operator!=(const Guid& other) const
{
return !(*this == other);
}
bool Guid::isZero() const
{
for (int i = 0; i < 16; ++i)
{
if (data[i] != 0)
return false;
}
return true;
}
std::string Guid::toString() const
{
// Standard GUID format: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
// Windows GUIDs store the first three groups in little-endian
char buf[64];
std::snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
data[3], data[2], data[1], data[0], // Data1 (LE)
data[5], data[4], // Data2 (LE)
data[7], data[6], // Data3 (LE)
data[8], data[9], // Data4[0..1]
data[10], data[11], data[12], data[13], data[14], data[15]);
return std::string(buf);
}
Guid Guid::fromString(const std::string& str)
{
Guid g{};
unsigned int d[16]{};
// Parse "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
if (std::sscanf(str.c_str(),
"%2x%2x%2x%2x-%2x%2x-%2x%2x-%2x%2x-%2x%2x%2x%2x%2x%2x",
&d[3], &d[2], &d[1], &d[0],
&d[5], &d[4],
&d[7], &d[6],
&d[8], &d[9],
&d[10], &d[11], &d[12], &d[13], &d[14], &d[15]) == 16)
{
for (int i = 0; i < 16; ++i)
g.data[i] = static_cast<uint8_t>(d[i]);
}
return g;
}
Guid Guid::generate()
{
Guid g{};
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<unsigned int> dist(0, 255);
for (int i = 0; i < 16; ++i)
g.data[i] = static_cast<uint8_t>(dist(gen));
// Set version 4 (random) — bits 48-51 = 0100
g.data[7] = static_cast<uint8_t>((g.data[7] & 0x0F) | 0x40);
// Set variant 1 — bits 64-65 = 10
g.data[8] = static_cast<uint8_t>((g.data[8] & 0x3F) | 0x80);
return g;
}
} // namespace spw

View File

@@ -121,6 +121,13 @@ enum class DiskInterfaceType
Virtual, // VHD, VHDX, etc.
};
// Access mode for opening raw disks or volumes
enum class DiskAccessMode
{
ReadOnly,
ReadWrite,
};
// Media types
enum class MediaType
{

View File

@@ -0,0 +1,821 @@
// Benchmark.cpp -- Disk performance benchmark using direct I/O.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// Write benchmarks create temporary files on the target volume.
#include "Benchmark.h"
#include <algorithm>
#include <cstring>
#include <random>
#include <sstream>
#include <thread>
namespace spw
{
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
Benchmark::Benchmark(const std::string& volumePath)
: m_volumePath(volumePath)
{
}
// ---------------------------------------------------------------------------
// getTimestamp -- QueryPerformanceCounter-based high-resolution timer
// ---------------------------------------------------------------------------
double Benchmark::getTimestamp()
{
static LARGE_INTEGER frequency = {};
if (frequency.QuadPart == 0)
QueryPerformanceFrequency(&frequency);
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return static_cast<double>(now.QuadPart) / static_cast<double>(frequency.QuadPart);
}
// ---------------------------------------------------------------------------
// getVolumeSize -- query the free space on the volume
// ---------------------------------------------------------------------------
Result<uint64_t> Benchmark::getVolumeSize() const
{
// Convert path to wide string
std::wstring wpath(m_volumePath.begin(), m_volumePath.end());
ULARGE_INTEGER freeBytesAvailable, totalBytes, totalFreeBytes;
BOOL ok = GetDiskFreeSpaceExW(wpath.c_str(), &freeBytesAvailable,
&totalBytes, &totalFreeBytes);
if (!ok)
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"GetDiskFreeSpaceExW failed");
return static_cast<uint64_t>(totalBytes.QuadPart);
}
// ---------------------------------------------------------------------------
// createTempFile -- create a preallocated temp file for write testing
// ---------------------------------------------------------------------------
Result<std::wstring> Benchmark::createTempFile(uint64_t sizeBytes)
{
std::wstring wpath(m_volumePath.begin(), m_volumePath.end());
// Generate temp file path
wchar_t tempPath[MAX_PATH + 1] = {};
wchar_t tempFile[MAX_PATH + 1] = {};
// Use the volume root as temp directory
wcsncpy_s(tempPath, wpath.c_str(), MAX_PATH);
if (GetTempFileNameW(tempPath, L"spw", 0, tempFile) == 0)
return ErrorInfo::fromWin32(ErrorCode::FileCreateFailed, GetLastError(),
"Cannot create temp file");
m_tempFilePath = tempFile;
// Open with FILE_FLAG_NO_BUFFERING for direct I/O
HANDLE hFile = CreateFileW(
m_tempFilePath.c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::FileCreateFailed, GetLastError(),
"Cannot open temp file for writing");
// Preallocate by setting the file pointer and end of file.
// We need to write actual data since FILE_FLAG_NO_BUFFERING requires
// sector-aligned writes.
LARGE_INTEGER fileSize;
fileSize.QuadPart = static_cast<LONGLONG>(sizeBytes);
SetFilePointerEx(hFile, fileSize, nullptr, FILE_BEGIN);
SetEndOfFile(hFile);
SetFilePointerEx(hFile, {}, nullptr, FILE_BEGIN);
// Write zeros in 1 MiB chunks to actually allocate the space
const uint32_t chunkSize = 1024 * 1024;
std::vector<uint8_t> zeros(chunkSize, 0);
uint64_t written = 0;
while (written < sizeBytes)
{
DWORD toWrite = static_cast<DWORD>(std::min(
static_cast<uint64_t>(chunkSize), sizeBytes - written));
// Align to sector size
toWrite = (toWrite / 512) * 512;
if (toWrite == 0)
break;
DWORD bytesWritten = 0;
WriteFile(hFile, zeros.data(), toWrite, &bytesWritten, nullptr);
if (bytesWritten == 0)
break;
written += bytesWritten;
}
CloseHandle(hFile);
return m_tempFilePath;
}
// ---------------------------------------------------------------------------
// deleteTempFile
// ---------------------------------------------------------------------------
void Benchmark::deleteTempFile()
{
if (!m_tempFilePath.empty())
{
DeleteFileW(m_tempFilePath.c_str());
m_tempFilePath.clear();
}
}
// ---------------------------------------------------------------------------
// sequentialRead -- read large contiguous blocks, measure throughput
// ---------------------------------------------------------------------------
Result<double> Benchmark::sequentialRead(int durationSec, uint32_t blockSize,
BenchmarkProgress progressCb,
std::atomic<bool>* cancelFlag)
{
// We'll read from the raw volume. Build the volume device path.
// E.g., for "C:\", the device path is "\\.\C:"
if (m_volumePath.empty())
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Volume path is empty");
wchar_t driveLetter = static_cast<wchar_t>(m_volumePath[0]);
std::wstring devicePath = L"\\\\.\\";
devicePath += driveLetter;
devicePath += L':';
HANDLE hDevice = CreateFileW(
devicePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN,
nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open volume for sequential read benchmark");
// Align block size to 512-byte boundary
blockSize = (blockSize / 512) * 512;
if (blockSize == 0)
blockSize = BENCH_BLOCK_SEQ;
// Allocate an aligned read buffer
void* buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer)
{
CloseHandle(hDevice);
return ErrorInfo::fromCode(ErrorCode::OutOfMemory, "Cannot allocate aligned buffer");
}
uint64_t totalBytesRead = 0;
double startTime = getTimestamp();
double elapsed = 0.0;
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled);
}
DWORD bytesRead = 0;
BOOL ok = ReadFile(hDevice, buffer, blockSize, &bytesRead, nullptr);
if (!ok || bytesRead == 0)
{
// Reached end of volume or error; seek back to start
LARGE_INTEGER zero = {};
SetFilePointerEx(hDevice, zero, nullptr, FILE_BEGIN);
}
else
{
totalBytesRead += bytesRead;
}
elapsed = getTimestamp() - startTime;
if (progressCb)
{
int pct = static_cast<int>((elapsed / durationSec) * 100.0);
pct = std::min(pct, 100);
BenchmarkResults partial;
partial.seqReadMBps = (totalBytesRead / (1024.0 * 1024.0)) / std::max(elapsed, 0.001);
progressCb(BenchmarkPhase::SequentialRead, pct, partial);
}
}
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
if (elapsed <= 0.0)
return 0.0;
double mbps = (static_cast<double>(totalBytesRead) / (1024.0 * 1024.0)) / elapsed;
return mbps;
}
// ---------------------------------------------------------------------------
// sequentialWrite -- write large contiguous blocks to a temp file
// ---------------------------------------------------------------------------
Result<double> Benchmark::sequentialWrite(int durationSec, uint32_t blockSize,
BenchmarkProgress progressCb,
std::atomic<bool>* cancelFlag)
{
blockSize = (blockSize / 512) * 512;
if (blockSize == 0)
blockSize = BENCH_BLOCK_SEQ;
// Create temp file
auto tempResult = createTempFile(static_cast<uint64_t>(blockSize) * 2048);
if (tempResult.isError())
return tempResult.error();
HANDLE hFile = CreateFileW(
m_tempFilePath.c_str(),
GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
deleteTempFile();
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open temp file for write benchmark");
}
// Allocate aligned write buffer with random data
void* buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer)
{
CloseHandle(hFile);
deleteTempFile();
return ErrorInfo::fromCode(ErrorCode::OutOfMemory, "Cannot allocate aligned buffer");
}
// Fill with random data to avoid compression effects
std::mt19937 rng(42);
uint32_t* buf32 = static_cast<uint32_t*>(buffer);
for (uint32_t i = 0; i < blockSize / 4; ++i)
buf32[i] = rng();
uint64_t totalBytesWritten = 0;
double startTime = getTimestamp();
double elapsed = 0.0;
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hFile);
deleteTempFile();
return ErrorInfo::fromCode(ErrorCode::OperationCanceled);
}
DWORD bytesWritten = 0;
BOOL ok = WriteFile(hFile, buffer, blockSize, &bytesWritten, nullptr);
if (!ok || bytesWritten == 0)
{
// Wrap around to start of file
LARGE_INTEGER zero = {};
SetFilePointerEx(hFile, zero, nullptr, FILE_BEGIN);
}
else
{
totalBytesWritten += bytesWritten;
}
elapsed = getTimestamp() - startTime;
if (progressCb)
{
int pct = static_cast<int>((elapsed / durationSec) * 100.0);
pct = std::min(pct, 100);
BenchmarkResults partial;
partial.seqWriteMBps = (totalBytesWritten / (1024.0 * 1024.0)) / std::max(elapsed, 0.001);
progressCb(BenchmarkPhase::SequentialWrite, pct, partial);
}
}
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hFile);
deleteTempFile();
if (elapsed <= 0.0)
return 0.0;
double mbps = (static_cast<double>(totalBytesWritten) / (1024.0 * 1024.0)) / elapsed;
return mbps;
}
// ---------------------------------------------------------------------------
// randomRead4K -- random 4K reads, measure IOPS
// ---------------------------------------------------------------------------
Result<double> Benchmark::randomRead4K(int durationSec, int queueDepth,
BenchmarkProgress progressCb,
std::atomic<bool>* cancelFlag)
{
if (m_volumePath.empty())
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Volume path is empty");
auto volSizeResult = getVolumeSize();
if (volSizeResult.isError())
return volSizeResult.error();
const uint64_t volumeSize = volSizeResult.value();
const uint32_t blockSize = BENCH_BLOCK_RND;
const uint64_t maxOffset = (volumeSize / blockSize) * blockSize;
if (maxOffset < blockSize)
return ErrorInfo::fromCode(ErrorCode::BenchmarkFailed, "Volume too small for random read test");
wchar_t driveLetter = static_cast<wchar_t>(m_volumePath[0]);
std::wstring devicePath = L"\\\\.\\";
devicePath += driveLetter;
devicePath += L':';
BenchmarkPhase phase = (queueDepth > 1)
? BenchmarkPhase::RandomRead4K_QD32
: BenchmarkPhase::RandomRead4K_QD1;
if (queueDepth <= 1)
{
// QD1: simple synchronous random reads
HANDLE hDevice = CreateFileW(
devicePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_RANDOM_ACCESS,
nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open volume for random read");
void* buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer)
{
CloseHandle(hDevice);
return ErrorInfo::fromCode(ErrorCode::OutOfMemory, "Cannot allocate buffer");
}
std::mt19937_64 rng(std::random_device{}());
uint64_t totalOps = 0;
double totalLatency = 0.0;
double startTime = getTimestamp();
double elapsed = 0.0;
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled);
}
// Random aligned offset
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
LARGE_INTEGER li;
li.QuadPart = static_cast<LONGLONG>(offset);
SetFilePointerEx(hDevice, li, nullptr, FILE_BEGIN);
double opStart = getTimestamp();
DWORD bytesRead = 0;
ReadFile(hDevice, buffer, blockSize, &bytesRead, nullptr);
double opEnd = getTimestamp();
if (bytesRead > 0)
{
++totalOps;
totalLatency += (opEnd - opStart);
}
elapsed = getTimestamp() - startTime;
if (progressCb && (totalOps % 1000 == 0))
{
int pct = static_cast<int>((elapsed / durationSec) * 100.0);
BenchmarkResults partial;
partial.rnd4kReadIOPS = totalOps / std::max(elapsed, 0.001);
partial.avgReadLatencyUs = (totalOps > 0)
? (totalLatency / totalOps) * 1e6
: 0.0;
progressCb(phase, std::min(pct, 100), partial);
}
}
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
return (elapsed > 0.0) ? (static_cast<double>(totalOps) / elapsed) : 0.0;
}
else
{
// QD32: use overlapped I/O for concurrent requests
HANDLE hDevice = CreateFileW(
devicePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_RANDOM_ACCESS,
nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open volume for QD32 random read");
struct IoSlot
{
OVERLAPPED overlapped = {};
void* buffer = nullptr;
bool pending = false;
};
std::vector<IoSlot> slots(queueDepth);
for (auto& slot : slots)
{
slot.buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
slot.overlapped.hEvent = CreateEventW(nullptr, TRUE, TRUE, nullptr);
}
std::mt19937_64 rng(std::random_device{}());
uint64_t totalOps = 0;
double startTime = getTimestamp();
double elapsed = 0.0;
// Submit initial batch
for (auto& slot : slots)
{
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
slot.overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
slot.overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
ResetEvent(slot.overlapped.hEvent);
ReadFile(hDevice, slot.buffer, blockSize, nullptr, &slot.overlapped);
slot.pending = true;
}
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
break;
for (auto& slot : slots)
{
if (!slot.pending)
continue;
DWORD bytesRead = 0;
BOOL result = GetOverlappedResult(hDevice, &slot.overlapped, &bytesRead, FALSE);
if (result || GetLastError() != ERROR_IO_INCOMPLETE)
{
++totalOps;
slot.pending = false;
// Resubmit
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
slot.overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
slot.overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
slot.overlapped.Internal = 0;
slot.overlapped.InternalHigh = 0;
ResetEvent(slot.overlapped.hEvent);
ReadFile(hDevice, slot.buffer, blockSize, nullptr, &slot.overlapped);
slot.pending = true;
}
}
elapsed = getTimestamp() - startTime;
}
// Cancel outstanding I/O and clean up
CancelIo(hDevice);
for (auto& slot : slots)
{
if (slot.pending)
{
DWORD bytesRead = 0;
GetOverlappedResult(hDevice, &slot.overlapped, &bytesRead, TRUE);
}
if (slot.overlapped.hEvent)
CloseHandle(slot.overlapped.hEvent);
if (slot.buffer)
VirtualFree(slot.buffer, 0, MEM_RELEASE);
}
CloseHandle(hDevice);
return (elapsed > 0.0) ? (static_cast<double>(totalOps) / elapsed) : 0.0;
}
}
// ---------------------------------------------------------------------------
// randomWrite4K -- random 4K writes, measure IOPS
// ---------------------------------------------------------------------------
Result<double> Benchmark::randomWrite4K(int durationSec, int queueDepth,
BenchmarkProgress progressCb,
std::atomic<bool>* cancelFlag)
{
const uint32_t blockSize = BENCH_BLOCK_RND;
// Create a temp file for random writes
uint64_t tempSize = 256ULL * 1024 * 1024; // 256 MiB
auto tempResult = createTempFile(tempSize);
if (tempResult.isError())
return tempResult.error();
const uint64_t maxOffset = (tempSize / blockSize) * blockSize;
BenchmarkPhase phase = (queueDepth > 1)
? BenchmarkPhase::RandomWrite4K_QD32
: BenchmarkPhase::RandomWrite4K_QD1;
if (queueDepth <= 1)
{
HANDLE hFile = CreateFileW(
m_tempFilePath.c_str(),
GENERIC_WRITE | GENERIC_READ,
0,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_RANDOM_ACCESS,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
deleteTempFile();
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open temp file for random write");
}
void* buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer)
{
CloseHandle(hFile);
deleteTempFile();
return ErrorInfo::fromCode(ErrorCode::OutOfMemory, "Cannot allocate buffer");
}
// Fill buffer with random data
std::mt19937 fillRng(42);
uint32_t* buf32 = static_cast<uint32_t*>(buffer);
for (uint32_t i = 0; i < blockSize / 4; ++i)
buf32[i] = fillRng();
std::mt19937_64 rng(std::random_device{}());
uint64_t totalOps = 0;
double totalLatency = 0.0;
double startTime = getTimestamp();
double elapsed = 0.0;
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hFile);
deleteTempFile();
return ErrorInfo::fromCode(ErrorCode::OperationCanceled);
}
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
LARGE_INTEGER li;
li.QuadPart = static_cast<LONGLONG>(offset);
SetFilePointerEx(hFile, li, nullptr, FILE_BEGIN);
double opStart = getTimestamp();
DWORD bytesWritten = 0;
WriteFile(hFile, buffer, blockSize, &bytesWritten, nullptr);
double opEnd = getTimestamp();
if (bytesWritten > 0)
{
++totalOps;
totalLatency += (opEnd - opStart);
}
elapsed = getTimestamp() - startTime;
if (progressCb && (totalOps % 1000 == 0))
{
int pct = static_cast<int>((elapsed / durationSec) * 100.0);
BenchmarkResults partial;
partial.rnd4kWriteIOPS = totalOps / std::max(elapsed, 0.001);
partial.avgWriteLatencyUs = (totalOps > 0)
? (totalLatency / totalOps) * 1e6
: 0.0;
progressCb(phase, std::min(pct, 100), partial);
}
}
VirtualFree(buffer, 0, MEM_RELEASE);
CloseHandle(hFile);
deleteTempFile();
return (elapsed > 0.0) ? (static_cast<double>(totalOps) / elapsed) : 0.0;
}
else
{
// QD32 overlapped writes
HANDLE hFile = CreateFileW(
m_tempFilePath.c_str(),
GENERIC_WRITE | GENERIC_READ,
0,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_OVERLAPPED |
FILE_FLAG_RANDOM_ACCESS,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
deleteTempFile();
return ErrorInfo::fromWin32(ErrorCode::BenchmarkFailed, GetLastError(),
"Cannot open temp file for QD32 random write");
}
struct IoSlot
{
OVERLAPPED overlapped = {};
void* buffer = nullptr;
bool pending = false;
};
std::vector<IoSlot> slots(queueDepth);
std::mt19937 fillRng(42);
for (auto& slot : slots)
{
slot.buffer = VirtualAlloc(nullptr, blockSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
uint32_t* buf32 = static_cast<uint32_t*>(slot.buffer);
for (uint32_t i = 0; i < blockSize / 4; ++i)
buf32[i] = fillRng();
slot.overlapped.hEvent = CreateEventW(nullptr, TRUE, TRUE, nullptr);
}
std::mt19937_64 rng(std::random_device{}());
uint64_t totalOps = 0;
double startTime = getTimestamp();
double elapsed = 0.0;
// Submit initial batch
for (auto& slot : slots)
{
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
slot.overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
slot.overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
ResetEvent(slot.overlapped.hEvent);
WriteFile(hFile, slot.buffer, blockSize, nullptr, &slot.overlapped);
slot.pending = true;
}
while (elapsed < durationSec)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
break;
for (auto& slot : slots)
{
if (!slot.pending)
continue;
DWORD bytesWritten = 0;
BOOL result = GetOverlappedResult(hFile, &slot.overlapped, &bytesWritten, FALSE);
if (result || GetLastError() != ERROR_IO_INCOMPLETE)
{
++totalOps;
slot.pending = false;
uint64_t offset = (rng() % (maxOffset / blockSize)) * blockSize;
slot.overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
slot.overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
slot.overlapped.Internal = 0;
slot.overlapped.InternalHigh = 0;
ResetEvent(slot.overlapped.hEvent);
WriteFile(hFile, slot.buffer, blockSize, nullptr, &slot.overlapped);
slot.pending = true;
}
}
elapsed = getTimestamp() - startTime;
}
CancelIo(hFile);
for (auto& slot : slots)
{
if (slot.pending)
{
DWORD bw = 0;
GetOverlappedResult(hFile, &slot.overlapped, &bw, TRUE);
}
if (slot.overlapped.hEvent)
CloseHandle(slot.overlapped.hEvent);
if (slot.buffer)
VirtualFree(slot.buffer, 0, MEM_RELEASE);
}
CloseHandle(hFile);
deleteTempFile();
return (elapsed > 0.0) ? (static_cast<double>(totalOps) / elapsed) : 0.0;
}
}
// ---------------------------------------------------------------------------
// run -- complete benchmark suite
// ---------------------------------------------------------------------------
Result<BenchmarkResults> Benchmark::run(
const BenchmarkConfig& config,
BenchmarkProgress progressCb,
std::atomic<bool>* cancelFlag)
{
BenchmarkResults results;
// Sequential read
auto seqReadResult = sequentialRead(config.durationSeconds, config.seqBlockSize,
progressCb, cancelFlag);
if (seqReadResult.isOk())
results.seqReadMBps = seqReadResult.value();
else if (seqReadResult.error().code == ErrorCode::OperationCanceled)
return seqReadResult.error();
// Sequential write
if (!config.skipWriteTests)
{
auto seqWriteResult = sequentialWrite(config.durationSeconds, config.seqBlockSize,
progressCb, cancelFlag);
if (seqWriteResult.isOk())
results.seqWriteMBps = seqWriteResult.value();
else if (seqWriteResult.error().code == ErrorCode::OperationCanceled)
return seqWriteResult.error();
}
// Random read 4K QD1
auto rndReadResult = randomRead4K(config.durationSeconds, 1, progressCb, cancelFlag);
if (rndReadResult.isOk())
results.rnd4kReadIOPS = rndReadResult.value();
else if (rndReadResult.error().code == ErrorCode::OperationCanceled)
return rndReadResult.error();
// Random read 4K QD32
auto rndReadQD32 = randomRead4K(config.durationSeconds, 32, progressCb, cancelFlag);
if (rndReadQD32.isOk())
results.rnd4kReadIOPS_QD32 = rndReadQD32.value();
else if (rndReadQD32.error().code == ErrorCode::OperationCanceled)
return rndReadQD32.error();
// Random write 4K QD1
if (!config.skipWriteTests)
{
auto rndWriteResult = randomWrite4K(config.durationSeconds, 1, progressCb, cancelFlag);
if (rndWriteResult.isOk())
results.rnd4kWriteIOPS = rndWriteResult.value();
else if (rndWriteResult.error().code == ErrorCode::OperationCanceled)
return rndWriteResult.error();
// Random write 4K QD32
auto rndWriteQD32 = randomWrite4K(config.durationSeconds, 32, progressCb, cancelFlag);
if (rndWriteQD32.isOk())
results.rnd4kWriteIOPS_QD32 = rndWriteQD32.value();
else if (rndWriteQD32.error().code == ErrorCode::OperationCanceled)
return rndWriteQD32.error();
}
// Calculate average latencies from QD1 results
if (results.rnd4kReadIOPS > 0)
results.avgReadLatencyUs = (1.0 / results.rnd4kReadIOPS) * 1e6;
if (results.rnd4kWriteIOPS > 0)
results.avgWriteLatencyUs = (1.0 / results.rnd4kWriteIOPS) * 1e6;
if (progressCb)
{
progressCb(BenchmarkPhase::Complete, 100, results);
}
return results;
}
} // namespace spw

View File

@@ -0,0 +1,122 @@
#pragma once
// Benchmark -- Measure sequential/random read and write performance of a disk.
//
// Uses Win32 file I/O with FILE_FLAG_NO_BUFFERING for direct disk access, and
// QueryPerformanceCounter for sub-microsecond timing precision.
//
// Tests:
// - Sequential read (1 MiB blocks)
// - Sequential write (1 MiB blocks, temp file on target volume)
// - Random read 4K (QD1 and QD32)
// - Random write 4K (QD1 and QD32)
//
// DISCLAIMER: This code is for authorized disk utility software only.
// Write tests create temporary files; random write tests involve
// sustained 4K random writes which add write amplification on SSDs.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Results of a complete benchmark run
struct BenchmarkResults
{
double seqReadMBps = 0.0; // Sequential read throughput (MiB/s)
double seqWriteMBps = 0.0; // Sequential write throughput (MiB/s)
double rnd4kReadIOPS = 0.0; // Random 4K read IOPS (QD1)
double rnd4kWriteIOPS = 0.0; // Random 4K write IOPS (QD1)
double rnd4kReadIOPS_QD32 = 0.0; // Random 4K read IOPS (QD32)
double rnd4kWriteIOPS_QD32 = 0.0; // Random 4K write IOPS (QD32)
double avgReadLatencyUs = 0.0;// Average read latency (microseconds)
double avgWriteLatencyUs = 0.0;// Average write latency (microseconds)
};
// Which test is currently running
enum class BenchmarkPhase
{
SequentialRead,
SequentialWrite,
RandomRead4K_QD1,
RandomWrite4K_QD1,
RandomRead4K_QD32,
RandomWrite4K_QD32,
Complete,
};
// Progress callback.
// Parameters: (currentPhase, phasePercentage 0-100, partialResults)
using BenchmarkProgress = std::function<void(BenchmarkPhase phase,
int percentage,
const BenchmarkResults& partial)>;
// Configuration for the benchmark
struct BenchmarkConfig
{
int durationSeconds = BENCH_DEFAULT_DURATION_SEC; // Per test
uint32_t seqBlockSize = BENCH_BLOCK_SEQ; // Sequential block size
uint32_t rndBlockSize = BENCH_BLOCK_RND; // Random block size
uint64_t testFileSizeBytes = 1024ULL * 1024 * 1024; // 1 GiB temp file for writes
bool skipWriteTests = false; // Skip write tests (safe mode)
};
class Benchmark
{
public:
// volumePath: root of the volume to benchmark, e.g. "C:\\"
explicit Benchmark(const std::string& volumePath);
// Run the complete benchmark suite
Result<BenchmarkResults> run(
const BenchmarkConfig& config = {},
BenchmarkProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
// Run individual tests
Result<double> sequentialRead(int durationSec, uint32_t blockSize,
BenchmarkProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
Result<double> sequentialWrite(int durationSec, uint32_t blockSize,
BenchmarkProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
Result<double> randomRead4K(int durationSec, int queueDepth,
BenchmarkProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
Result<double> randomWrite4K(int durationSec, int queueDepth,
BenchmarkProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
private:
// Create a temp file filled with random data for write testing
Result<std::wstring> createTempFile(uint64_t sizeBytes);
// Delete the temp file
void deleteTempFile();
// Get high-precision timestamp in seconds
static double getTimestamp();
// Get volume size for clamping random offsets
Result<uint64_t> getVolumeSize() const;
std::string m_volumePath;
std::wstring m_tempFilePath;
};
} // namespace spw

View File

@@ -0,0 +1,255 @@
// SurfaceScan.cpp -- Bad sector detection via read / write-verify testing.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// Write-verify mode DESTROYS all data on the scanned area.
#include "SurfaceScan.h"
#include <algorithm>
#include <cstring>
namespace spw
{
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
SurfaceScan::SurfaceScan(RawDiskHandle& disk)
: m_disk(disk)
{
}
// ---------------------------------------------------------------------------
// scanDisk -- scan the entire physical disk
// ---------------------------------------------------------------------------
Result<SurfaceScanResults> SurfaceScan::scanDisk(
SurfaceScanMode mode,
SurfaceScanProgress progressCb,
std::atomic<bool>* cancelFlag)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
const auto& geo = geoResult.value();
const uint32_t sectorSize = geo.bytesPerSector;
if (sectorSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 bytes/sector");
const uint64_t totalSectors = geo.totalBytes / sectorSize;
return scanImpl(0, totalSectors, sectorSize, mode, progressCb, cancelFlag);
}
// ---------------------------------------------------------------------------
// scanRange -- scan a specific LBA range
// ---------------------------------------------------------------------------
Result<SurfaceScanResults> SurfaceScan::scanRange(
SectorOffset startLba,
SectorCount sectorCount,
SurfaceScanMode mode,
SurfaceScanProgress progressCb,
std::atomic<bool>* cancelFlag)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
const uint32_t sectorSize = geoResult.value().bytesPerSector;
if (sectorSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 bytes/sector");
return scanImpl(startLba, sectorCount, sectorSize, mode, progressCb, cancelFlag);
}
// ---------------------------------------------------------------------------
// scanImpl -- core scan loop
//
// We read in chunks of 256 sectors (128 KiB at 512 bytes/sector) for
// throughput. When a chunk fails, we fall back to reading individual
// sectors within that chunk to isolate the specific bad sector(s).
// ---------------------------------------------------------------------------
Result<SurfaceScanResults> SurfaceScan::scanImpl(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
SurfaceScanMode mode,
SurfaceScanProgress progressCb,
std::atomic<bool>* cancelFlag)
{
if (sectorCount == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Sector count is 0");
SurfaceScanResults results;
results.totalSectorsTested = 0;
results.badSectorCount = 0;
// Chunk size in sectors: read 256 sectors at a time
const SectorCount chunkSectors = 256;
// For write-verify mode, we use a pattern buffer
// Pattern: alternating 0xAA and 0x55 bytes (checkerboard)
const uint32_t chunkBytes = static_cast<uint32_t>(chunkSectors) * sectorSize;
std::vector<uint8_t> writePattern(chunkBytes);
for (size_t i = 0; i < writePattern.size(); ++i)
writePattern[i] = (i % 2 == 0) ? 0xAA : 0x55;
// Timing
LARGE_INTEGER perfFreq, perfStart, perfNow;
QueryPerformanceFrequency(&perfFreq);
QueryPerformanceCounter(&perfStart);
SectorOffset currentLba = startLba;
SectorOffset endLba = startLba + sectorCount;
while (currentLba < endLba)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
return ErrorInfo::fromCode(ErrorCode::OperationCanceled, "Surface scan canceled");
SectorCount remaining = endLba - currentLba;
SectorCount thisChunk = std::min(chunkSectors, remaining);
bool chunkOk = true;
if (mode == SurfaceScanMode::ReadOnly)
{
// Attempt to read the entire chunk
auto readResult = m_disk.readSectors(currentLba, thisChunk, sectorSize);
if (readResult.isError())
chunkOk = false;
}
else // WriteVerify
{
// Write the pattern
uint32_t patternSize = static_cast<uint32_t>(thisChunk) * sectorSize;
auto writeResult = m_disk.writeSectors(currentLba, writePattern.data(),
thisChunk, sectorSize);
if (writeResult.isError())
{
chunkOk = false;
}
else
{
// Read back and verify
auto readResult = m_disk.readSectors(currentLba, thisChunk, sectorSize);
if (readResult.isError())
{
chunkOk = false;
}
else
{
const auto& readData = readResult.value();
if (readData.size() < patternSize ||
std::memcmp(readData.data(), writePattern.data(), patternSize) != 0)
{
chunkOk = false;
}
}
}
}
if (!chunkOk)
{
// Chunk had an error. Fall back to testing individual sectors
// to isolate which specific sectors are bad.
for (SectorCount s = 0; s < thisChunk; ++s)
{
SectorOffset testLba = currentLba + s;
BadSector bad;
bad.lba = testLba;
if (mode == SurfaceScanMode::ReadOnly)
{
auto singleRead = m_disk.readSectors(testLba, 1, sectorSize);
if (singleRead.isError())
{
bad.readError = true;
results.badSectors.push_back(bad);
results.badSectorCount++;
}
}
else // WriteVerify
{
// Write one sector
auto singleWrite = m_disk.writeSectors(testLba, writePattern.data(),
1, sectorSize);
if (singleWrite.isError())
{
bad.writeError = true;
results.badSectors.push_back(bad);
results.badSectorCount++;
continue;
}
// Read back
auto singleRead = m_disk.readSectors(testLba, 1, sectorSize);
if (singleRead.isError())
{
bad.readError = true;
results.badSectors.push_back(bad);
results.badSectorCount++;
continue;
}
// Verify
const auto& readData = singleRead.value();
if (readData.size() < sectorSize ||
std::memcmp(readData.data(), writePattern.data(), sectorSize) != 0)
{
bad.verifyError = true;
results.badSectors.push_back(bad);
results.badSectorCount++;
}
}
}
}
results.totalSectorsTested += thisChunk;
currentLba += thisChunk;
// Progress reporting
if (progressCb)
{
QueryPerformanceCounter(&perfNow);
double elapsed = static_cast<double>(perfNow.QuadPart - perfStart.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
double bytesScanned = static_cast<double>(results.totalSectorsTested) * sectorSize;
double speedMBps = (elapsed > 0.0)
? (bytesScanned / (1024.0 * 1024.0)) / elapsed
: 0.0;
double sectorsRemaining = static_cast<double>(sectorCount - results.totalSectorsTested);
double sectorsPerSec = (elapsed > 0.0)
? static_cast<double>(results.totalSectorsTested) / elapsed
: 0.0;
double etaSeconds = (sectorsPerSec > 0.0)
? sectorsRemaining / sectorsPerSec
: 0.0;
progressCb(results.totalSectorsTested,
sectorCount,
results.badSectorCount,
speedMBps,
etaSeconds);
}
}
// Final timing
QueryPerformanceCounter(&perfNow);
results.elapsedSeconds = static_cast<double>(perfNow.QuadPart - perfStart.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
double totalMB = static_cast<double>(results.totalSectorsTested) * sectorSize / (1024.0 * 1024.0);
results.averageSpeedMBps = (results.elapsedSeconds > 0.0)
? totalMB / results.elapsedSeconds
: 0.0;
return results;
}
} // namespace spw

View File

@@ -0,0 +1,99 @@
#pragma once
// SurfaceScan -- Bad sector detection via read (and optional write-verify) testing.
//
// Reads every sector on a disk or partition and records any sectors that
// return I/O errors. The optional write test (read-write-verify) is DESTRUCTIVE:
// it writes a known pattern, reads it back, and verifies the data matches.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// The write-verify test DESTROYS all data on the scanned area.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Record of a single bad sector
struct BadSector
{
SectorOffset lba = 0;
bool readError = false; // Failed to read
bool writeError = false; // Failed to write (write-verify mode only)
bool verifyError = false; // Read-back mismatch (write-verify mode only)
};
// Results of a surface scan
struct SurfaceScanResults
{
uint64_t totalSectorsTested = 0;
uint64_t badSectorCount = 0;
double elapsedSeconds = 0.0;
double averageSpeedMBps = 0.0;
std::vector<BadSector> badSectors;
};
// Scan mode
enum class SurfaceScanMode
{
ReadOnly, // Non-destructive: read every sector
WriteVerify, // DESTRUCTIVE: write pattern, read back, verify
};
// Progress callback.
// Parameters: (sectorsScanned, totalSectors, badSectorsFound, currentSpeedMBps, etaSeconds)
using SurfaceScanProgress = std::function<void(uint64_t sectorsScanned,
uint64_t totalSectors,
uint64_t badSectors,
double speedMBps,
double etaSeconds)>;
class SurfaceScan
{
public:
explicit SurfaceScan(RawDiskHandle& disk);
// Scan the entire disk
Result<SurfaceScanResults> scanDisk(
SurfaceScanMode mode = SurfaceScanMode::ReadOnly,
SurfaceScanProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
// Scan a specific partition (range of sectors)
Result<SurfaceScanResults> scanRange(
SectorOffset startLba,
SectorCount sectorCount,
SurfaceScanMode mode = SurfaceScanMode::ReadOnly,
SurfaceScanProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
private:
// Internal implementation shared by scanDisk and scanRange
Result<SurfaceScanResults> scanImpl(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
SurfaceScanMode mode,
SurfaceScanProgress progressCb,
std::atomic<bool>* cancelFlag);
RawDiskHandle& m_disk;
};
} // namespace spw

View File

@@ -0,0 +1,895 @@
#include "DiskEnumerator.h"
#include "RawDiskHandle.h"
// Windows headers for SetupAPI and WMI
#include <initguid.h> // Must come before devguid.h/ntddstor.h for GUID definitions
#include <setupapi.h>
#include <devguid.h>
#include <winioctl.h>
#include <comdef.h>
#include <Wbemidl.h>
#include <algorithm>
#include <sstream>
#include <memory>
// Link against required libraries
#pragma comment(lib, "setupapi.lib")
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
namespace spw
{
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
static ErrorInfo makeHResultError(ErrorCode code, HRESULT hr, const std::string& context)
{
std::ostringstream oss;
oss << context << " (HRESULT 0x" << std::hex << hr << ")";
return ErrorInfo::fromHResult(code, hr, oss.str());
}
// Trim trailing whitespace (common in WMI strings and STORAGE_DEVICE_DESCRIPTOR)
static std::wstring trimRight(const std::wstring& str)
{
auto end = str.find_last_not_of(L" \t\r\n");
if (end == std::wstring::npos) return L"";
return str.substr(0, end + 1);
}
// Convert a narrow ANSI string at an offset in a byte buffer to a wide string
static std::wstring narrowToWide(const char* narrowStr)
{
if (!narrowStr || narrowStr[0] == '\0') return L"";
int needed = ::MultiByteToWideChar(CP_ACP, 0, narrowStr, -1, nullptr, 0);
if (needed <= 0) return L"";
std::wstring result(static_cast<size_t>(needed), L'\0');
::MultiByteToWideChar(CP_ACP, 0, narrowStr, -1, &result[0], needed);
// Remove the null terminator that MultiByteToWideChar includes
if (!result.empty() && result.back() == L'\0')
result.pop_back();
return trimRight(result);
}
// ---------------------------------------------------------------------------
// RAII wrapper for COM initialization
// ---------------------------------------------------------------------------
class ComInitGuard
{
public:
ComInitGuard()
{
m_hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED);
}
~ComInitGuard()
{
if (SUCCEEDED(m_hr))
::CoUninitialize();
}
bool succeeded() const { return SUCCEEDED(m_hr); }
HRESULT result() const { return m_hr; }
ComInitGuard(const ComInitGuard&) = delete;
ComInitGuard& operator=(const ComInitGuard&) = delete;
private:
HRESULT m_hr;
};
// ---------------------------------------------------------------------------
// RAII wrapper for HDEVINFO
// ---------------------------------------------------------------------------
class DevInfoGuard
{
public:
explicit DevInfoGuard(HDEVINFO h) : m_handle(h) {}
~DevInfoGuard()
{
if (m_handle != INVALID_HANDLE_VALUE)
::SetupDiDestroyDeviceInfoList(m_handle);
}
HDEVINFO get() const { return m_handle; }
bool isValid() const { return m_handle != INVALID_HANDLE_VALUE; }
DevInfoGuard(const DevInfoGuard&) = delete;
DevInfoGuard& operator=(const DevInfoGuard&) = delete;
private:
HDEVINFO m_handle;
};
// ---------------------------------------------------------------------------
// Helper: get STORAGE_DEVICE_DESCRIPTOR for a physical drive
// ---------------------------------------------------------------------------
static bool getStorageDescriptor(HANDLE diskHandle,
std::wstring& outModel,
std::wstring& outSerial,
std::wstring& outFirmware,
bool& outRemovable)
{
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageDeviceProperty;
query.QueryType = PropertyStandardQuery;
// First call to get the needed size
STORAGE_DESCRIPTOR_HEADER header = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
&header, sizeof(header),
&bytesReturned, nullptr);
if (!ok || header.Size == 0) return false;
std::vector<uint8_t> buffer(header.Size, 0);
ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
buffer.data(), static_cast<DWORD>(buffer.size()),
&bytesReturned, nullptr);
if (!ok) return false;
const auto* desc = reinterpret_cast<const STORAGE_DEVICE_DESCRIPTOR*>(buffer.data());
if (desc->VendorIdOffset != 0)
{
const char* vendor = reinterpret_cast<const char*>(buffer.data()) + desc->VendorIdOffset;
std::wstring vendorW = narrowToWide(vendor);
if (!vendorW.empty())
outModel = vendorW + L" ";
}
if (desc->ProductIdOffset != 0)
{
const char* product = reinterpret_cast<const char*>(buffer.data()) + desc->ProductIdOffset;
outModel += narrowToWide(product);
}
outModel = trimRight(outModel);
if (desc->SerialNumberOffset != 0)
{
const char* serial = reinterpret_cast<const char*>(buffer.data()) + desc->SerialNumberOffset;
outSerial = narrowToWide(serial);
}
if (desc->ProductRevisionOffset != 0)
{
const char* rev = reinterpret_cast<const char*>(buffer.data()) + desc->ProductRevisionOffset;
outFirmware = narrowToWide(rev);
}
outRemovable = (desc->RemovableMedia != FALSE);
return true;
}
// ---------------------------------------------------------------------------
// Helper: Detect interface type from STORAGE_ADAPTER_DESCRIPTOR bus type
// ---------------------------------------------------------------------------
static DiskInterfaceType busTypeToInterface(STORAGE_BUS_TYPE busType)
{
switch (busType)
{
case BusTypeAta: return DiskInterfaceType::IDE;
case BusTypeSata: return DiskInterfaceType::SATA;
case BusTypeUsb: return DiskInterfaceType::USB;
case BusTypeScsi: return DiskInterfaceType::SCSI;
case BusTypeSas: return DiskInterfaceType::SAS;
case BusTypeNvme: return DiskInterfaceType::NVMe;
case BusTypeSd: return DiskInterfaceType::MMC;
case BusTypeMmc: return DiskInterfaceType::MMC;
case BusType1394: return DiskInterfaceType::Firewire;
case BusTypeVirtual: return DiskInterfaceType::Virtual;
case BusTypeFileBackedVirtual: return DiskInterfaceType::Virtual;
default: return DiskInterfaceType::Unknown;
}
}
// ---------------------------------------------------------------------------
// Helper: Get bus type via STORAGE_ADAPTER_DESCRIPTOR
// ---------------------------------------------------------------------------
static DiskInterfaceType getInterfaceType(HANDLE diskHandle)
{
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageAdapterProperty;
query.QueryType = PropertyStandardQuery;
STORAGE_DESCRIPTOR_HEADER header = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
&header, sizeof(header),
&bytesReturned, nullptr);
if (!ok || header.Size == 0) return DiskInterfaceType::Unknown;
std::vector<uint8_t> buffer(header.Size, 0);
ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
buffer.data(), static_cast<DWORD>(buffer.size()),
&bytesReturned, nullptr);
if (!ok) return DiskInterfaceType::Unknown;
const auto* desc = reinterpret_cast<const STORAGE_ADAPTER_DESCRIPTOR*>(buffer.data());
return busTypeToInterface(static_cast<STORAGE_BUS_TYPE>(desc->BusType));
}
// ---------------------------------------------------------------------------
// Helper: Detect if disk is SSD using IOCTL_ATA_PASS_THROUGH (IDENTIFY DEVICE)
// or by checking the seek penalty via IOCTL_STORAGE_QUERY_PROPERTY.
// ---------------------------------------------------------------------------
static MediaType detectMediaType(HANDLE diskHandle, DiskInterfaceType ifType, bool isRemovable)
{
if (isRemovable)
{
if (ifType == DiskInterfaceType::USB) return MediaType::USBFlash;
if (ifType == DiskInterfaceType::MMC) return MediaType::SDCard;
}
if (ifType == DiskInterfaceType::NVMe) return MediaType::NVMe;
if (ifType == DiskInterfaceType::Virtual) return MediaType::Virtual;
// Use IOCTL_STORAGE_QUERY_PROPERTY with StorageDeviceSeekPenaltyProperty
// to determine if the device has no seek penalty (SSD) or has one (HDD).
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageDeviceSeekPenaltyProperty;
query.QueryType = PropertyStandardQuery;
DEVICE_SEEK_PENALTY_DESCRIPTOR seekDesc = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
&seekDesc, sizeof(seekDesc),
&bytesReturned, nullptr);
if (ok && bytesReturned >= sizeof(DEVICE_SEEK_PENALTY_DESCRIPTOR))
{
return seekDesc.IncursSeekPenalty ? MediaType::HDD : MediaType::SSD;
}
// Fallback: unknown
return MediaType::Unknown;
}
// ---------------------------------------------------------------------------
// Enumerate physical disks
// ---------------------------------------------------------------------------
Result<std::vector<DiskInfo>> DiskEnumerator::enumerateDisks()
{
std::vector<DiskInfo> disks;
// Strategy: Try SetupDiGetClassDevs with GUID_DEVINTERFACE_DISK first to get
// device paths, then fall back to iterating PhysicalDrive0..31 for any disks
// not found via SetupAPI.
// Phase 1: SetupAPI enumeration
DevInfoGuard devInfo(::SetupDiGetClassDevsW(
&GUID_DEVINTERFACE_DISK,
nullptr, nullptr,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE));
std::vector<int> foundIndices;
if (devInfo.isValid())
{
SP_DEVICE_INTERFACE_DATA ifData = {};
ifData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD idx = 0;
::SetupDiEnumDeviceInterfaces(devInfo.get(), nullptr,
&GUID_DEVINTERFACE_DISK, idx, &ifData);
++idx)
{
// Get required buffer size for the detail struct
DWORD detailSize = 0;
::SetupDiGetDeviceInterfaceDetailW(devInfo.get(), &ifData,
nullptr, 0, &detailSize, nullptr);
if (detailSize == 0) continue;
std::vector<uint8_t> detailBuf(detailSize, 0);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(detailBuf.data());
detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
SP_DEVINFO_DATA devInfoData = {};
devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
if (!::SetupDiGetDeviceInterfaceDetailW(devInfo.get(), &ifData,
detail, detailSize, nullptr, &devInfoData))
{
continue;
}
std::wstring devicePath = detail->DevicePath;
// Open the device to query properties
HANDLE hDisk = ::CreateFileW(
devicePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hDisk == INVALID_HANDLE_VALUE) continue;
// Get disk number to determine DiskId
STORAGE_DEVICE_NUMBER deviceNumber = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(hDisk, IOCTL_STORAGE_GET_DEVICE_NUMBER,
nullptr, 0,
&deviceNumber, sizeof(deviceNumber),
&bytesReturned, nullptr);
if (!ok || deviceNumber.DeviceType != FILE_DEVICE_DISK)
{
::CloseHandle(hDisk);
continue;
}
DiskInfo info;
info.id = static_cast<DiskId>(deviceNumber.DeviceNumber);
info.devicePath = devicePath;
// Get geometry
uint8_t geomBuf[256] = {};
ok = ::DeviceIoControl(hDisk, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
nullptr, 0, geomBuf, sizeof(geomBuf),
&bytesReturned, nullptr);
if (ok)
{
const auto* geomEx = reinterpret_cast<const DISK_GEOMETRY_EX*>(geomBuf);
info.sizeBytes = static_cast<uint64_t>(geomEx->DiskSize.QuadPart);
info.sectorSize = geomEx->Geometry.BytesPerSector;
}
// Get model, serial, firmware, removable flag
getStorageDescriptor(hDisk, info.model, info.serialNumber,
info.firmwareRevision, info.isRemovable);
// Get interface type
info.interfaceType = getInterfaceType(hDisk);
// Detect media type (SSD vs HDD)
info.mediaType = detectMediaType(hDisk, info.interfaceType, info.isRemovable);
// Get partition table type
constexpr size_t kLayoutBufSize = sizeof(DRIVE_LAYOUT_INFORMATION_EX)
+ 128 * sizeof(PARTITION_INFORMATION_EX);
std::vector<uint8_t> layoutBuf(kLayoutBufSize, 0);
ok = ::DeviceIoControl(hDisk, IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
nullptr, 0,
layoutBuf.data(), static_cast<DWORD>(layoutBuf.size()),
&bytesReturned, nullptr);
if (ok)
{
const auto* layout =
reinterpret_cast<const DRIVE_LAYOUT_INFORMATION_EX*>(layoutBuf.data());
switch (layout->PartitionStyle)
{
case PARTITION_STYLE_MBR: info.partitionTableType = PartitionTableType::MBR; break;
case PARTITION_STYLE_GPT: info.partitionTableType = PartitionTableType::GPT; break;
default: info.partitionTableType = PartitionTableType::Unknown; break;
}
}
::CloseHandle(hDisk);
foundIndices.push_back(info.id);
disks.push_back(std::move(info));
}
}
// Phase 2: Fallback — try PhysicalDrive0..31 for any we missed
for (int driveIdx = 0; driveIdx < 32; ++driveIdx)
{
// Skip indices already found by SetupAPI
if (std::find(foundIndices.begin(), foundIndices.end(), driveIdx) != foundIndices.end())
continue;
std::wostringstream pathStream;
pathStream << L"\\\\.\\PhysicalDrive" << driveIdx;
std::wstring drivePath = pathStream.str();
HANDLE hDisk = ::CreateFileW(
drivePath.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hDisk == INVALID_HANDLE_VALUE) continue;
DiskInfo info;
info.id = driveIdx;
info.devicePath = drivePath;
uint8_t geomBuf[256] = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(hDisk, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
nullptr, 0, geomBuf, sizeof(geomBuf),
&bytesReturned, nullptr);
if (ok)
{
const auto* geomEx = reinterpret_cast<const DISK_GEOMETRY_EX*>(geomBuf);
info.sizeBytes = static_cast<uint64_t>(geomEx->DiskSize.QuadPart);
info.sectorSize = geomEx->Geometry.BytesPerSector;
}
getStorageDescriptor(hDisk, info.model, info.serialNumber,
info.firmwareRevision, info.isRemovable);
info.interfaceType = getInterfaceType(hDisk);
info.mediaType = detectMediaType(hDisk, info.interfaceType, info.isRemovable);
// Partition table type
constexpr size_t kLayoutBufSize = sizeof(DRIVE_LAYOUT_INFORMATION_EX)
+ 128 * sizeof(PARTITION_INFORMATION_EX);
std::vector<uint8_t> layoutBuf(kLayoutBufSize, 0);
ok = ::DeviceIoControl(hDisk, IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
nullptr, 0,
layoutBuf.data(), static_cast<DWORD>(layoutBuf.size()),
&bytesReturned, nullptr);
if (ok)
{
const auto* layout =
reinterpret_cast<const DRIVE_LAYOUT_INFORMATION_EX*>(layoutBuf.data());
switch (layout->PartitionStyle)
{
case PARTITION_STYLE_MBR: info.partitionTableType = PartitionTableType::MBR; break;
case PARTITION_STYLE_GPT: info.partitionTableType = PartitionTableType::GPT; break;
default: info.partitionTableType = PartitionTableType::Unknown; break;
}
}
::CloseHandle(hDisk);
disks.push_back(std::move(info));
}
// Sort by disk index for consistent ordering
std::sort(disks.begin(), disks.end(),
[](const DiskInfo& a, const DiskInfo& b) { return a.id < b.id; });
return disks;
}
// ---------------------------------------------------------------------------
// Enumerate all volumes using FindFirstVolumeW / FindNextVolumeW
// ---------------------------------------------------------------------------
Result<std::vector<VolumeInfo>> DiskEnumerator::enumerateVolumes()
{
std::vector<VolumeInfo> volumes;
wchar_t volumeNameBuf[MAX_PATH] = {};
HANDLE findHandle = ::FindFirstVolumeW(volumeNameBuf, MAX_PATH);
if (findHandle == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::DiskReadError, "FindFirstVolumeW failed");
}
do
{
VolumeInfo vol;
vol.guidPath = volumeNameBuf;
// Get mount points (drive letters and folder mounts)
DWORD pathNamesLen = 0;
// First call to get needed buffer size
::GetVolumePathNamesForVolumeNameW(volumeNameBuf, nullptr, 0, &pathNamesLen);
if (pathNamesLen > 0)
{
std::vector<wchar_t> pathNames(pathNamesLen, L'\0');
if (::GetVolumePathNamesForVolumeNameW(volumeNameBuf, pathNames.data(),
pathNamesLen, &pathNamesLen))
{
// The result is a multi-string: each path terminated by L'\0',
// with an extra L'\0' at the end.
const wchar_t* current = pathNames.data();
while (*current != L'\0')
{
vol.mountPoints.push_back(current);
current += wcslen(current) + 1;
}
}
}
// Get filesystem info using the volume GUID path (needs trailing backslash)
std::wstring rootPath = volumeNameBuf; // Already has trailing backslash from FindFirstVolumeW
wchar_t fsLabel[MAX_PATH + 1] = {};
wchar_t fsName[MAX_PATH + 1] = {};
DWORD serialNumber = 0;
DWORD maxComponentLen = 0;
DWORD fsFlags = 0;
if (::GetVolumeInformationW(rootPath.c_str(),
fsLabel, MAX_PATH,
&serialNumber,
&maxComponentLen,
&fsFlags,
fsName, MAX_PATH))
{
vol.filesystemLabel = fsLabel;
vol.filesystemName = fsName;
}
// Get total and free space
ULARGE_INTEGER freeBytesAvail = {};
ULARGE_INTEGER totalBytes = {};
ULARGE_INTEGER totalFreeBytes = {};
if (::GetDiskFreeSpaceExW(rootPath.c_str(),
&freeBytesAvail, &totalBytes, &totalFreeBytes))
{
vol.totalBytes = totalBytes.QuadPart;
vol.freeBytes = totalFreeBytes.QuadPart;
}
volumes.push_back(std::move(vol));
} while (::FindNextVolumeW(findHandle, volumeNameBuf, MAX_PATH));
::FindVolumeClose(findHandle);
return volumes;
}
// ---------------------------------------------------------------------------
// WMI helper: extract a string property from a WMI object
// ---------------------------------------------------------------------------
static std::wstring getWmiString(IWbemClassObject* obj, const wchar_t* propName)
{
VARIANT vtProp;
::VariantInit(&vtProp);
HRESULT hr = obj->Get(propName, 0, &vtProp, nullptr, nullptr);
if (FAILED(hr) || vtProp.vt == VT_NULL)
{
::VariantClear(&vtProp);
return L"";
}
std::wstring result;
if (vtProp.vt == VT_BSTR && vtProp.bstrVal)
{
result = vtProp.bstrVal;
}
::VariantClear(&vtProp);
return result;
}
static uint64_t getWmiUint64(IWbemClassObject* obj, const wchar_t* propName)
{
VARIANT vtProp;
::VariantInit(&vtProp);
HRESULT hr = obj->Get(propName, 0, &vtProp, nullptr, nullptr);
if (FAILED(hr) || vtProp.vt == VT_NULL)
{
::VariantClear(&vtProp);
return 0;
}
uint64_t result = 0;
if (vtProp.vt == VT_BSTR && vtProp.bstrVal)
{
// WMI returns large integers as strings
result = _wcstoui64(vtProp.bstrVal, nullptr, 10);
}
else if (vtProp.vt == VT_I4 || vtProp.vt == VT_UI4)
{
result = static_cast<uint64_t>(vtProp.ulVal);
}
::VariantClear(&vtProp);
return result;
}
static uint32_t getWmiUint32(IWbemClassObject* obj, const wchar_t* propName)
{
return static_cast<uint32_t>(getWmiUint64(obj, propName));
}
static bool getWmiBool(IWbemClassObject* obj, const wchar_t* propName)
{
VARIANT vtProp;
::VariantInit(&vtProp);
HRESULT hr = obj->Get(propName, 0, &vtProp, nullptr, nullptr);
if (FAILED(hr) || vtProp.vt == VT_NULL)
{
::VariantClear(&vtProp);
return false;
}
bool result = false;
if (vtProp.vt == VT_BOOL)
{
result = (vtProp.boolVal != VARIANT_FALSE);
}
::VariantClear(&vtProp);
return result;
}
// ---------------------------------------------------------------------------
// Use WMI to enumerate partitions with full disk->partition->volume mapping.
// This is the most reliable way to get the partition-to-drive-letter mapping.
// ---------------------------------------------------------------------------
Result<std::vector<PartitionInfo>> DiskEnumerator::enumeratePartitionsWmi()
{
ComInitGuard comGuard;
if (!comGuard.succeeded() && comGuard.result() != RPC_E_CHANGED_MODE)
{
return makeHResultError(ErrorCode::WmiQueryFailed, comGuard.result(),
"COM initialization failed");
}
// Set COM security. If already set, S_FALSE or RPC_E_TOO_LATE is acceptable.
HRESULT hr = ::CoInitializeSecurity(
nullptr, -1, nullptr, nullptr,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
nullptr, EOAC_NONE, nullptr);
if (FAILED(hr) && hr != RPC_E_TOO_LATE)
{
// Non-fatal: proceed anyway, some queries may still work
}
// Connect to WMI
IWbemLocator* pLocator = nullptr;
hr = ::CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
IID_IWbemLocator, reinterpret_cast<void**>(&pLocator));
if (FAILED(hr))
{
return makeHResultError(ErrorCode::WmiQueryFailed, hr,
"Failed to create WMI locator");
}
IWbemServices* pServices = nullptr;
hr = pLocator->ConnectServer(
_bstr_t(L"ROOT\\CIMV2"), nullptr, nullptr, nullptr, 0, nullptr, nullptr, &pServices);
if (FAILED(hr))
{
pLocator->Release();
return makeHResultError(ErrorCode::WmiQueryFailed, hr,
"Failed to connect to WMI ROOT\\CIMV2");
}
// Set proxy security on the WMI connection
hr = ::CoSetProxyBlanket(pServices,
RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr,
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
nullptr, EOAC_NONE);
std::vector<PartitionInfo> partitions;
// Step 1: Query Win32_DiskPartition for partition details
IEnumWbemClassObject* pPartEnum = nullptr;
hr = pServices->ExecQuery(
_bstr_t(L"WQL"),
_bstr_t(L"SELECT * FROM Win32_DiskPartition"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr, &pPartEnum);
if (SUCCEEDED(hr))
{
IWbemClassObject* pObj = nullptr;
ULONG numReturned = 0;
while (pPartEnum->Next(WBEM_INFINITE, 1, &pObj, &numReturned) == S_OK)
{
PartitionInfo pi;
pi.diskId = static_cast<DiskId>(getWmiUint32(pObj, L"DiskIndex"));
pi.index = static_cast<PartitionId>(getWmiUint32(pObj, L"Index"));
pi.offsetBytes = getWmiUint64(pObj, L"StartingOffset");
pi.sizeBytes = getWmiUint64(pObj, L"Size");
pi.isBootable = getWmiBool(pObj, L"Bootable");
pi.isActive = getWmiBool(pObj, L"BootPartition");
std::wstring partType = getWmiString(pObj, L"Type");
// WMI Type field has format like "GPT: Basic Data" or "Installable File System"
if (partType.find(L"GPT") != std::wstring::npos)
{
// GPT partition — type string varies; we will get the actual GUID from
// IOCTL_DISK_GET_DRIVE_LAYOUT_EX, but the WMI type gives us hints
}
std::wstring deviceId = getWmiString(pObj, L"DeviceID");
// DeviceID is like "Disk #0, Partition #1"
partitions.push_back(std::move(pi));
pObj->Release();
}
pPartEnum->Release();
}
// Step 2: Query Win32_LogicalDiskToPartition for drive letter mapping.
// This WMI associator maps "Win32_DiskPartition.DeviceID" to "Win32_LogicalDisk.DeviceID".
IEnumWbemClassObject* pAssocEnum = nullptr;
hr = pServices->ExecQuery(
_bstr_t(L"WQL"),
_bstr_t(L"SELECT * FROM Win32_LogicalDiskToPartition"),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr, &pAssocEnum);
if (SUCCEEDED(hr))
{
IWbemClassObject* pObj = nullptr;
ULONG numReturned = 0;
while (pAssocEnum->Next(WBEM_INFINITE, 1, &pObj, &numReturned) == S_OK)
{
// Antecedent is the partition, Dependent is the logical disk
std::wstring antecedent = getWmiString(pObj, L"Antecedent");
std::wstring dependent = getWmiString(pObj, L"Dependent");
// Parse disk index and partition index from the Antecedent string.
// Format: \\HOSTNAME\root\cimv2:Win32_DiskPartition.DeviceID="Disk #0, Partition #1"
int diskIdx = -1, partIdx = -1;
auto diskPos = antecedent.find(L"Disk #");
auto partPos = antecedent.find(L"Partition #");
if (diskPos != std::wstring::npos)
diskIdx = _wtoi(antecedent.c_str() + diskPos + 6);
if (partPos != std::wstring::npos)
partIdx = _wtoi(antecedent.c_str() + partPos + 11);
// Parse drive letter from Dependent.
// Format: \\HOSTNAME\root\cimv2:Win32_LogicalDisk.DeviceID="C:"
wchar_t driveLetter = L'\0';
auto quotePos = dependent.rfind(L'"');
if (quotePos != std::wstring::npos && quotePos >= 2)
{
// The character before the last quote should be ':'
if (dependent[quotePos - 1] == L':')
driveLetter = dependent[quotePos - 2];
}
// Match to our partition list
if (diskIdx >= 0 && partIdx >= 0 && driveLetter != L'\0')
{
for (auto& part : partitions)
{
if (part.diskId == diskIdx && part.index == partIdx)
{
part.driveLetter = driveLetter;
// Also look up the volume GUID path for this drive letter
wchar_t rootPath[] = L"X:\\";
rootPath[0] = driveLetter;
wchar_t guidBuf[MAX_PATH] = {};
if (::GetVolumeNameForVolumeMountPointW(rootPath, guidBuf, MAX_PATH))
{
part.volumeGuidPath = guidBuf;
}
// Get filesystem label and type
wchar_t labelBuf[MAX_PATH + 1] = {};
wchar_t fsBuf[MAX_PATH + 1] = {};
if (::GetVolumeInformationW(rootPath, labelBuf, MAX_PATH,
nullptr, nullptr, nullptr,
fsBuf, MAX_PATH))
{
part.label = labelBuf;
part.filesystemType = classifyFilesystem(fsBuf);
}
break;
}
}
}
pObj->Release();
}
pAssocEnum->Release();
}
pServices->Release();
pLocator->Release();
return partitions;
}
// ---------------------------------------------------------------------------
// Full system snapshot
// ---------------------------------------------------------------------------
Result<SystemDiskSnapshot> DiskEnumerator::getSystemSnapshot()
{
SystemDiskSnapshot snapshot;
auto disksResult = enumerateDisks();
if (disksResult.isError()) return disksResult.error();
snapshot.disks = std::move(disksResult.value());
auto volumesResult = enumerateVolumes();
if (volumesResult.isError()) return volumesResult.error();
snapshot.volumes = std::move(volumesResult.value());
auto partitionsResult = enumeratePartitionsWmi();
if (partitionsResult.isError()) return partitionsResult.error();
snapshot.partitions = std::move(partitionsResult.value());
return snapshot;
}
// ---------------------------------------------------------------------------
// Get info for a single disk
// ---------------------------------------------------------------------------
Result<DiskInfo> DiskEnumerator::getDiskInfo(DiskId diskIndex)
{
auto allDisks = enumerateDisks();
if (allDisks.isError()) return allDisks.error();
for (auto& disk : allDisks.value())
{
if (disk.id == diskIndex)
return std::move(disk);
}
return ErrorInfo::fromCode(ErrorCode::DiskNotFound, "Physical disk not found");
}
// ---------------------------------------------------------------------------
// Classify interface type from WMI string
// ---------------------------------------------------------------------------
DiskInterfaceType DiskEnumerator::classifyInterfaceType(const std::wstring& wmiInterfaceType)
{
if (wmiInterfaceType == L"IDE" || wmiInterfaceType == L"ATA")
return DiskInterfaceType::IDE;
if (wmiInterfaceType == L"SCSI")
return DiskInterfaceType::SCSI;
if (wmiInterfaceType == L"USB")
return DiskInterfaceType::USB;
if (wmiInterfaceType == L"1394")
return DiskInterfaceType::Firewire;
if (wmiInterfaceType == L"SAS")
return DiskInterfaceType::SAS;
return DiskInterfaceType::Unknown;
}
// ---------------------------------------------------------------------------
// Classify media type from WMI and interface hints
// ---------------------------------------------------------------------------
MediaType DiskEnumerator::classifyMediaType(const std::wstring& wmiMediaType,
DiskInterfaceType ifType)
{
if (ifType == DiskInterfaceType::NVMe) return MediaType::NVMe;
if (ifType == DiskInterfaceType::USB) return MediaType::USBFlash;
if (ifType == DiskInterfaceType::MMC) return MediaType::SDCard;
if (wmiMediaType.find(L"Fixed") != std::wstring::npos) return MediaType::HDD;
if (wmiMediaType.find(L"Removable") != std::wstring::npos) return MediaType::USBFlash;
if (wmiMediaType.find(L"External") != std::wstring::npos) return MediaType::HDD;
return MediaType::Unknown;
}
// ---------------------------------------------------------------------------
// Classify filesystem name string to enum
// ---------------------------------------------------------------------------
FilesystemType DiskEnumerator::classifyFilesystem(const std::wstring& fsName)
{
if (fsName == L"NTFS") return FilesystemType::NTFS;
if (fsName == L"FAT32") return FilesystemType::FAT32;
if (fsName == L"FAT16") return FilesystemType::FAT16;
if (fsName == L"FAT12") return FilesystemType::FAT12;
if (fsName == L"FAT") return FilesystemType::FAT16; // Windows reports FAT for FAT12/16
if (fsName == L"exFAT") return FilesystemType::ExFAT;
if (fsName == L"ReFS") return FilesystemType::ReFS;
if (fsName == L"UDF") return FilesystemType::UDF;
if (fsName == L"CDFS") return FilesystemType::ISO9660;
if (fsName == L"ext2") return FilesystemType::Ext2;
if (fsName == L"ext3") return FilesystemType::Ext3;
if (fsName == L"ext4") return FilesystemType::Ext4;
if (fsName == L"Btrfs") return FilesystemType::Btrfs;
if (fsName == L"HPFS") return FilesystemType::HPFS;
return FilesystemType::Unknown;
}
} // namespace spw

View File

@@ -0,0 +1,112 @@
#pragma once
// DiskEnumerator — Enumerates physical disks, partitions, and volumes on Windows.
// Uses SetupAPI for physical disk discovery, WMI for disk-partition-volume mapping,
// and FindFirstVolumeW/FindNextVolumeW for volume enumeration.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <cstdint>
#include <string>
#include <vector>
namespace spw
{
// Information about a physical disk
struct DiskInfo
{
DiskId id = -1; // Physical drive index
std::wstring model; // e.g. L"Samsung SSD 970 EVO Plus"
std::wstring serialNumber;
std::wstring firmwareRevision;
uint64_t sizeBytes = 0;
uint32_t sectorSize = 512;
DiskInterfaceType interfaceType = DiskInterfaceType::Unknown;
MediaType mediaType = MediaType::Unknown;
bool isRemovable = false;
PartitionTableType partitionTableType = PartitionTableType::Unknown;
std::wstring devicePath; // e.g. L"\\.\PhysicalDrive0"
};
// Information about a partition on a disk
struct PartitionInfo
{
DiskId diskId = -1;
PartitionId index = -1;
uint64_t offsetBytes = 0;
uint64_t sizeBytes = 0;
FilesystemType filesystemType = FilesystemType::Unknown;
std::wstring label;
wchar_t driveLetter = L'\0'; // L'\0' if no drive letter
std::wstring volumeGuidPath; // e.g. L"\\?\Volume{GUID}\"
bool isActive = false;
bool isBootable = false;
// MBR-specific
uint8_t mbrType = 0;
// GPT-specific
Guid gptTypeGuid;
Guid gptPartitionGuid;
};
// Information about a mounted volume
struct VolumeInfo
{
std::wstring guidPath; // e.g. L"\\?\Volume{GUID}\"
std::vector<std::wstring> mountPoints; // Drive letters and folder mounts
std::wstring filesystemLabel;
std::wstring filesystemName; // e.g. L"NTFS"
uint64_t totalBytes = 0;
uint64_t freeBytes = 0;
};
// Complete system disk snapshot
struct SystemDiskSnapshot
{
std::vector<DiskInfo> disks;
std::vector<PartitionInfo> partitions;
std::vector<VolumeInfo> volumes;
};
namespace DiskEnumerator
{
// Enumerate all physical disks. Uses SetupAPI (SetupDiGetClassDevs) with
// GUID_DEVINTERFACE_DISK and falls back to iterating PhysicalDrive0..31.
Result<std::vector<DiskInfo>> enumerateDisks();
// Enumerate all volumes using FindFirstVolumeW/FindNextVolumeW and
// GetVolumePathNamesForVolumeNameW for mount points.
Result<std::vector<VolumeInfo>> enumerateVolumes();
// Use WMI to build the full disk -> partition -> volume mapping.
// Queries Win32_DiskDrive, Win32_DiskDriveToDiskPartition, Win32_LogicalDiskToPartition.
Result<std::vector<PartitionInfo>> enumeratePartitionsWmi();
// Full system snapshot combining all three enumerations.
Result<SystemDiskSnapshot> getSystemSnapshot();
// Get info for a single disk by index.
Result<DiskInfo> getDiskInfo(DiskId diskIndex);
// Helper: classify interface type from a WMI InterfaceType string
DiskInterfaceType classifyInterfaceType(const std::wstring& wmiInterfaceType);
// Helper: classify media type from WMI MediaType string and interface hints
MediaType classifyMediaType(const std::wstring& wmiMediaType, DiskInterfaceType ifType);
// Helper: convert WMI filesystem name string to FilesystemType enum
FilesystemType classifyFilesystem(const std::wstring& fsName);
} // namespace DiskEnumerator
} // namespace spw

View File

@@ -0,0 +1,140 @@
#include "DiskGeometry.h"
namespace spw
{
namespace DiskGeometry
{
Result<SectorOffset> chsToLba(const CHSAddress& chs, const CHSGeometry& geometry)
{
if (geometry.headsPerCylinder == 0 || geometry.sectorsPerTrack == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"CHS geometry has zero heads or sectors per track");
}
// CHS sector numbers are 1-based; sector 0 is invalid
if (chs.sector == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"CHS sector number must be >= 1 (1-based addressing)");
}
// LBA = (C * HPC * SPT) + (H * SPT) + (S - 1)
uint64_t lba = static_cast<uint64_t>(chs.cylinder)
* geometry.headsPerCylinder
* geometry.sectorsPerTrack;
lba += static_cast<uint64_t>(chs.head) * geometry.sectorsPerTrack;
lba += static_cast<uint64_t>(chs.sector) - 1;
return lba;
}
Result<CHSAddress> lbaToChs(SectorOffset lba, const CHSGeometry& geometry)
{
if (geometry.headsPerCylinder == 0 || geometry.sectorsPerTrack == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"CHS geometry has zero heads or sectors per track");
}
const uint64_t headsTimeSectors =
static_cast<uint64_t>(geometry.headsPerCylinder) * geometry.sectorsPerTrack;
CHSAddress result;
result.cylinder = static_cast<uint32_t>(lba / headsTimeSectors);
uint64_t remainder = lba % headsTimeSectors;
result.head = static_cast<uint8_t>(remainder / geometry.sectorsPerTrack);
// +1 because CHS sectors are 1-based
result.sector = static_cast<uint8_t>((remainder % geometry.sectorsPerTrack) + 1);
return result;
}
uint32_t normalizeSectorSize(uint32_t reportedSize)
{
// Common physical sector sizes
switch (reportedSize)
{
case 512:
case 1024:
case 2048:
case 4096:
return reportedSize;
default:
// If the reported size is a power of two and in a sane range, accept it
if (reportedSize >= 512 && reportedSize <= 4096 &&
(reportedSize & (reportedSize - 1)) == 0)
{
return reportedSize;
}
return DEFAULT_SECTOR_SIZE;
}
}
bool isAligned(uint64_t byteOffset, uint64_t alignment)
{
if (alignment == 0) return true;
return (byteOffset % alignment) == 0;
}
bool isSectorAligned(SectorOffset lba, SectorCount alignmentSectors)
{
if (alignmentSectors == 0) return true;
return (lba % alignmentSectors) == 0;
}
uint64_t alignUp(uint64_t byteOffset, uint64_t alignment)
{
if (alignment == 0) return byteOffset;
const uint64_t remainder = byteOffset % alignment;
if (remainder == 0) return byteOffset;
return byteOffset + (alignment - remainder);
}
uint64_t alignDown(uint64_t byteOffset, uint64_t alignment)
{
if (alignment == 0) return byteOffset;
return byteOffset - (byteOffset % alignment);
}
SectorOffset alignSectorUp(SectorOffset lba, SectorCount alignmentSectors)
{
if (alignmentSectors == 0) return lba;
const SectorOffset remainder = lba % alignmentSectors;
if (remainder == 0) return lba;
return lba + (alignmentSectors - remainder);
}
SectorOffset alignSectorDown(SectorOffset lba, SectorCount alignmentSectors)
{
if (alignmentSectors == 0) return lba;
return lba - (lba % alignmentSectors);
}
uint64_t totalCapacity(SectorCount sectorCount, uint32_t sectorSize)
{
return sectorCount * static_cast<uint64_t>(sectorSize);
}
SectorCount bytesToSectors(uint64_t bytes, uint32_t sectorSize)
{
if (sectorSize == 0) return 0;
return bytes / sectorSize;
}
SectorCount defaultAlignmentSectors(uint32_t sectorSize)
{
if (sectorSize == 0) return 0;
return DEFAULT_ALIGNMENT_BYTES / sectorSize;
}
SectorOffset optimalPartitionStart(SectorOffset desiredLba, uint32_t sectorSize)
{
const SectorCount alignment = defaultAlignmentSectors(sectorSize);
if (alignment == 0) return desiredLba;
return alignSectorUp(desiredLba, alignment);
}
} // namespace DiskGeometry
} // namespace spw

View File

@@ -0,0 +1,84 @@
#pragma once
// DiskGeometry — CHS/LBA conversion, alignment checking, and capacity calculations.
// Reference: ATA/ATAPI Command Set (ACS-3), Section 6.2 for CHS addressing.
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../common/Constants.h"
#include <cstdint>
namespace spw
{
// Cylinder-Head-Sector address
struct CHSAddress
{
uint32_t cylinder = 0;
uint8_t head = 0;
uint8_t sector = 0; // 1-based (CHS sectors start at 1, not 0)
};
// Geometry parameters needed for CHS<->LBA conversion
struct CHSGeometry
{
uint32_t headsPerCylinder = 0;
uint32_t sectorsPerTrack = 0;
};
namespace DiskGeometry
{
// Convert CHS address to LBA.
// Formula: LBA = (C * HPC * SPT) + (H * SPT) + (S - 1)
// where HPC = heads per cylinder, SPT = sectors per track.
// Returns error if geometry parameters are zero (division by zero guard).
Result<SectorOffset> chsToLba(const CHSAddress& chs, const CHSGeometry& geometry);
// Convert LBA to CHS address.
// This is the inverse: C = LBA / (HPC * SPT), H = (LBA / SPT) % HPC, S = (LBA % SPT) + 1
Result<CHSAddress> lbaToChs(SectorOffset lba, const CHSGeometry& geometry);
// Detect the physical sector size from a given value.
// Returns SECTOR_SIZE_512 or SECTOR_SIZE_4K.
// Falls back to DEFAULT_SECTOR_SIZE if the value is not recognized.
uint32_t normalizeSectorSize(uint32_t reportedSize);
// Check if a byte offset is aligned to a given alignment boundary.
bool isAligned(uint64_t byteOffset, uint64_t alignment);
// Check if an LBA is aligned to a given sector count boundary.
bool isSectorAligned(SectorOffset lba, SectorCount alignmentSectors);
// Round a byte offset UP to the next alignment boundary.
// Returns the offset unchanged if it is already aligned.
uint64_t alignUp(uint64_t byteOffset, uint64_t alignment);
// Round a byte offset DOWN to the previous alignment boundary.
uint64_t alignDown(uint64_t byteOffset, uint64_t alignment);
// Round an LBA up to the next aligned sector.
SectorOffset alignSectorUp(SectorOffset lba, SectorCount alignmentSectors);
// Round an LBA down to the previous aligned sector.
SectorOffset alignSectorDown(SectorOffset lba, SectorCount alignmentSectors);
// Calculate total capacity in bytes from sector count and sector size.
uint64_t totalCapacity(SectorCount sectorCount, uint32_t sectorSize);
// Calculate the number of sectors that fit in a given byte count.
// Rounds DOWN — partial sectors are not counted.
SectorCount bytesToSectors(uint64_t bytes, uint32_t sectorSize);
// Calculate the default alignment in sectors for a given sector size.
// Uses DEFAULT_ALIGNMENT_BYTES (1 MiB).
SectorCount defaultAlignmentSectors(uint32_t sectorSize);
// Calculate optimal partition start for a given desired LBA, respecting alignment.
// Returns the next aligned LBA >= desiredLba.
SectorOffset optimalPartitionStart(SectorOffset desiredLba, uint32_t sectorSize);
} // namespace DiskGeometry
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
#pragma once
// FilesystemDetector — Identifies filesystem type by reading magic bytes and structural signatures.
//
// Checks a comprehensive set of filesystem signatures covering modern, legacy, and exotic
// filesystem types. Detection works by reading specific byte offsets from the target volume
// and matching against known magic values.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../common/Constants.h"
#include "PartitionTable.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Summary of detection result
struct FilesystemDetection
{
FilesystemType type = FilesystemType::Unknown;
std::string label; // Volume label if readable during detection
std::string uuid; // UUID/serial if readable
uint32_t blockSize = 0; // Block/cluster size if determinable
std::string description; // Human-readable filesystem name
bool isDetected() const { return type != FilesystemType::Unknown; }
};
// ============================================================================
// FilesystemDetector — static methods for filesystem identification
// ============================================================================
class FilesystemDetector
{
public:
// Detect the filesystem type present at the given read callback.
// The callback reads raw bytes from the start of the volume/partition.
// Parameters:
// readFunc — reads (offset, size) relative to partition/volume start
// volumeSize — total size of partition in bytes (0 if unknown)
static Result<FilesystemDetection> detect(
const DiskReadCallback& readFunc,
uint64_t volumeSize = 0);
// Detect from a raw buffer (useful for testing or when data is already in memory).
// The buffer should contain at least the first 128 KiB of the volume for reliable detection.
// For Btrfs, 72 KiB is needed (superblock at 0x10000 + 64 bytes).
static Result<FilesystemDetection> detectFromBuffer(
const std::vector<uint8_t>& data,
uint64_t volumeSize = 0);
// Get a human-readable name for a FilesystemType
static const char* filesystemName(FilesystemType type);
private:
// Individual detection routines — each returns true if the filesystem was positively identified
static bool detectNtfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectFat(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectExfat(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectExt(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectBtrfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectXfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectHfsPlus(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectApfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectReFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectIso9660(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectUdf(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectReiserFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectJfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectHpfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectMinix(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectUfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectBfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectQnx4(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectZfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectSquashFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectCramFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectRomFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
static bool detectLinuxSwap(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize);
// Helper: safely read bytes through the callback, returning empty vector on failure
static std::vector<uint8_t> safeRead(const DiskReadCallback& readFunc, uint64_t offset, uint32_t size);
};
} // namespace spw

View File

@@ -0,0 +1,922 @@
// FilesystemInfo.cpp — Reads detailed filesystem metadata from on-disk structures.
//
// After FilesystemDetector identifies the filesystem type, this module reads the
// relevant superblock/BPB/volume header to extract label, UUID, sizes, feature flags,
// and other metadata. Each filesystem stores this information at different offsets
// in different formats — this is the single place that knows all those layouts.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "FilesystemInfo.h"
#include "../common/Logging.h"
#include <algorithm>
#include <cstring>
#include <sstream>
namespace spw
{
// ============================================================================
// Endian helpers (duplicated from FilesystemDetector.cpp for self-containment;
// in a larger project these would be in a shared utility header)
// ============================================================================
static uint16_t readLE16(const uint8_t* p)
{
return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
}
static uint32_t readLE32(const uint8_t* p)
{
return static_cast<uint32_t>(p[0])
| (static_cast<uint32_t>(p[1]) << 8)
| (static_cast<uint32_t>(p[2]) << 16)
| (static_cast<uint32_t>(p[3]) << 24);
}
static uint64_t readLE64(const uint8_t* p)
{
return static_cast<uint64_t>(readLE32(p))
| (static_cast<uint64_t>(readLE32(p + 4)) << 32);
}
static uint16_t readBE16(const uint8_t* p)
{
return (static_cast<uint16_t>(p[0]) << 8) | static_cast<uint16_t>(p[1]);
}
static uint32_t readBE32(const uint8_t* p)
{
return (static_cast<uint32_t>(p[0]) << 24)
| (static_cast<uint32_t>(p[1]) << 16)
| (static_cast<uint32_t>(p[2]) << 8)
| static_cast<uint32_t>(p[3]);
}
static uint64_t readBE64(const uint8_t* p)
{
return (static_cast<uint64_t>(readBE32(p)) << 32) | readBE32(p + 4);
}
static bool isPowerOf2(uint32_t v) { return v != 0 && (v & (v - 1)) == 0; }
static std::string extractString(const uint8_t* data, size_t maxLen)
{
size_t len = 0;
while (len < maxLen && data[len] != 0)
len++;
while (len > 0 && data[len - 1] == ' ')
len--;
return std::string(reinterpret_cast<const char*>(data), len);
}
static std::string formatUuid(const uint8_t* uuid)
{
char buf[48];
snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
uuid[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5],
uuid[6], uuid[7],
uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]);
return buf;
}
std::vector<uint8_t> FilesystemInfo::safeRead(const DiskReadCallback& readFunc, uint64_t offset, uint32_t size)
{
auto result = readFunc(offset, size);
if (result.isError())
return {};
return result.value();
}
// ============================================================================
// Entry points
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::read(
FilesystemType type,
const DiskReadCallback& readFunc,
uint64_t volumeSize)
{
switch (type)
{
case FilesystemType::NTFS:
return readNtfs(readFunc, volumeSize);
case FilesystemType::FAT12:
case FilesystemType::FAT16:
case FilesystemType::FAT32:
return readFat(readFunc, volumeSize);
case FilesystemType::ExFAT:
return readExfat(readFunc, volumeSize);
case FilesystemType::Ext2:
case FilesystemType::Ext3:
case FilesystemType::Ext4:
return readExt(readFunc, volumeSize, type);
case FilesystemType::Btrfs:
return readBtrfs(readFunc, volumeSize);
case FilesystemType::XFS:
return readXfs(readFunc, volumeSize);
case FilesystemType::HFSPlus:
case FilesystemType::HFS:
return readHfsPlus(readFunc, volumeSize);
case FilesystemType::APFS:
return readApfs(readFunc, volumeSize);
default:
return readGeneric(type, readFunc, volumeSize);
}
}
Result<FilesystemInfoData> FilesystemInfo::detectAndRead(
const DiskReadCallback& readFunc,
uint64_t volumeSize)
{
auto detectResult = FilesystemDetector::detect(readFunc, volumeSize);
if (detectResult.isError())
return detectResult.error();
const auto& detection = detectResult.value();
if (!detection.isDetected())
{
FilesystemInfoData info;
info.type = FilesystemType::Unknown;
info.typeName = "Unknown";
return info;
}
return read(detection.type, readFunc, volumeSize);
}
// ============================================================================
// NTFS metadata reader
//
// Boot sector layout:
// 0x00 [3]: Jump
// 0x03 [8]: OEM ID "NTFS "
// 0x0B [2]: Bytes per sector
// 0x0D [1]: Sectors per cluster
// 0x28 [8]: Total sectors
// 0x30 [8]: MFT cluster number
// 0x38 [8]: MFT mirror cluster number
// 0x40 [1]: Clusters per MFT record (signed: if negative, size = 2^|value|)
// 0x48 [8]: Volume serial number
//
// $Volume file (MFT record 3) contains the version info, but reading MFT
// records requires significant parsing. We extract what's available from BPB.
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readNtfs(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto bpb = safeRead(readFunc, 0, 512);
if (bpb.size() < 512)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read NTFS boot sector");
FilesystemInfoData info;
info.type = FilesystemType::NTFS;
info.typeName = "NTFS";
uint16_t bytesPerSector = readLE16(bpb.data() + 0x0B);
uint8_t sectorsPerCluster = bpb[0x0D];
uint64_t totalSectors = readLE64(bpb.data() + 0x28);
if (bytesPerSector == 0 || sectorsPerCluster == 0)
return ErrorInfo::fromCode(ErrorCode::FilesystemCorrupt, "NTFS BPB has zero sector/cluster size");
info.blockSize = bytesPerSector * sectorsPerCluster;
info.totalBlocks = totalSectors / sectorsPerCluster;
info.totalSizeBytes = totalSectors * bytesPerSector;
info.ntfs.mftCluster = readLE64(bpb.data() + 0x30);
info.ntfs.mftMirrorCluster = readLE64(bpb.data() + 0x38);
// MFT record size: if byte at 0x40 is negative, size = 2^|value|
// If positive, size = value * clustersPerMftRecord * bytesPerSector
int8_t mftRecordVal = static_cast<int8_t>(bpb[0x40]);
if (mftRecordVal < 0)
info.ntfs.mftRecordSize = 1u << static_cast<uint32_t>(-mftRecordVal);
else
info.ntfs.mftRecordSize = static_cast<uint32_t>(mftRecordVal) * info.blockSize;
info.ntfs.serialNumber = readLE64(bpb.data() + 0x48);
// Format serial as UUID-like string
if (info.ntfs.serialNumber != 0)
{
char buf[20];
snprintf(buf, sizeof(buf), "%04X-%04X",
static_cast<unsigned>((info.ntfs.serialNumber >> 48) & 0xFFFF),
static_cast<unsigned>((info.ntfs.serialNumber >> 32) & 0xFFFF));
info.uuid = buf;
}
// Try to read the volume label from the $Volume MFT record (#3).
// The $Volume record is at MFT cluster + (3 * mftRecordSize / clusterSize) * clusterSize.
// This is a complex parse — read the MFT record, find the $VOLUME_NAME attribute (0x60).
if (info.ntfs.mftRecordSize > 0 && info.ntfs.mftCluster > 0)
{
uint64_t mftOffset = info.ntfs.mftCluster * info.blockSize;
uint64_t volumeRecordOffset = mftOffset + (3ULL * info.ntfs.mftRecordSize);
auto mftRecord = safeRead(readFunc, volumeRecordOffset, info.ntfs.mftRecordSize);
if (mftRecord.size() >= info.ntfs.mftRecordSize)
{
// Validate MFT record signature "FILE"
if (mftRecord.size() >= 4 && std::memcmp(mftRecord.data(), "FILE", 4) == 0)
{
// First attribute offset at byte 0x14 (uint16)
uint16_t attrOffset = readLE16(mftRecord.data() + 0x14);
// Walk attributes looking for $VOLUME_NAME (type 0x60)
// and $VOLUME_INFORMATION (type 0x70)
uint32_t pos = attrOffset;
while (pos + 16 < info.ntfs.mftRecordSize)
{
uint32_t attrType = readLE32(mftRecord.data() + pos);
uint32_t attrLength = readLE32(mftRecord.data() + pos + 4);
if (attrType == 0xFFFFFFFF || attrLength == 0)
break;
if (attrType == 0x60) // $VOLUME_NAME
{
// Resident attribute: name is at content offset
uint8_t nonResident = mftRecord[pos + 8];
if (nonResident == 0) // Resident
{
uint32_t contentSize = readLE32(mftRecord.data() + pos + 0x10);
uint16_t contentOffset = readLE16(mftRecord.data() + pos + 0x14);
if (pos + contentOffset + contentSize <= info.ntfs.mftRecordSize && contentSize > 0)
{
// UTF-16LE volume name
const uint16_t* nameData = reinterpret_cast<const uint16_t*>(
mftRecord.data() + pos + contentOffset);
size_t nameChars = contentSize / 2;
// Convert UTF-16LE to UTF-8 (BMP only)
std::string name;
for (size_t i = 0; i < nameChars; i++)
{
uint16_t ch = nameData[i];
if (ch == 0) break;
if (ch < 0x80)
name.push_back(static_cast<char>(ch));
else if (ch < 0x800)
{
name.push_back(static_cast<char>(0xC0 | (ch >> 6)));
name.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
else
{
name.push_back(static_cast<char>(0xE0 | (ch >> 12)));
name.push_back(static_cast<char>(0x80 | ((ch >> 6) & 0x3F)));
name.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
}
info.label = name;
}
}
}
else if (attrType == 0x70) // $VOLUME_INFORMATION
{
uint8_t nonResident = mftRecord[pos + 8];
if (nonResident == 0)
{
uint16_t contentOffset = readLE16(mftRecord.data() + pos + 0x14);
uint32_t contentSize = readLE32(mftRecord.data() + pos + 0x10);
if (pos + contentOffset + contentSize <= info.ntfs.mftRecordSize && contentSize >= 4)
{
// $VOLUME_INFORMATION content:
// 0x00 [8]: reserved
// 0x08 [1]: major version
// 0x09 [1]: minor version
// 0x0A [2]: flags
const uint8_t* viData = mftRecord.data() + pos + contentOffset;
if (contentSize >= 12)
{
info.ntfs.majorVersion = viData[8];
info.ntfs.minorVersion = viData[9];
}
else if (contentSize >= 4)
{
// Compact format
info.ntfs.majorVersion = viData[0];
info.ntfs.minorVersion = viData[1];
}
}
}
}
pos += attrLength;
}
}
}
}
return info;
}
// ============================================================================
// FAT metadata reader
//
// Common BPB (BIOS Parameter Block) layout:
// 0x00 [3]: Jump instruction
// 0x03 [8]: OEM name
// 0x0B [2]: Bytes per sector
// 0x0D [1]: Sectors per cluster
// 0x0E [2]: Reserved sectors
// 0x10 [1]: Number of FATs
// 0x11 [2]: Root entry count (FAT12/16 only)
// 0x13 [2]: Total sectors (16-bit, 0 if 32-bit used)
// 0x15 [1]: Media type
// 0x16 [2]: FAT size (16-bit, 0 for FAT32)
// 0x20 [4]: Total sectors (32-bit)
//
// FAT32 extended BPB:
// 0x24 [4]: FAT size (32-bit)
// 0x2C [4]: Root cluster
// 0x43 [4]: Volume serial
// 0x47 [11]: Volume label
//
// FAT12/16 extended BPB:
// 0x27 [4]: Volume serial
// 0x2B [11]: Volume label
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readFat(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto bpb = safeRead(readFunc, 0, 512);
if (bpb.size() < 512)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read FAT boot sector");
FilesystemInfoData info;
uint16_t bytesPerSector = readLE16(bpb.data() + 0x0B);
uint8_t sectorsPerCluster = bpb[0x0D];
uint16_t reservedSectors = readLE16(bpb.data() + 0x0E);
uint8_t numFats = bpb[0x10];
uint16_t rootEntryCount = readLE16(bpb.data() + 0x11);
uint16_t totalSectors16 = readLE16(bpb.data() + 0x13);
uint16_t fatSize16 = readLE16(bpb.data() + 0x16);
uint32_t totalSectors32 = readLE32(bpb.data() + 0x20);
if (bytesPerSector == 0 || sectorsPerCluster == 0)
return ErrorInfo::fromCode(ErrorCode::FilesystemCorrupt, "FAT BPB has zero values");
uint32_t fatSize = fatSize16;
bool isFat32 = (fatSize == 0);
if (isFat32)
fatSize = readLE32(bpb.data() + 0x24);
uint32_t totalSectors = (totalSectors16 != 0) ? totalSectors16 : totalSectors32;
uint32_t rootDirSectors = ((rootEntryCount * 32) + (bytesPerSector - 1)) / bytesPerSector;
uint32_t dataStartSector = reservedSectors + (numFats * fatSize) + rootDirSectors;
uint32_t dataSectors = (totalSectors > dataStartSector) ? totalSectors - dataStartSector : 0;
uint32_t totalClusters = (sectorsPerCluster > 0) ? dataSectors / sectorsPerCluster : 0;
// Determine FAT type
if (totalClusters < 4085)
{
info.type = FilesystemType::FAT12;
info.typeName = "FAT12";
}
else if (totalClusters < 65525)
{
info.type = FilesystemType::FAT16;
info.typeName = "FAT16";
}
else
{
info.type = FilesystemType::FAT32;
info.typeName = "FAT32";
}
info.blockSize = static_cast<uint32_t>(bytesPerSector) * sectorsPerCluster;
info.totalBlocks = totalClusters;
info.totalSizeBytes = static_cast<uint64_t>(totalSectors) * bytesPerSector;
info.fat.fatCount = numFats;
info.fat.fatSize = fatSize;
info.fat.reservedSectors = reservedSectors;
info.fat.rootEntryCount = rootEntryCount;
info.fat.totalClusters = totalClusters;
// OEM name
info.fat.oemName = extractString(bpb.data() + 3, 8);
// Volume label and serial
if (isFat32)
{
info.fat.volumeSerial = readLE32(bpb.data() + 0x43);
info.label = extractString(bpb.data() + 0x47, 11);
}
else
{
info.fat.volumeSerial = readLE32(bpb.data() + 0x27);
info.label = extractString(bpb.data() + 0x2B, 11);
}
if (info.label == "NO NAME")
info.label.clear();
if (info.fat.volumeSerial != 0)
{
char buf[12];
snprintf(buf, sizeof(buf), "%04X-%04X",
(info.fat.volumeSerial >> 16) & 0xFFFF,
info.fat.volumeSerial & 0xFFFF);
info.uuid = buf;
}
// Count free clusters by scanning the FAT
// For FAT32, read the FSInfo sector at sector 1 (offset 0x1E8 has free count)
if (isFat32)
{
uint16_t fsInfoSector = readLE16(bpb.data() + 0x30);
if (fsInfoSector > 0 && fsInfoSector < reservedSectors)
{
auto fsInfo = safeRead(readFunc,
static_cast<uint64_t>(fsInfoSector) * bytesPerSector, 512);
if (fsInfo.size() >= 512)
{
// FSInfo signatures: 0x41615252 at offset 0, 0x61417272 at offset 484
uint32_t sig1 = readLE32(fsInfo.data());
uint32_t sig2 = readLE32(fsInfo.data() + 484);
if (sig1 == 0x41615252u && sig2 == 0x61417272u)
{
uint32_t freeClusters = readLE32(fsInfo.data() + 488);
if (freeClusters != 0xFFFFFFFFu) // 0xFFFFFFFF = unknown
{
info.freeBlocks = freeClusters;
info.freeSizeBytes = static_cast<uint64_t>(freeClusters) * info.blockSize;
info.usedSizeBytes = info.totalSizeBytes - info.freeSizeBytes;
}
}
}
}
}
return info;
}
// ============================================================================
// exFAT metadata reader
//
// Boot sector layout:
// 0x00 [3]: Jump
// 0x03 [8]: "EXFAT "
// 0x40 [8]: Partition offset (sectors)
// 0x48 [8]: Volume length (sectors)
// 0x50 [4]: FAT offset (sectors)
// 0x54 [4]: FAT length (sectors)
// 0x58 [4]: Cluster heap offset (sectors)
// 0x5C [4]: Cluster count
// 0x60 [4]: First cluster of root directory
// 0x64 [4]: Volume serial number
// 0x68 [2]: FS revision
// 0x6C [1]: BytesPerSectorShift
// 0x6D [1]: SectorsPerClusterShift
// 0x6E [1]: Number of FATs
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readExfat(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto boot = safeRead(readFunc, 0, 512);
if (boot.size() < 512)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read exFAT boot sector");
FilesystemInfoData info;
info.type = FilesystemType::ExFAT;
info.typeName = "exFAT";
uint64_t volumeLength = readLE64(boot.data() + 0x48);
uint32_t clusterCount = readLE32(boot.data() + 0x5C);
uint32_t volumeSerial = readLE32(boot.data() + 0x64);
uint16_t fsRevision = readLE16(boot.data() + 0x68);
uint8_t bytesPerSectorShift = boot[0x6C];
uint8_t sectorsPerClusterShift = boot[0x6D];
uint32_t bytesPerSector = (bytesPerSectorShift <= 12) ? (1u << bytesPerSectorShift) : 512;
uint32_t sectorsPerCluster = (sectorsPerClusterShift <= 25) ? (1u << sectorsPerClusterShift) : 1;
info.blockSize = bytesPerSector * sectorsPerCluster;
info.totalBlocks = clusterCount;
info.totalSizeBytes = volumeLength * bytesPerSector;
info.exfat.fsRevision = fsRevision;
info.exfat.clusterCount = clusterCount;
info.exfat.volumeSerial = volumeSerial;
if (volumeSerial != 0)
{
char buf[12];
snprintf(buf, sizeof(buf), "%04X-%04X",
(volumeSerial >> 16) & 0xFFFF,
volumeSerial & 0xFFFF);
info.uuid = buf;
}
// exFAT volume label is stored in the root directory as a Volume Label entry (type 0x83).
// Read the root directory cluster to find it.
uint32_t rootCluster = readLE32(boot.data() + 0x60);
uint32_t clusterHeapOffset = readLE32(boot.data() + 0x58);
if (rootCluster >= 2)
{
uint64_t rootOffset = static_cast<uint64_t>(clusterHeapOffset + (rootCluster - 2) * sectorsPerCluster) * bytesPerSector;
auto rootData = safeRead(readFunc, rootOffset, info.blockSize);
if (rootData.size() >= 32)
{
// Scan for Volume Label entry (entry type 0x83)
for (size_t pos = 0; pos + 32 <= rootData.size(); pos += 32)
{
uint8_t entryType = rootData[pos];
if (entryType == 0x83) // Volume Label
{
uint8_t charCount = rootData[pos + 1];
if (charCount > 11) charCount = 11;
// Label is UTF-16LE starting at offset 2
const uint16_t* labelData = reinterpret_cast<const uint16_t*>(&rootData[pos + 2]);
std::string label;
for (int i = 0; i < charCount; i++)
{
uint16_t ch = labelData[i];
if (ch < 0x80)
label.push_back(static_cast<char>(ch));
else if (ch < 0x800)
{
label.push_back(static_cast<char>(0xC0 | (ch >> 6)));
label.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
else
{
label.push_back(static_cast<char>(0xE0 | (ch >> 12)));
label.push_back(static_cast<char>(0x80 | ((ch >> 6) & 0x3F)));
label.push_back(static_cast<char>(0x80 | (ch & 0x3F)));
}
}
info.label = label;
break;
}
else if (entryType == 0x00)
{
break; // End of directory
}
}
}
}
return info;
}
// ============================================================================
// ext2/3/4 metadata reader
//
// Superblock at byte offset 1024, size 1024 bytes.
// All fields are little-endian.
//
// Key superblock offsets (relative to superblock start):
// 0x00 [4]: s_inodes_count
// 0x04 [4]: s_blocks_count_lo
// 0x08 [4]: s_r_blocks_count_lo
// 0x0C [4]: s_free_blocks_count_lo
// 0x10 [4]: s_free_inodes_count
// 0x14 [4]: s_first_data_block
// 0x18 [4]: s_log_block_size (block size = 1024 << value)
// 0x38 [2]: s_magic (0xEF53)
// 0x3A [2]: s_state
// 0x3C [2]: s_errors
// 0x48 [4]: s_creator_os
// 0x5C [4]: s_feature_compat
// 0x60 [4]: s_feature_incompat
// 0x64 [4]: s_feature_ro_compat
// 0x68 [16]: s_uuid
// 0x78 [16]: s_volume_name
// 0x150 [4]: s_blocks_count_hi (ext4 64-bit)
// 0x158 [4]: s_free_blocks_count_hi (ext4 64-bit)
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readExt(const DiskReadCallback& readFunc, uint64_t volumeSize, FilesystemType type)
{
auto sb = safeRead(readFunc, 1024, 1024);
if (sb.size() < 256)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read ext superblock");
// Verify magic
uint16_t magic = readLE16(sb.data() + 0x38);
if (magic != EXT_SUPER_MAGIC)
return ErrorInfo::fromCode(ErrorCode::FilesystemCorrupt, "Invalid ext superblock magic");
FilesystemInfoData info;
info.type = type;
info.typeName = FilesystemDetector::filesystemName(type);
// Block size
uint32_t logBlockSize = readLE32(sb.data() + 0x18);
info.blockSize = (logBlockSize < 10) ? (1024u << logBlockSize) : 4096;
// Block counts
uint32_t blocksLo = readLE32(sb.data() + 0x04);
uint32_t freeBlocksLo = readLE32(sb.data() + 0x0C);
uint32_t compatFeatures = readLE32(sb.data() + 0x5C);
uint32_t incompatFeatures = readLE32(sb.data() + 0x60);
uint32_t roCompatFeatures = readLE32(sb.data() + 0x64);
// 64-bit block counts for ext4
uint64_t totalBlocks = blocksLo;
uint64_t freeBlocks = freeBlocksLo;
if ((incompatFeatures & ExtFeatures::Incompat_64bit) && sb.size() >= 0x15C)
{
uint32_t blocksHi = readLE32(sb.data() + 0x150);
uint32_t freeBlocksHi = readLE32(sb.data() + 0x158);
totalBlocks |= (static_cast<uint64_t>(blocksHi) << 32);
freeBlocks |= (static_cast<uint64_t>(freeBlocksHi) << 32);
}
info.totalBlocks = totalBlocks;
info.freeBlocks = freeBlocks;
info.totalSizeBytes = totalBlocks * info.blockSize;
info.freeSizeBytes = freeBlocks * info.blockSize;
info.usedSizeBytes = info.totalSizeBytes - info.freeSizeBytes;
// Inodes
info.ext.inodeCount = readLE32(sb.data() + 0x00);
info.ext.freeInodes = readLE32(sb.data() + 0x10);
// Block groups
uint32_t blocksPerGroup = readLE32(sb.data() + 0x20);
if (blocksPerGroup > 0)
info.ext.blockGroupCount = static_cast<uint32_t>((totalBlocks + blocksPerGroup - 1) / blocksPerGroup);
// State and error handling
info.ext.state = readLE16(sb.data() + 0x3A);
info.ext.errors = readLE16(sb.data() + 0x3C);
info.ext.creatorOs = readLE32(sb.data() + 0x48);
// Features
info.ext.compatFeatures = compatFeatures;
info.ext.incompatFeatures = incompatFeatures;
info.ext.roCompatFeatures = roCompatFeatures;
// Build human-readable feature list
auto& fs = info.ext.featureStrings;
if (compatFeatures & ExtFeatures::Compat_HasJournal) fs.push_back("has_journal");
if (compatFeatures & ExtFeatures::Compat_DirIndex) fs.push_back("dir_index");
if (incompatFeatures & ExtFeatures::Incompat_Filetype) fs.push_back("filetype");
if (incompatFeatures & ExtFeatures::Incompat_Extents) fs.push_back("extents");
if (incompatFeatures & ExtFeatures::Incompat_64bit) fs.push_back("64bit");
if (incompatFeatures & ExtFeatures::Incompat_FlexBg) fs.push_back("flex_bg");
if (roCompatFeatures & ExtFeatures::RoCompat_Sparse) fs.push_back("sparse_super");
if (roCompatFeatures & ExtFeatures::RoCompat_LargeFile) fs.push_back("large_file");
if (roCompatFeatures & ExtFeatures::RoCompat_HugeFile) fs.push_back("huge_file");
if (roCompatFeatures & ExtFeatures::RoCompat_Metadata) fs.push_back("metadata_csum");
// Label
info.label = extractString(sb.data() + 0x78, 16);
// UUID
info.uuid = formatUuid(sb.data() + 0x68);
return info;
}
// ============================================================================
// Btrfs metadata reader
//
// Superblock at offset 0x10000 (64 KiB).
// Key offsets relative to superblock start:
// 0x00 [32]: csum
// 0x20 [16]: fsid (UUID)
// 0x30 [8]: bytenr (physical offset of this block)
// 0x40 [8]: magic "_BHRfS_M"
// 0x48 [8]: generation
// 0x50 [8]: root
// 0x58 [8]: chunk_root
// 0x60 [8]: log_root
// 0x80 [4]: sectorsize
// 0x84 [4]: nodesize
// 0x8C [8]: total_bytes
// 0x94 [8]: bytes_used
// 0x12B [256]: label
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readBtrfs(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto sb = safeRead(readFunc, 0x10000, 0x200);
if (sb.size() < 0x1A0)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read Btrfs superblock");
FilesystemInfoData info;
info.type = FilesystemType::Btrfs;
info.typeName = "Btrfs";
info.blockSize = readLE32(sb.data() + 0x80); // sectorsize
info.totalSizeBytes = readLE64(sb.data() + 0x8C); // total_bytes
info.usedSizeBytes = readLE64(sb.data() + 0x94); // bytes_used
info.freeSizeBytes = (info.totalSizeBytes > info.usedSizeBytes) ?
info.totalSizeBytes - info.usedSizeBytes : 0;
if (info.blockSize > 0)
{
info.totalBlocks = info.totalSizeBytes / info.blockSize;
info.freeBlocks = info.freeSizeBytes / info.blockSize;
}
// UUID (fsid) at offset 0x20
info.uuid = formatUuid(sb.data() + 0x20);
// Label at offset 0x12B (256 bytes)
if (sb.size() > 0x12B + 256)
info.label = extractString(sb.data() + 0x12B, 256);
return info;
}
// ============================================================================
// XFS metadata reader
//
// Superblock at offset 0, all fields big-endian.
// Key offsets:
// 0x00 [4]: sb_magicnum "XFSB"
// 0x04 [4]: sb_blocksize
// 0x08 [8]: sb_dblocks (total data blocks)
// 0x10 [8]: sb_rblocks (realtime blocks)
// 0x18 [8]: sb_rextents
// 0x20 [16]: sb_uuid
// 0x60 [8]: sb_fdblocks (free data blocks)
// 0x68 [8]: sb_icount
// 0x70 [8]: sb_ifree
// 0x6C [12]: sb_fname (label)
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readXfs(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto sb = safeRead(readFunc, 0, 512);
if (sb.size() < 256)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read XFS superblock");
FilesystemInfoData info;
info.type = FilesystemType::XFS;
info.typeName = "XFS";
info.blockSize = readBE32(sb.data() + 0x04);
uint64_t totalBlocks = readBE64(sb.data() + 0x08);
uint64_t freeBlocks = readBE64(sb.data() + 0x60);
info.totalBlocks = totalBlocks;
info.freeBlocks = freeBlocks;
info.totalSizeBytes = totalBlocks * info.blockSize;
info.freeSizeBytes = freeBlocks * info.blockSize;
info.usedSizeBytes = info.totalSizeBytes - info.freeSizeBytes;
// Label at offset 0x6C (12 bytes)
info.label = extractString(sb.data() + 0x6C, 12);
// UUID at offset 0x20
info.uuid = formatUuid(sb.data() + 0x20);
return info;
}
// ============================================================================
// HFS+ metadata reader
//
// Volume header at offset 1024 (byte offset from partition start), big-endian.
// Key offsets:
// 0x00 [2]: signature (0x482B "H+" or 0x4858 "HX")
// 0x02 [2]: version
// 0x04 [4]: attributes
// 0x12 [2]: modify date
// 0x1C [4]: fileCount
// 0x20 [4]: folderCount
// 0x28 [4]: blockSize
// 0x2C [4]: totalBlocks
// 0x30 [4]: freeBlocks
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readHfsPlus(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto vh = safeRead(readFunc, 1024, 512);
if (vh.size() < 162)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read HFS+ volume header");
uint16_t sig = readBE16(vh.data());
FilesystemInfoData info;
if (sig == 0x4244) // Classic HFS
{
info.type = FilesystemType::HFS;
info.typeName = "HFS (Classic)";
// Basic HFS Master Directory Block parsing
info.blockSize = readBE32(vh.data() + 0x14); // drAlBlkSiz
uint16_t numABlocks = readBE16(vh.data() + 0x12); // drNmAlBlks
uint16_t freeABlocks = readBE16(vh.data() + 0x22); // drFreeBks
info.totalBlocks = numABlocks;
info.freeBlocks = freeABlocks;
info.totalSizeBytes = static_cast<uint64_t>(numABlocks) * info.blockSize;
info.freeSizeBytes = static_cast<uint64_t>(freeABlocks) * info.blockSize;
info.usedSizeBytes = info.totalSizeBytes - info.freeSizeBytes;
// Volume name at offset 0x25 (Pascal string: length byte + chars)
uint8_t nameLen = vh[0x24];
if (nameLen > 0 && nameLen <= 27)
info.label = extractString(vh.data() + 0x25, nameLen);
return info;
}
info.type = FilesystemType::HFSPlus;
info.typeName = (sig == HFSX_MAGIC) ? "HFSX" : "HFS+";
info.hfsplus.version = readBE16(vh.data() + 0x02);
info.hfsplus.fileCount = readBE32(vh.data() + 0x1C);
info.hfsplus.folderCount = readBE32(vh.data() + 0x20);
info.blockSize = readBE32(vh.data() + 0x28);
uint32_t totalBlocks = readBE32(vh.data() + 0x2C);
uint32_t freeBlocks = readBE32(vh.data() + 0x30);
info.totalBlocks = totalBlocks;
info.freeBlocks = freeBlocks;
info.totalSizeBytes = static_cast<uint64_t>(totalBlocks) * info.blockSize;
info.freeSizeBytes = static_cast<uint64_t>(freeBlocks) * info.blockSize;
info.usedSizeBytes = info.totalSizeBytes - info.freeSizeBytes;
// HFS+ stores the volume name in the catalog file, which requires B-tree traversal.
// This is complex — we leave label empty for HFS+ unless the caller provides it.
return info;
}
// ============================================================================
// APFS metadata reader
//
// Container superblock (NXSB) at offset 0, little-endian.
// Fields after the 32-byte object header (obj_phys_t):
// +0x00 [4]: nx_magic (0x4253584E "NXSB")
// +0x04 [4]: nx_block_size
// +0x08 [8]: nx_block_count
// +0x18 [16]: nx_uuid
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readApfs(const DiskReadCallback& readFunc, uint64_t volumeSize)
{
auto nxsb = safeRead(readFunc, 0, 4096);
if (nxsb.size() < 128)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to read APFS container superblock");
FilesystemInfoData info;
info.type = FilesystemType::APFS;
info.typeName = "APFS";
// Object header is 32 bytes, then container superblock fields
info.blockSize = readLE32(nxsb.data() + 36); // nx_block_size
uint64_t blockCount = readLE64(nxsb.data() + 40); // nx_block_count
info.totalBlocks = blockCount;
info.totalSizeBytes = blockCount * info.blockSize;
// UUID at offset 32+24 = 56 (nx_uuid)
if (nxsb.size() >= 72)
info.uuid = formatUuid(nxsb.data() + 56);
// APFS stores volume names in volume superblocks, which are referenced through
// the object map. Full parsing would require B-tree traversal of the omap.
// We leave label empty for the container level.
return info;
}
// ============================================================================
// Generic metadata reader — for filesystems where we have limited parsing
// ============================================================================
Result<FilesystemInfoData> FilesystemInfo::readGeneric(FilesystemType type, const DiskReadCallback& readFunc, uint64_t volumeSize)
{
// Use detection results as the baseline for less common filesystems
auto detectResult = FilesystemDetector::detect(readFunc, volumeSize);
if (detectResult.isError())
return detectResult.error();
const auto& det = detectResult.value();
FilesystemInfoData info;
info.type = type;
info.typeName = FilesystemDetector::filesystemName(type);
info.label = det.label;
info.uuid = det.uuid;
info.blockSize = det.blockSize;
info.totalSizeBytes = volumeSize;
return info;
}
} // namespace spw

View File

@@ -0,0 +1,149 @@
#pragma once
// FilesystemInfo — Reads detailed filesystem metadata after detection.
//
// Once FilesystemDetector identifies a filesystem type, this class reads the
// on-disk structures to extract label, UUID, size, free space, features, and
// version information. Each filesystem has its own superblock/BPB layout.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "PartitionTable.h"
#include "FilesystemDetector.h"
#include <cstdint>
#include <string>
#include <vector>
namespace spw
{
// Feature flags for ext2/3/4 (a selection of the most important ones)
namespace ExtFeatures
{
constexpr uint32_t Compat_HasJournal = 0x0004;
constexpr uint32_t Compat_ExtAttr = 0x0010;
constexpr uint32_t Compat_ResizeInode = 0x0010;
constexpr uint32_t Compat_DirIndex = 0x0020;
constexpr uint32_t Incompat_Filetype = 0x0002;
constexpr uint32_t Incompat_Recover = 0x0004;
constexpr uint32_t Incompat_Extents = 0x0040;
constexpr uint32_t Incompat_64bit = 0x0080;
constexpr uint32_t Incompat_FlexBg = 0x0200;
constexpr uint32_t RoCompat_Sparse = 0x0001;
constexpr uint32_t RoCompat_LargeFile = 0x0002;
constexpr uint32_t RoCompat_HugeFile = 0x0008;
constexpr uint32_t RoCompat_Metadata = 0x1000;
}
// ============================================================================
// Detailed filesystem information
// ============================================================================
struct FilesystemInfoData
{
// Basic identification
FilesystemType type = FilesystemType::Unknown;
std::string typeName; // Human-readable type name
std::string label; // Volume label / name
std::string uuid; // UUID, serial number, or equivalent
// Size information
uint32_t blockSize = 0; // Block/cluster size in bytes
uint64_t totalBlocks = 0; // Total blocks/clusters
uint64_t freeBlocks = 0; // Free blocks (if readable from superblock)
uint64_t totalSizeBytes = 0; // Total filesystem size in bytes
uint64_t freeSizeBytes = 0; // Free space in bytes (0 if unknown)
uint64_t usedSizeBytes = 0; // Used space in bytes
// NTFS-specific
struct
{
uint8_t majorVersion = 0; // NTFS version (e.g. 3.1)
uint8_t minorVersion = 0;
uint64_t mftCluster = 0; // Starting cluster of $MFT
uint64_t mftMirrorCluster = 0; // Starting cluster of $MFTMirr
uint32_t mftRecordSize = 0; // Bytes per MFT record
uint64_t serialNumber = 0; // Volume serial number
} ntfs;
// FAT-specific
struct
{
uint8_t fatCount = 0; // Number of FAT copies
uint32_t fatSize = 0; // Sectors per FAT
uint16_t reservedSectors = 0;
uint16_t rootEntryCount = 0; // FAT12/16 root dir entries
uint32_t totalClusters = 0;
uint32_t volumeSerial = 0;
std::string oemName; // OEM name from BPB (8 bytes)
} fat;
// ext-specific
struct
{
uint32_t inodeCount = 0;
uint32_t freeInodes = 0;
uint32_t blockGroupCount = 0;
uint32_t compatFeatures = 0;
uint32_t incompatFeatures = 0;
uint32_t roCompatFeatures = 0;
uint16_t state = 0; // 1 = clean, 2 = errors
uint16_t errors = 0; // Behavior on error
uint32_t creatorOs = 0; // 0=Linux, 1=Hurd, 2=Masix, 3=FreeBSD, 4=Lites
std::vector<std::string> featureStrings; // Human-readable feature list
} ext;
// exFAT-specific
struct
{
uint16_t fsRevision = 0;
uint32_t clusterCount = 0;
uint32_t volumeSerial = 0;
} exfat;
// HFS+ specific
struct
{
uint16_t version = 0; // 4 = HFS+, 5 = HFSX
uint32_t fileCount = 0;
uint32_t folderCount = 0;
} hfsplus;
};
// ============================================================================
// FilesystemInfo — reads detailed metadata from a detected filesystem
// ============================================================================
class FilesystemInfo
{
public:
// Read detailed metadata for a filesystem that was already detected.
// readFunc reads raw bytes from the start of the volume/partition.
static Result<FilesystemInfoData> read(
FilesystemType type,
const DiskReadCallback& readFunc,
uint64_t volumeSize = 0);
// Convenience: detect and then read info in one call
static Result<FilesystemInfoData> detectAndRead(
const DiskReadCallback& readFunc,
uint64_t volumeSize = 0);
private:
static Result<FilesystemInfoData> readNtfs(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readFat(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readExfat(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readExt(const DiskReadCallback& readFunc, uint64_t volumeSize, FilesystemType type);
static Result<FilesystemInfoData> readBtrfs(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readXfs(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readHfsPlus(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readApfs(const DiskReadCallback& readFunc, uint64_t volumeSize);
static Result<FilesystemInfoData> readGeneric(FilesystemType type, const DiskReadCallback& readFunc, uint64_t volumeSize);
// Helper: read N bytes safely
static std::vector<uint8_t> safeRead(const DiskReadCallback& readFunc, uint64_t offset, uint32_t size);
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
#pragma once
// PartitionTable — Abstract base class and concrete MBR/GPT/APM partition table implementations.
// Parses on-disk structures, validates integrity, and supports read/write operations.
//
// Reference specifications:
// MBR: "Master Boot Record" — de facto standard, 512-byte sector 0
// GPT: UEFI Specification, Chapter 5 — GUID Partition Table
// APM: Apple Technote 1350 — Apple Partition Map
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../common/Constants.h"
#include "DiskGeometry.h"
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace spw
{
// ============================================================================
// Read callback: abstracts reading raw bytes from disk, file, or buffer.
// Parameters: (byteOffset, byteCount) -> raw data
// ============================================================================
using DiskReadCallback = std::function<Result<std::vector<uint8_t>>(uint64_t offset, uint32_t size)>;
// ============================================================================
// On-disk MBR partition entry (16 bytes, packed)
// Offsets relative to entry start:
// 0x00 [1] Status / boot indicator (0x80 = active, 0x00 = inactive)
// 0x01 [3] CHS of first sector
// 0x04 [1] Partition type byte
// 0x05 [3] CHS of last sector
// 0x08 [4] LBA of first sector (little-endian)
// 0x0C [4] Sector count (little-endian)
// ============================================================================
#pragma pack(push, 1)
struct MbrPartitionEntryRaw
{
uint8_t status;
uint8_t chsFirst[3];
uint8_t type;
uint8_t chsLast[3];
uint32_t lbaStart;
uint32_t sectorCount;
};
static_assert(sizeof(MbrPartitionEntryRaw) == 16, "MBR partition entry must be 16 bytes");
struct MbrSectorRaw
{
uint8_t bootCode[446];
MbrPartitionEntryRaw entries[4];
uint16_t signature; // Must be 0xAA55
};
static_assert(sizeof(MbrSectorRaw) == 512, "MBR sector must be 512 bytes");
// On-disk GPT header (92 bytes at LBA 1)
struct GptHeaderRaw
{
uint64_t signature; // "EFI PART" = 0x5452415020494645
uint32_t revision; // Typically 0x00010000
uint32_t headerSize; // Usually 92
uint32_t headerCrc32; // CRC32 of header (with this field zeroed)
uint32_t reserved; // Must be zero
uint64_t myLba; // LBA of this header
uint64_t alternateLba; // LBA of alternate header
uint64_t firstUsableLba;
uint64_t lastUsableLba;
uint8_t diskGuid[16];
uint64_t partitionEntryLba; // LBA of partition entry array
uint32_t partitionEntryCount; // Number of entries (usually 128)
uint32_t partitionEntrySize; // Size of each entry (usually 128)
uint32_t partitionEntryCrc32; // CRC32 of entire entry array
};
static_assert(sizeof(GptHeaderRaw) == 92, "GPT header must be 92 bytes");
// On-disk GPT partition entry (128 bytes)
struct GptPartitionEntryRaw
{
uint8_t typeGuid[16];
uint8_t uniqueGuid[16];
uint64_t startLba;
uint64_t endLba; // Inclusive
uint64_t attributes;
uint16_t name[36]; // UTF-16LE, null-terminated
};
static_assert(sizeof(GptPartitionEntryRaw) == 128, "GPT partition entry must be 128 bytes");
// On-disk APM Driver Descriptor Map (block 0)
struct ApmDdmRaw
{
uint16_t signature; // 0x4552 "ER"
uint16_t blockSize;
uint32_t blockCount;
uint16_t deviceType;
uint16_t deviceId;
uint32_t driverData;
uint16_t driverCount;
uint8_t reserved[486]; // Pad to 512 (or blockSize)
};
// On-disk APM partition entry (512 bytes per entry)
struct ApmPartitionEntryRaw
{
uint16_t signature; // 0x504D "PM"
uint16_t reserved1;
uint32_t mapEntries; // Total number of partition map entries
uint32_t pBlockStart; // Physical block start
uint32_t pBlockCount; // Physical block count
char name[32]; // Null-terminated partition name
char type[32]; // Null-terminated type string (e.g. "Apple_HFS")
uint32_t lBlockStart; // Logical block start (within partition)
uint32_t lBlockCount; // Logical block count
uint32_t flags;
uint32_t bootBlockStart;
uint32_t bootBlockCount;
uint32_t bootLoadAddr;
uint32_t reserved2;
uint32_t bootEntryAddr;
uint32_t reserved3;
uint32_t bootChecksum;
char processor[16];
uint8_t padding[376]; // Pad to 512
};
#pragma pack(pop)
// ============================================================================
// Parsed partition entry — common structure across all table types
// ============================================================================
struct PartitionEntry
{
int index = -1; // 0-based index in partition table
SectorOffset startLba = 0;
SectorCount sectorCount = 0;
uint32_t sectorSize = SECTOR_SIZE_512; // Context: sector size of disk
// MBR fields
uint8_t mbrType = 0; // MBR type byte (0x07, 0x0C, etc.)
bool isActive = false; // MBR bootable flag
bool isExtended = false; // True if this is an extended container
bool isLogical = false; // True if inside extended partition
CHSAddress chsStart = {};
CHSAddress chsEnd = {};
// GPT fields
Guid typeGuid; // Partition type GUID
Guid uniqueGuid; // Unique partition GUID
uint64_t gptAttributes = 0;
std::string gptName; // UTF-8 name from GPT entry
// APM fields
std::string apmName; // APM partition name
std::string apmType; // APM type string ("Apple_HFS", etc.)
// Derived / cached
FilesystemType detectedFs = FilesystemType::Unknown;
std::string label; // Filesystem label if detected
// Convenience
uint64_t startByte() const { return startLba * sectorSize; }
uint64_t sizeBytes() const { return sectorCount * sectorSize; }
SectorOffset endLba() const { return (sectorCount > 0) ? (startLba + sectorCount - 1) : startLba; }
};
// ============================================================================
// Parameters for creating a new partition
// ============================================================================
struct PartitionParams
{
SectorOffset startLba = 0;
SectorCount sectorCount = 0;
// MBR: type byte
uint8_t mbrType = 0x07; // Default: NTFS/HPFS/exFAT
// GPT: type GUID, name
Guid typeGuid;
std::string gptName;
// Flags
bool isActive = false;
bool isLogical = false; // MBR: create inside extended partition
};
// ============================================================================
// Well-known MBR partition type bytes
// ============================================================================
namespace MbrTypes
{
constexpr uint8_t Empty = 0x00;
constexpr uint8_t FAT12 = 0x01;
constexpr uint8_t FAT16_Small = 0x04; // < 32 MiB
constexpr uint8_t Extended = 0x05;
constexpr uint8_t FAT16_Large = 0x06; // >= 32 MiB
constexpr uint8_t NTFS_HPFS = 0x07;
constexpr uint8_t FAT32_CHS = 0x0B;
constexpr uint8_t FAT32_LBA = 0x0C;
constexpr uint8_t FAT16_LBA = 0x0E;
constexpr uint8_t Extended_LBA = 0x0F;
constexpr uint8_t HiddenFAT32 = 0x1B;
constexpr uint8_t HiddenFAT32_LBA = 0x1C;
constexpr uint8_t DynDisk = 0x42;
constexpr uint8_t LinuxSwap = 0x82;
constexpr uint8_t LinuxNative = 0x83;
constexpr uint8_t LinuxExtended = 0x85;
constexpr uint8_t LinuxLVM = 0x8E;
constexpr uint8_t FreeBSD = 0xA5;
constexpr uint8_t OpenBSD = 0xA6;
constexpr uint8_t NetBSD = 0xA9;
constexpr uint8_t HFS_APM = 0xAF;
constexpr uint8_t GPT_Protective = 0xEE;
constexpr uint8_t EFI_System = 0xEF;
constexpr uint8_t LinuxRaid = 0xFD;
// Returns a human-readable name for an MBR type byte
const char* typeName(uint8_t type);
// Returns true if this type represents an extended/container partition
bool isExtendedType(uint8_t type);
}
// ============================================================================
// Well-known GPT partition type GUIDs
// ============================================================================
namespace GptTypes
{
// Microsoft
Guid microsoftBasicData(); // EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
Guid microsoftReserved(); // E3C9E316-0B5C-4DB8-817D-F92DF00215AE
Guid efiSystem(); // C12A7328-F81F-11D2-BA4B-00A0C93EC93B
Guid microsoftLdmMetadata(); // 5808C8AA-7E8F-42E0-85D2-E1E90434CFB3
Guid microsoftLdmData(); // AF9B60A0-1431-4F62-BC68-3311714A69AD
Guid microsoftRecovery(); // DE94BBA4-06D1-4D40-A16A-BFD50179D6AC
// Linux
Guid linuxFilesystem(); // 0FC63DAF-8483-4772-8E79-3D69D8477DE4
Guid linuxSwap(); // 0657FD6D-A4AB-43C4-84E5-0933C84B4F4F
Guid linuxHome(); // 933AC7E1-2EB4-4F13-B844-0E14E2AEF915
Guid linuxLvm(); // E6D6D379-F507-44C2-A23C-238F2A3DF928
Guid linuxRaid(); // A19D880F-05FC-4D3B-A006-743F0F84911E
// Apple
Guid appleHfsPlus(); // 48465300-0000-11AA-AA11-00306543ECAC
Guid appleApfs(); // 7C3457EF-0000-11AA-AA11-00306543ECAC
Guid appleBoot(); // 426F6F74-0000-11AA-AA11-00306543ECAC
// BSD
Guid freebsdUfs(); // 516E7CB6-6ECF-11D6-8FF8-00022D09712B
Guid freebsdSwap(); // 516E7CB5-6ECF-11D6-8FF8-00022D09712B
Guid freebsdZfs(); // 516E7CBA-6ECF-11D6-8FF8-00022D09712B
// Returns a human-readable name for a GPT type GUID
std::string typeName(const Guid& guid);
}
// ============================================================================
// Abstract partition table interface
// ============================================================================
class PartitionTable
{
public:
virtual ~PartitionTable() = default;
// What kind of partition table is this?
virtual PartitionTableType type() const = 0;
// Get all parsed partition entries
virtual std::vector<PartitionEntry> partitions() const = 0;
// Modification operations
virtual Result<void> addPartition(const PartitionParams& params) = 0;
virtual Result<void> deletePartition(int index) = 0;
virtual Result<void> resizePartition(int index, SectorOffset newStart, SectorCount newSize) = 0;
// Serialize the entire partition table to bytes for writing back to disk
virtual Result<std::vector<uint8_t>> serialize() const = 0;
// Parse a partition table from a read callback.
// Automatically detects MBR vs GPT (GPT protective MBR -> GPT).
// APM is detected by the DDM signature at block 0.
static Result<std::unique_ptr<PartitionTable>> parse(
const DiskReadCallback& readFunc,
uint64_t diskSizeBytes,
uint32_t sectorSize = SECTOR_SIZE_512);
// Create a brand new empty partition table
static std::unique_ptr<PartitionTable> createNew(
PartitionTableType type,
uint64_t diskSizeBytes,
uint32_t sectorSize = SECTOR_SIZE_512,
const Guid& diskGuid = {});
protected:
uint64_t m_diskSizeBytes = 0;
uint32_t m_sectorSize = SECTOR_SIZE_512;
};
// ============================================================================
// MBR partition table
// ============================================================================
class MbrPartitionTable : public PartitionTable
{
public:
MbrPartitionTable();
PartitionTableType type() const override { return PartitionTableType::MBR; }
std::vector<PartitionEntry> partitions() const override;
Result<void> addPartition(const PartitionParams& params) override;
Result<void> deletePartition(int index) override;
Result<void> resizePartition(int index, SectorOffset newStart, SectorCount newSize) override;
Result<std::vector<uint8_t>> serialize() const override;
// Parse from raw sector data (reads MBR + walks EBR chain)
Result<void> parse(const DiskReadCallback& readFunc);
// Access to boot code for boot repair scenarios
const std::array<uint8_t, 446>& bootCode() const { return m_bootCode; }
void setBootCode(const std::array<uint8_t, 446>& code) { m_bootCode = code; }
// Set active (bootable) partition. Pass -1 to clear.
Result<void> setActivePartition(int index);
// MBR disk signature (bytes 440-443)
uint32_t diskSignature() const { return m_diskSignature; }
void setDiskSignature(uint32_t sig) { m_diskSignature = sig; }
// Does this MBR contain a GPT protective entry?
bool hasGptProtective() const;
private:
// Walk the extended partition EBR chain
Result<void> walkExtendedChain(const DiskReadCallback& readFunc, SectorOffset extStart, SectorOffset extSize);
// Find the extended partition (container), or -1
int findExtendedIndex() const;
// Check if a proposed region overlaps existing partitions
bool overlapsExisting(SectorOffset start, SectorCount count, int excludeIndex = -1) const;
std::array<uint8_t, 446> m_bootCode = {};
uint32_t m_diskSignature = 0;
uint16_t m_reserved = 0; // bytes 444-445
// Primary entries (up to 4). Logical entries follow.
std::vector<PartitionEntry> m_entries;
};
// ============================================================================
// GPT partition table
// ============================================================================
class GptPartitionTable : public PartitionTable
{
public:
GptPartitionTable();
PartitionTableType type() const override { return PartitionTableType::GPT; }
std::vector<PartitionEntry> partitions() const override;
Result<void> addPartition(const PartitionParams& params) override;
Result<void> deletePartition(int index) override;
Result<void> resizePartition(int index, SectorOffset newStart, SectorCount newSize) override;
Result<std::vector<uint8_t>> serialize() const override;
// Parse from read callback (reads protective MBR, primary GPT header + entries)
Result<void> parse(const DiskReadCallback& readFunc);
// Disk GUID
Guid diskGuid() const { return m_diskGuid; }
void setDiskGuid(const Guid& guid) { m_diskGuid = guid; }
// Header revision
uint32_t revision() const { return m_revision; }
// Usable LBA range
SectorOffset firstUsableLba() const { return m_firstUsableLba; }
SectorOffset lastUsableLba() const { return m_lastUsableLba; }
// Validate CRC32 of header and entry array
Result<void> validateCrcs(const DiskReadCallback& readFunc) const;
// Read backup GPT from end of disk
Result<void> parseBackup(const DiskReadCallback& readFunc);
private:
// Parse the entry array from raw bytes
Result<void> parseEntries(const std::vector<uint8_t>& entryData);
// Check for overlapping partitions
bool overlapsExisting(SectorOffset start, SectorOffset end, int excludeIndex = -1) const;
// Protective MBR bytes (preserved for serialization)
std::array<uint8_t, 512> m_protectiveMbr = {};
Guid m_diskGuid;
uint32_t m_revision = 0x00010000;
uint64_t m_firstUsableLba = 34;
uint64_t m_lastUsableLba = 0;
uint64_t m_alternateLba = 0;
uint32_t m_entryCount = GPT_MAX_PARTITIONS;
uint32_t m_entrySize = GPT_ENTRY_SIZE;
std::vector<PartitionEntry> m_entries;
};
// ============================================================================
// APM partition table (read-only)
// ============================================================================
class ApmPartitionTable : public PartitionTable
{
public:
ApmPartitionTable();
PartitionTableType type() const override { return PartitionTableType::APM; }
std::vector<PartitionEntry> partitions() const override;
// APM is read-only in this implementation
Result<void> addPartition(const PartitionParams& params) override;
Result<void> deletePartition(int index) override;
Result<void> resizePartition(int index, SectorOffset newStart, SectorCount newSize) override;
Result<std::vector<uint8_t>> serialize() const override;
// Parse from read callback
Result<void> parse(const DiskReadCallback& readFunc);
// APM block size (from DDM, usually 512)
uint32_t blockSize() const { return m_blockSize; }
private:
uint32_t m_blockSize = 512;
uint32_t m_blockCount = 0;
std::vector<PartitionEntry> m_entries;
};
// ============================================================================
// CRC32 utility (for GPT header/entry validation)
// Uses the standard CRC-32/ISO-HDLC polynomial (0xEDB88320 reflected)
// ============================================================================
uint32_t crc32(const uint8_t* data, size_t length);
uint32_t crc32(const std::vector<uint8_t>& data);
} // namespace spw

View File

@@ -0,0 +1,493 @@
#include "RawDiskHandle.h"
#include <sstream>
#include <cstring>
namespace spw
{
// ---------------------------------------------------------------------------
// Helper: Build a Win32 error message string incorporating GetLastError().
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
// Destructor — RAII close
// ---------------------------------------------------------------------------
RawDiskHandle::~RawDiskHandle()
{
close();
}
// ---------------------------------------------------------------------------
// Move semantics
// ---------------------------------------------------------------------------
RawDiskHandle::RawDiskHandle(RawDiskHandle&& other) noexcept
: m_handle(other.m_handle)
, m_diskId(other.m_diskId)
, m_accessMode(other.m_accessMode)
{
other.m_handle = INVALID_HANDLE_VALUE;
other.m_diskId = -1;
}
RawDiskHandle& RawDiskHandle::operator=(RawDiskHandle&& other) noexcept
{
if (this != &other)
{
close();
m_handle = other.m_handle;
m_diskId = other.m_diskId;
m_accessMode = other.m_accessMode;
other.m_handle = INVALID_HANDLE_VALUE;
other.m_diskId = -1;
}
return *this;
}
// ---------------------------------------------------------------------------
// Open by disk index
// ---------------------------------------------------------------------------
Result<RawDiskHandle> RawDiskHandle::open(DiskId diskIndex, DiskAccessMode mode)
{
// Build \\.\PhysicalDriveN path
std::wostringstream pathStream;
pathStream << L"\\\\.\\PhysicalDrive" << diskIndex;
std::wstring path = pathStream.str();
auto result = openPath(path, mode);
if (result.isOk())
{
result.value().m_diskId = diskIndex;
}
return result;
}
// ---------------------------------------------------------------------------
// Open by explicit device path
// ---------------------------------------------------------------------------
Result<RawDiskHandle> RawDiskHandle::openPath(const std::wstring& devicePath, DiskAccessMode mode)
{
DWORD desiredAccess = GENERIC_READ;
if (mode == DiskAccessMode::ReadWrite)
{
desiredAccess |= GENERIC_WRITE;
}
// FILE_SHARE_READ | FILE_SHARE_WRITE is required for physical drives so other
// processes (including Windows itself) can still access the disk.
HANDLE handle = ::CreateFileW(
devicePath.c_str(),
desiredAccess,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, // No FILE_FLAG_OVERLAPPED; we use OVERLAPPED only for offset
nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
const DWORD lastErr = ::GetLastError();
if (lastErr == ERROR_ACCESS_DENIED)
{
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, lastErr,
"Access denied opening disk. Run as Administrator.");
}
if (lastErr == ERROR_FILE_NOT_FOUND || lastErr == ERROR_PATH_NOT_FOUND)
{
return ErrorInfo::fromWin32(ErrorCode::DiskNotFound, lastErr,
"Physical disk not found");
}
return makeWin32Error(ErrorCode::DiskReadError, "Failed to open disk handle");
}
RawDiskHandle diskHandle;
diskHandle.m_handle = handle;
diskHandle.m_diskId = -1; // Caller (open()) sets this
diskHandle.m_accessMode = mode;
return diskHandle;
}
// ---------------------------------------------------------------------------
bool RawDiskHandle::isValid() const
{
return m_handle != INVALID_HANDLE_VALUE;
}
void RawDiskHandle::close()
{
if (m_handle != INVALID_HANDLE_VALUE)
{
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
}
}
// ---------------------------------------------------------------------------
// Read sectors at a given LBA using an OVERLAPPED struct to specify the offset.
// ---------------------------------------------------------------------------
Result<std::vector<uint8_t>> RawDiskHandle::readSectors(
SectorOffset lba, SectorCount count, uint32_t sectorSize) const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid disk handle");
}
if (count == 0)
{
return std::vector<uint8_t>{};
}
const uint64_t byteOffset = lba * sectorSize;
const uint64_t totalBytes = count * sectorSize;
// Win32 ReadFile length is a DWORD (32-bit), so cap per-call reads.
// For very large reads we would loop, but typical sector reads are well under 4 GiB.
if (totalBytes > static_cast<uint64_t>(MAXDWORD))
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Read request exceeds maximum single ReadFile size");
}
std::vector<uint8_t> buffer(static_cast<size_t>(totalBytes));
// Use OVERLAPPED to set the file offset without calling SetFilePointerEx.
// Even though we did NOT open with FILE_FLAG_OVERLAPPED, Windows still uses
// the Offset/OffsetHigh fields when you pass an OVERLAPPED to ReadFile on
// a synchronous handle — the call blocks and reads from the specified offset.
OVERLAPPED ov = {};
ov.Offset = static_cast<DWORD>(byteOffset & 0xFFFFFFFF);
ov.OffsetHigh = static_cast<DWORD>(byteOffset >> 32);
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(m_handle, buffer.data(), static_cast<DWORD>(totalBytes),
&bytesRead, &ov);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError, "ReadFile failed on physical disk");
}
if (bytesRead != static_cast<DWORD>(totalBytes))
{
// Partial read — resize buffer to what we actually got
buffer.resize(bytesRead);
}
return buffer;
}
// ---------------------------------------------------------------------------
// Write sectors at a given LBA.
// ---------------------------------------------------------------------------
Result<void> RawDiskHandle::writeSectors(
SectorOffset lba, const uint8_t* data, SectorCount count, uint32_t sectorSize) const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Invalid disk handle");
}
if (m_accessMode != DiskAccessMode::ReadWrite)
{
return ErrorInfo::fromCode(ErrorCode::DiskAccessDenied,
"Handle opened read-only, cannot write");
}
if (count == 0)
{
return Result<void>::ok();
}
if (!data)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Null data pointer");
}
const uint64_t byteOffset = lba * sectorSize;
const uint64_t totalBytes = count * sectorSize;
if (totalBytes > static_cast<uint64_t>(MAXDWORD))
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Write request exceeds maximum single WriteFile size");
}
OVERLAPPED ov = {};
ov.Offset = static_cast<DWORD>(byteOffset & 0xFFFFFFFF);
ov.OffsetHigh = static_cast<DWORD>(byteOffset >> 32);
DWORD bytesWritten = 0;
BOOL ok = ::WriteFile(m_handle, data, static_cast<DWORD>(totalBytes),
&bytesWritten, &ov);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskWriteError, "WriteFile failed on physical disk");
}
if (bytesWritten != static_cast<DWORD>(totalBytes))
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError,
"Partial write: not all sectors were written");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// IOCTL_DISK_GET_DRIVE_GEOMETRY_EX
// ---------------------------------------------------------------------------
Result<DiskGeometryInfo> RawDiskHandle::getGeometry() const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid disk handle");
}
// DISK_GEOMETRY_EX is a variable-length struct; allocate enough space for the
// base structure plus detection/partition info that Windows may append.
uint8_t buffer[256] = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
m_handle,
IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
nullptr, 0,
buffer, sizeof(buffer),
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError,
"IOCTL_DISK_GET_DRIVE_GEOMETRY_EX failed");
}
const auto* geomEx = reinterpret_cast<const DISK_GEOMETRY_EX*>(buffer);
const DISK_GEOMETRY& geom = geomEx->Geometry;
DiskGeometryInfo info;
info.totalBytes = static_cast<uint64_t>(geomEx->DiskSize.QuadPart);
info.bytesPerSector = geom.BytesPerSector;
info.sectorsPerTrack = geom.SectorsPerTrack;
info.tracksPerCylinder = geom.TracksPerCylinder;
info.cylinders = static_cast<uint64_t>(geom.Cylinders.QuadPart);
info.mediaType = geom.MediaType;
return info;
}
// ---------------------------------------------------------------------------
// IOCTL_DISK_GET_DRIVE_LAYOUT_EX
// ---------------------------------------------------------------------------
Result<DriveLayoutInfo> RawDiskHandle::getDriveLayout() const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid disk handle");
}
// The output is DRIVE_LAYOUT_INFORMATION_EX followed by a variable number of
// PARTITION_INFORMATION_EX entries. Allocate generously.
constexpr size_t kBufferSize = sizeof(DRIVE_LAYOUT_INFORMATION_EX)
+ 128 * sizeof(PARTITION_INFORMATION_EX);
std::vector<uint8_t> buffer(kBufferSize, 0);
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
m_handle,
IOCTL_DISK_GET_DRIVE_LAYOUT_EX,
nullptr, 0,
buffer.data(), static_cast<DWORD>(buffer.size()),
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError,
"IOCTL_DISK_GET_DRIVE_LAYOUT_EX failed");
}
const auto* layout = reinterpret_cast<const DRIVE_LAYOUT_INFORMATION_EX*>(buffer.data());
DriveLayoutInfo result;
result.partitionCount = layout->PartitionCount;
switch (layout->PartitionStyle)
{
case PARTITION_STYLE_MBR:
result.partitionStyle = PartitionTableType::MBR;
result.mbrSignature = layout->Mbr.Signature;
break;
case PARTITION_STYLE_GPT:
result.partitionStyle = PartitionTableType::GPT;
std::memcpy(result.gptDiskId.data, &layout->Gpt.DiskId, 16);
break;
default:
result.partitionStyle = PartitionTableType::Unknown;
break;
}
for (DWORD i = 0; i < layout->PartitionCount; ++i)
{
const PARTITION_INFORMATION_EX& partEx = layout->PartitionEntry[i];
// Windows may return entries with zero length for "empty" slots
if (partEx.PartitionLength.QuadPart == 0)
continue;
DriveLayoutPartition part;
part.partitionNumber = partEx.PartitionNumber;
part.startingOffset = static_cast<uint64_t>(partEx.StartingOffset.QuadPart);
part.partitionLength = static_cast<uint64_t>(partEx.PartitionLength.QuadPart);
part.rewritePartition = (partEx.RewritePartition != FALSE);
part.isRecognized = (partEx.PartitionStyle == PARTITION_STYLE_GPT)
|| IsRecognizedPartition(partEx.Mbr.PartitionType);
if (partEx.PartitionStyle == PARTITION_STYLE_MBR)
{
part.mbrPartitionType = partEx.Mbr.PartitionType;
part.mbrBootIndicator = (partEx.Mbr.BootIndicator != FALSE);
}
else if (partEx.PartitionStyle == PARTITION_STYLE_GPT)
{
std::memcpy(part.gptPartitionType.data, &partEx.Gpt.PartitionType, 16);
std::memcpy(part.gptPartitionId.data, &partEx.Gpt.PartitionId, 16);
part.gptAttributes = partEx.Gpt.Attributes;
part.gptName = partEx.Gpt.Name;
}
result.partitions.push_back(std::move(part));
}
return result;
}
// ---------------------------------------------------------------------------
// Lock a volume by its drive letter. Opens \\.\X: and sends FSCTL_LOCK_VOLUME.
// Returns the handle (caller must close it or pass to unlockVolume).
// ---------------------------------------------------------------------------
Result<HANDLE> RawDiskHandle::lockVolume(wchar_t volumeLetter)
{
wchar_t path[] = L"\\\\.\\X:";
path[4] = volumeLetter;
HANDLE hVolume = ::CreateFileW(
path,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (hVolume == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::DiskAccessDenied, "Failed to open volume for locking");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
hVolume,
FSCTL_LOCK_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
if (!ok)
{
DWORD err = ::GetLastError();
::CloseHandle(hVolume);
return ErrorInfo::fromWin32(ErrorCode::DiskLockFailed, err,
"FSCTL_LOCK_VOLUME failed — volume may be in use");
}
return hVolume;
}
// ---------------------------------------------------------------------------
Result<void> RawDiskHandle::unlockVolume(HANDLE volumeHandle)
{
if (volumeHandle == INVALID_HANDLE_VALUE)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Invalid volume handle");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
volumeHandle,
FSCTL_UNLOCK_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskLockFailed, "FSCTL_UNLOCK_VOLUME failed");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
Result<void> RawDiskHandle::dismountVolume(wchar_t volumeLetter)
{
wchar_t path[] = L"\\\\.\\X:";
path[4] = volumeLetter;
HANDLE hVolume = ::CreateFileW(
path,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (hVolume == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::DiskAccessDenied, "Failed to open volume for dismount");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
hVolume,
FSCTL_DISMOUNT_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
DWORD err = ::GetLastError();
::CloseHandle(hVolume);
if (!ok)
{
return ErrorInfo::fromWin32(ErrorCode::DiskDismountFailed, err,
"FSCTL_DISMOUNT_VOLUME failed");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
Result<void> RawDiskHandle::flushBuffers() const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Invalid disk handle");
}
if (!::FlushFileBuffers(m_handle))
{
return makeWin32Error(ErrorCode::DiskWriteError, "FlushFileBuffers failed");
}
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,127 @@
#pragma once
// RawDiskHandle — RAII wrapper for raw physical disk access via \\.\PhysicalDriveN.
// All operations return Result<T> so callers must handle errors explicitly.
// DISCLAIMER: This code is for authorized disk utility software only.
#include <windows.h>
#include <winioctl.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace spw
{
// Parsed disk geometry returned from IOCTL_DISK_GET_DRIVE_GEOMETRY_EX
struct DiskGeometryInfo
{
uint64_t totalBytes = 0;
uint32_t bytesPerSector = 0;
uint32_t sectorsPerTrack = 0;
uint32_t tracksPerCylinder = 0;
uint64_t cylinders = 0;
MEDIA_TYPE mediaType = Unknown;
};
// Partition entry from IOCTL_DISK_GET_DRIVE_LAYOUT_EX
struct DriveLayoutPartition
{
uint32_t partitionNumber = 0;
uint64_t startingOffset = 0;
uint64_t partitionLength = 0;
bool rewritePartition = false;
bool isRecognized = false;
// MBR-specific
uint8_t mbrPartitionType = 0;
bool mbrBootIndicator = false;
// GPT-specific
Guid gptPartitionType;
Guid gptPartitionId;
uint64_t gptAttributes = 0;
std::wstring gptName;
};
// Full drive layout
struct DriveLayoutInfo
{
PartitionTableType partitionStyle = PartitionTableType::Unknown;
uint32_t partitionCount = 0;
// MBR-specific
uint32_t mbrSignature = 0;
// GPT-specific
Guid gptDiskId;
std::vector<DriveLayoutPartition> partitions;
};
class RawDiskHandle
{
public:
RawDiskHandle() = default;
~RawDiskHandle();
// Non-copyable, movable
RawDiskHandle(const RawDiskHandle&) = delete;
RawDiskHandle& operator=(const RawDiskHandle&) = delete;
RawDiskHandle(RawDiskHandle&& other) noexcept;
RawDiskHandle& operator=(RawDiskHandle&& other) noexcept;
// Open \\.\PhysicalDriveN
static Result<RawDiskHandle> open(DiskId diskIndex, DiskAccessMode mode);
// Open by explicit device path (e.g. "\\.\PhysicalDrive0")
static Result<RawDiskHandle> openPath(const std::wstring& devicePath, DiskAccessMode mode);
// Returns true if the handle is valid
bool isValid() const;
// Close the handle (also called by destructor)
void close();
// Read sectors starting at the given LBA. Returns the data read.
Result<std::vector<uint8_t>> readSectors(SectorOffset lba, SectorCount count, uint32_t sectorSize) const;
// Write sectors at the given LBA. Buffer size must be a multiple of sectorSize.
Result<void> writeSectors(SectorOffset lba, const uint8_t* data, SectorCount count, uint32_t sectorSize) const;
// Get disk geometry
Result<DiskGeometryInfo> getGeometry() const;
// Get drive layout (partition table)
Result<DriveLayoutInfo> getDriveLayout() const;
// Lock a volume on this disk. volumeLetter is e.g. L'C'.
// This opens \\.\X: internally and locks it.
static Result<HANDLE> lockVolume(wchar_t volumeLetter);
// Unlock a previously locked volume handle
static Result<void> unlockVolume(HANDLE volumeHandle);
// Dismount a volume by its letter
static Result<void> dismountVolume(wchar_t volumeLetter);
// Flush disk write buffers
Result<void> flushBuffers() const;
// Raw Win32 handle accessor (for advanced use)
HANDLE nativeHandle() const { return m_handle; }
DiskId diskId() const { return m_diskId; }
private:
HANDLE m_handle = INVALID_HANDLE_VALUE;
DiskId m_diskId = -1;
DiskAccessMode m_accessMode = DiskAccessMode::ReadOnly;
};
} // namespace spw

View File

@@ -0,0 +1,637 @@
#include "SmartReader.h"
#include <winioctl.h>
#include <ntddscsi.h>
// For NVMe: StorageAdapterProtocolSpecificProperty and STORAGE_PROTOCOL_SPECIFIC_DATA
// are available in ntddstor.h on Windows 10+.
#include <ntddstor.h>
#include <cstring>
#include <sstream>
// Link against required libraries
#pragma comment(lib, "kernel32.lib")
namespace spw
{
// ---------------------------------------------------------------------------
// ATA command constants for S.M.A.R.T.
// Reference: ATA/ATAPI Command Set (ACS-3), Section 7.51
// ---------------------------------------------------------------------------
static constexpr uint8_t ATA_SMART_CMD = 0xB0;
static constexpr uint8_t ATA_SMART_READ_DATA = 0xD0;
static constexpr uint8_t ATA_SMART_READ_THRESHOLDS = 0xD1;
static constexpr uint8_t ATA_SMART_ENABLE = 0xD8;
static constexpr uint8_t ATA_SMART_LBA_MID = 0x4F;
static constexpr uint8_t ATA_SMART_LBA_HI = 0xC2;
// S.M.A.R.T. data sector is always 512 bytes
static constexpr uint32_t SMART_DATA_SIZE = 512;
// Each ATA S.M.A.R.T. attribute entry is 12 bytes, starting at offset 2 in the data sector.
// There can be up to 30 attributes.
static constexpr int SMART_ATTR_ENTRY_SIZE = 12;
static constexpr int SMART_ATTR_START_OFFSET = 2;
static constexpr int SMART_MAX_ATTRS = 30;
// Threshold entries are also 12 bytes each, starting at offset 2 in the threshold sector.
static constexpr int SMART_THRESH_ENTRY_SIZE = 12;
static constexpr int SMART_THRESH_START_OFFSET = 2;
// ---------------------------------------------------------------------------
// Helper: build Win32 error
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
// ATA PASS-THROUGH structure for 28-bit commands.
// We use ATA_PASS_THROUGH_EX which is available on all modern Windows.
// ---------------------------------------------------------------------------
#pragma pack(push, 1)
struct AtaSmartReadCmd
{
ATA_PASS_THROUGH_EX header;
uint8_t dataBuffer[SMART_DATA_SIZE];
};
#pragma pack(pop)
// ---------------------------------------------------------------------------
// Send an ATA S.M.A.R.T. command and receive 512 bytes of data back.
// ---------------------------------------------------------------------------
static Result<std::vector<uint8_t>> sendAtaSmartRead(HANDLE diskHandle, uint8_t feature)
{
AtaSmartReadCmd cmd = {};
cmd.header.Length = sizeof(ATA_PASS_THROUGH_EX);
cmd.header.AtaFlags = ATA_FLAGS_DATA_IN | ATA_FLAGS_DRDY_REQUIRED;
cmd.header.DataTransferLength = SMART_DATA_SIZE;
cmd.header.TimeOutValue = 10; // seconds
// DataBufferOffset is the offset from the start of the structure to the data buffer
cmd.header.DataBufferOffset = offsetof(AtaSmartReadCmd, dataBuffer);
// Set up the ATA task file registers for S.M.A.R.T. READ DATA
auto& tf = cmd.header.CurrentTaskFile;
tf[0] = feature; // Feature register (0xD0 = read data, 0xD1 = read thresholds)
tf[1] = 0; // Sector Count
tf[2] = 0; // Sector Number (LBA low)
tf[3] = ATA_SMART_LBA_MID; // Cylinder Low (LBA mid) = 0x4F
tf[4] = ATA_SMART_LBA_HI; // Cylinder High (LBA high) = 0xC2
tf[5] = 0xA0; // Device/Head (master)
tf[6] = ATA_SMART_CMD; // Command register = 0xB0
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
diskHandle,
IOCTL_ATA_PASS_THROUGH,
&cmd, sizeof(cmd),
&cmd, sizeof(cmd),
&bytesReturned, nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::SmartReadFailed,
"IOCTL_ATA_PASS_THROUGH failed for SMART command");
}
// Check the status register in the returned task file
// Bit 0 (ERR) indicates an error
if (cmd.header.CurrentTaskFile[6] & 0x01)
{
return ErrorInfo::fromCode(ErrorCode::SmartReadFailed,
"ATA SMART command returned error in status register");
}
std::vector<uint8_t> result(SMART_DATA_SIZE);
std::memcpy(result.data(), cmd.dataBuffer, SMART_DATA_SIZE);
return result;
}
// ---------------------------------------------------------------------------
// Parse ATA S.M.A.R.T. attributes from the 512-byte data sector.
//
// Data layout (ATA Spec):
// Offset 0: Revision number (2 bytes)
// Offset 2: Attribute entries, 12 bytes each, up to 30 entries
// Offset 362: Reserved
//
// Each 12-byte attribute entry:
// Byte 0: Attribute ID
// Byte 1-2: Status flags
// Byte 3: Current value (normalized, 1-253)
// Byte 4: Worst value
// Byte 5-10: Raw value (6 bytes, little-endian)
// Byte 11: Reserved
// ---------------------------------------------------------------------------
static std::vector<SmartAttribute> parseAtaAttributes(const uint8_t* data)
{
std::vector<SmartAttribute> attrs;
for (int i = 0; i < SMART_MAX_ATTRS; ++i)
{
int offset = SMART_ATTR_START_OFFSET + (i * SMART_ATTR_ENTRY_SIZE);
uint8_t attrId = data[offset];
if (attrId == 0) continue; // Empty slot
SmartAttribute attr;
attr.id = attrId;
attr.name = SmartReader::getAttributeName(attrId);
attr.currentValue = data[offset + 3];
attr.worstValue = data[offset + 4];
// Raw value: 6 bytes little-endian starting at offset+5
attr.rawValue = 0;
for (int b = 5; b >= 0; --b)
{
attr.rawValue = (attr.rawValue << 8) | data[offset + 5 + b];
}
attrs.push_back(attr);
}
return attrs;
}
// ---------------------------------------------------------------------------
// Parse S.M.A.R.T. thresholds from the 512-byte threshold sector.
//
// Threshold layout:
// Offset 0: Revision number (2 bytes)
// Offset 2: Threshold entries, 12 bytes each
//
// Each 12-byte threshold entry:
// Byte 0: Attribute ID
// Byte 1: Threshold value
// Byte 2-11: Reserved
// ---------------------------------------------------------------------------
static std::vector<std::pair<uint8_t, uint8_t>> parseAtaThresholds(const uint8_t* data)
{
std::vector<std::pair<uint8_t, uint8_t>> thresholds;
for (int i = 0; i < SMART_MAX_ATTRS; ++i)
{
int offset = SMART_THRESH_START_OFFSET + (i * SMART_THRESH_ENTRY_SIZE);
uint8_t attrId = data[offset];
if (attrId == 0) continue;
uint8_t threshold = data[offset + 1];
thresholds.emplace_back(attrId, threshold);
}
return thresholds;
}
// ---------------------------------------------------------------------------
// Read ATA S.M.A.R.T. data
// ---------------------------------------------------------------------------
Result<SmartData> SmartReader::readAtaSmart(HANDLE diskHandle, DiskId diskId)
{
// Read the S.M.A.R.T. data sector (feature 0xD0)
auto dataResult = sendAtaSmartRead(diskHandle, ATA_SMART_READ_DATA);
if (dataResult.isError())
{
return dataResult.error();
}
const auto& dataSector = dataResult.value();
// Parse attributes
auto attributes = parseAtaAttributes(dataSector.data());
// Read thresholds (feature 0xD1)
auto threshResult = sendAtaSmartRead(diskHandle, ATA_SMART_READ_THRESHOLDS);
if (threshResult.isOk())
{
auto thresholds = parseAtaThresholds(threshResult.value().data());
// Merge thresholds into attributes
for (auto& attr : attributes)
{
for (const auto& [threshId, threshVal] : thresholds)
{
if (threshId == attr.id)
{
attr.threshold = threshVal;
attr.status = evaluateAttributeHealth(
attr.currentValue, attr.worstValue, attr.threshold);
break;
}
}
// If no threshold was found, mark as OK if value looks healthy
if (attr.status == SmartStatus::Unknown)
{
attr.status = (attr.currentValue > 0) ? SmartStatus::OK : SmartStatus::Unknown;
}
}
}
else
{
// Thresholds not available — mark all attributes as Unknown status
for (auto& attr : attributes)
{
attr.status = SmartStatus::Unknown;
}
}
SmartData result;
result.diskId = diskId;
result.isNvme = false;
result.attributes = std::move(attributes);
result.overallHealth = evaluateOverallHealth(result.attributes);
return result;
}
// ---------------------------------------------------------------------------
// Read NVMe S.M.A.R.T. / Health Information Log
//
// We use IOCTL_STORAGE_QUERY_PROPERTY with:
// PropertyId = StorageDeviceProtocolSpecificProperty (50)
// QueryType = PropertyStandardQuery
// ProtocolType = ProtocolTypeNvme
// DataType = NVMeDataTypeLogPage (2)
// ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO (0x02)
//
// The NVMe SMART/Health Information log (NVMe spec 1.4, Figure 93) is 512 bytes.
// ---------------------------------------------------------------------------
Result<SmartData> SmartReader::readNvmeSmart(HANDLE diskHandle, DiskId diskId)
{
// Build the query buffer. The layout is:
// STORAGE_PROPERTY_QUERY (header)
// -> AdditionalParameters contains STORAGE_PROTOCOL_SPECIFIC_DATA
// followed by space for the returned NVMe log page data (512 bytes)
constexpr DWORD kNvmeHealthLogSize = 512;
constexpr DWORD kQueryBufSize = FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters)
+ sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)
+ kNvmeHealthLogSize;
std::vector<uint8_t> queryBuf(kQueryBufSize, 0);
auto* query = reinterpret_cast<STORAGE_PROPERTY_QUERY*>(queryBuf.data());
query->PropertyId = StorageDeviceProtocolSpecificProperty;
query->QueryType = PropertyStandardQuery;
auto* protocolData = reinterpret_cast<STORAGE_PROTOCOL_SPECIFIC_DATA*>(
query->AdditionalParameters);
protocolData->ProtocolType = ProtocolTypeNvme;
protocolData->DataType = NVMeDataTypeLogPage;
protocolData->ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO; // 0x02
protocolData->ProtocolDataRequestSubValue = 0;
protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
protocolData->ProtocolDataLength = kNvmeHealthLogSize;
// Output buffer: STORAGE_PROTOCOL_DATA_DESCRIPTOR + log page data
constexpr DWORD kOutputBufSize = FIELD_OFFSET(STORAGE_PROTOCOL_DATA_DESCRIPTOR, ProtocolSpecificData)
+ sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)
+ kNvmeHealthLogSize;
std::vector<uint8_t> outputBuf(kOutputBufSize, 0);
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
diskHandle,
IOCTL_STORAGE_QUERY_PROPERTY,
queryBuf.data(), kQueryBufSize,
outputBuf.data(), kOutputBufSize,
&bytesReturned, nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::SmartReadFailed,
"IOCTL_STORAGE_QUERY_PROPERTY failed for NVMe SMART log");
}
// Parse the returned data descriptor
auto* descriptor = reinterpret_cast<STORAGE_PROTOCOL_DATA_DESCRIPTOR*>(outputBuf.data());
auto* returnedProtocol = &descriptor->ProtocolSpecificData;
if (returnedProtocol->ProtocolDataLength < kNvmeHealthLogSize)
{
return ErrorInfo::fromCode(ErrorCode::SmartReadFailed,
"NVMe SMART log page returned insufficient data");
}
// The log page data starts at ProtocolSpecificData offset + ProtocolDataOffset
const uint8_t* logData = outputBuf.data()
+ FIELD_OFFSET(STORAGE_PROTOCOL_DATA_DESCRIPTOR, ProtocolSpecificData)
+ returnedProtocol->ProtocolDataOffset;
// Parse NVMe SMART/Health Information Log (NVMe spec 1.4, Figure 93):
// Byte 0: Critical Warning
// Byte 1-2: Composite Temperature (Kelvin)
// Byte 3: Available Spare (%)
// Byte 4: Available Spare Threshold (%)
// Byte 5: Percentage Used
// Byte 6-31: Reserved
// Byte 32-47: Data Units Read (128-bit, in units of 1000 x 512 bytes)
// Byte 48-63: Data Units Written
// Byte 64-79: Host Read Commands
// Byte 80-95: Host Write Commands
// Byte 96-111: Controller Busy Time (minutes)
// Byte 112-127: Power Cycles
// Byte 128-143: Power On Hours
// Byte 144-159: Unsafe Shutdowns
// Byte 160-175: Media and Data Integrity Errors
// Byte 176-191: Number of Error Information Log Entries
NvmeHealthInfo health = {};
health.criticalWarning = logData[0];
health.temperature = static_cast<uint16_t>(logData[1]) |
(static_cast<uint16_t>(logData[2]) << 8);
health.availableSpare = logData[3];
health.availableSpareThreshold = logData[4];
health.percentageUsed = logData[5];
// Helper lambda to read low 64 bits of a 128-bit little-endian value
auto readLow64 = [&logData](int offset) -> uint64_t {
uint64_t val = 0;
for (int i = 7; i >= 0; --i)
{
val = (val << 8) | logData[offset + i];
}
return val;
};
health.dataUnitsRead = readLow64(32);
health.dataUnitsWritten = readLow64(48);
health.hostReadCommands = readLow64(64);
health.hostWriteCommands = readLow64(80);
health.controllerBusyTime = readLow64(96);
health.powerCycles = readLow64(112);
health.powerOnHours = readLow64(128);
health.unsafeShutdowns = readLow64(144);
health.mediaErrors = readLow64(160);
health.errorLogEntries = readLow64(176);
SmartData result;
result.diskId = diskId;
result.isNvme = true;
result.nvmeHealth = health;
result.overallHealth = evaluateNvmeHealth(health);
return result;
}
// ---------------------------------------------------------------------------
// Detect if a disk is NVMe using IOCTL_STORAGE_QUERY_PROPERTY
// ---------------------------------------------------------------------------
Result<bool> SmartReader::isNvmeDrive(HANDLE diskHandle)
{
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageAdapterProperty;
query.QueryType = PropertyStandardQuery;
STORAGE_DESCRIPTOR_HEADER header = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
&header, sizeof(header),
&bytesReturned, nullptr);
if (!ok || header.Size == 0)
{
return false; // Can't determine, assume not NVMe
}
std::vector<uint8_t> buffer(header.Size, 0);
ok = ::DeviceIoControl(diskHandle, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
buffer.data(), static_cast<DWORD>(buffer.size()),
&bytesReturned, nullptr);
if (!ok) return false;
const auto* desc = reinterpret_cast<const STORAGE_ADAPTER_DESCRIPTOR*>(buffer.data());
return (desc->BusType == BusTypeNvme);
}
// ---------------------------------------------------------------------------
// Auto-detect ATA vs NVMe and read S.M.A.R.T.
// ---------------------------------------------------------------------------
Result<SmartData> SmartReader::readSmartData(HANDLE diskHandle, DiskId diskId)
{
auto nvmeResult = isNvmeDrive(diskHandle);
bool nvme = false;
if (nvmeResult.isOk())
nvme = nvmeResult.value();
if (nvme)
{
return readNvmeSmart(diskHandle, diskId);
}
else
{
return readAtaSmart(diskHandle, diskId);
}
}
// ---------------------------------------------------------------------------
// Read ATA thresholds (public API)
// ---------------------------------------------------------------------------
Result<std::vector<std::pair<uint8_t, uint8_t>>> SmartReader::readAtaSmartThresholds(HANDLE diskHandle)
{
auto result = sendAtaSmartRead(diskHandle, ATA_SMART_READ_THRESHOLDS);
if (result.isError()) return result.error();
return parseAtaThresholds(result.value().data());
}
// ---------------------------------------------------------------------------
// Attribute name lookup table.
// Standard S.M.A.R.T. attribute IDs are defined by the ATA spec and individual
// drive manufacturers. This covers the most common/important ones.
// ---------------------------------------------------------------------------
const char* SmartReader::getAttributeName(uint8_t attributeId)
{
switch (attributeId)
{
case 1: return "Raw Read Error Rate";
case 2: return "Throughput Performance";
case 3: return "Spin-Up Time";
case 4: return "Start/Stop Count";
case 5: return "Reallocated Sectors Count";
case 6: return "Read Channel Margin";
case 7: return "Seek Error Rate";
case 8: return "Seek Time Performance";
case 9: return "Power-On Hours";
case 10: return "Spin Retry Count";
case 11: return "Recalibration Retries";
case 12: return "Power Cycle Count";
case 13: return "Soft Read Error Rate";
case 170: return "Available Reserved Space";
case 171: return "SSD Program Fail Count";
case 172: return "SSD Erase Fail Count";
case 173: return "SSD Wear Leveling Count";
case 174: return "Unexpected Power Loss Count";
case 175: return "Power Loss Protection Failure";
case 176: return "Erase Fail Count (chip)";
case 177: return "Wear Range Delta";
case 178: return "Used Reserved Block Count (chip)";
case 179: return "Used Reserved Block Count (total)";
case 180: return "Unused Reserved Block Count (total)";
case 181: return "Program Fail Count (total)";
case 182: return "Erase Fail Count (total)";
case 183: return "Runtime Bad Block";
case 184: return "End-to-End Error";
case 187: return "Reported Uncorrectable Errors";
case 188: return "Command Timeout";
case 189: return "High Fly Writes";
case 190: return "Airflow Temperature";
case 191: return "G-Sense Error Rate";
case 192: return "Power-Off Retract Count";
case 193: return "Load/Unload Cycle Count";
case 194: return "Temperature";
case 195: return "Hardware ECC Recovered";
case 196: return "Reallocation Event Count";
case 197: return "Current Pending Sector Count";
case 198: return "Offline Uncorrectable Sector Count";
case 199: return "Ultra DMA CRC Error Count";
case 200: return "Multi-Zone Error Rate";
case 201: return "Soft Read Error Rate";
case 202: return "Data Address Mark Errors";
case 203: return "Run Out Cancel";
case 204: return "Soft ECC Correction";
case 205: return "Thermal Asperity Rate";
case 206: return "Flying Height";
case 207: return "Spin High Current";
case 208: return "Spin Buzz";
case 209: return "Offline Seek Performance";
case 220: return "Disk Shift";
case 221: return "G-Sense Error Rate";
case 222: return "Loaded Hours";
case 223: return "Load/Unload Retry Count";
case 224: return "Load Friction";
case 225: return "Load/Unload Cycle Count";
case 226: return "Load-In Time";
case 227: return "Torque Amplification Count";
case 228: return "Power-Off Retract Cycle";
case 230: return "GMR Head Amplitude";
case 231: return "Life Left (SSD)";
case 232: return "Endurance Remaining";
case 233: return "Media Wearout Indicator";
case 234: return "Average Erase Count";
case 235: return "Good Block Count / System Free Block Count";
case 240: return "Head Flying Hours";
case 241: return "Total LBAs Written";
case 242: return "Total LBAs Read";
case 243: return "Total LBAs Written Expanded";
case 244: return "Total LBAs Read Expanded";
case 249: return "NAND Writes (1 GiB)";
case 250: return "Read Error Retry Rate";
case 251: return "Minimum Spares Remaining";
case 252: return "Newly Added Bad Flash Block";
case 254: return "Free Fall Protection";
default: return "Unknown Attribute";
}
}
// ---------------------------------------------------------------------------
// Evaluate health of a single attribute
// ---------------------------------------------------------------------------
SmartStatus SmartReader::evaluateAttributeHealth(uint8_t currentValue, uint8_t worstValue,
uint8_t threshold)
{
if (threshold == 0)
{
// Threshold of 0 means "always passing" per ATA spec
return SmartStatus::OK;
}
// Critical: current value is at or below threshold
if (currentValue <= threshold)
{
return SmartStatus::Critical;
}
// Warning: worst value has been at or below threshold, or current is close
if (worstValue <= threshold)
{
return SmartStatus::Warning;
}
// Warning: within 10% of threshold (approaching failure)
if (threshold > 0 && currentValue < static_cast<uint16_t>(threshold) + 10)
{
return SmartStatus::Warning;
}
return SmartStatus::OK;
}
// ---------------------------------------------------------------------------
// Overall health from all ATA attributes.
// Any Critical attribute makes the overall status Critical.
// Any Warning without Critical makes it Warning.
// ---------------------------------------------------------------------------
SmartStatus SmartReader::evaluateOverallHealth(const std::vector<SmartAttribute>& attributes)
{
bool hasWarning = false;
bool hasUnknown = false;
for (const auto& attr : attributes)
{
switch (attr.status)
{
case SmartStatus::Critical:
return SmartStatus::Critical;
case SmartStatus::Warning:
hasWarning = true;
break;
case SmartStatus::Unknown:
hasUnknown = true;
break;
default:
break;
}
}
if (hasWarning) return SmartStatus::Warning;
if (attributes.empty() || hasUnknown) return SmartStatus::Unknown;
return SmartStatus::OK;
}
// ---------------------------------------------------------------------------
// NVMe overall health evaluation
// ---------------------------------------------------------------------------
SmartStatus SmartReader::evaluateNvmeHealth(const NvmeHealthInfo& health)
{
// Critical Warning byte: any bit set indicates a problem
// Bit 0: Available spare below threshold
// Bit 1: Temperature above or below threshold
// Bit 2: NVM subsystem reliability degraded
// Bit 3: Media placed in read-only mode
// Bit 4: Volatile memory backup device has failed
if (health.criticalWarning != 0)
{
// Bit 2 (reliability) or bit 3 (read-only) are critical
if (health.criticalWarning & 0x0C)
return SmartStatus::Critical;
return SmartStatus::Warning;
}
// Available spare below threshold
if (health.availableSpare > 0 && health.availableSpareThreshold > 0 &&
health.availableSpare <= health.availableSpareThreshold)
{
return SmartStatus::Critical;
}
// Percentage used > 100% indicates the drive has exceeded its rated endurance
if (health.percentageUsed > 100)
{
return SmartStatus::Warning;
}
// Media errors indicate data integrity issues
if (health.mediaErrors > 0)
{
return SmartStatus::Warning;
}
return SmartStatus::OK;
}
} // namespace spw

119
src/core/disk/SmartReader.h Normal file
View File

@@ -0,0 +1,119 @@
#pragma once
// SmartReader — Read and parse S.M.A.R.T. data from ATA and NVMe drives.
// ATA drives: IOCTL_ATA_PASS_THROUGH with SMART READ DATA (command 0xB0, feature 0xD0).
// NVMe drives: IOCTL_STORAGE_QUERY_PROPERTY with StorageAdapterProtocolSpecificProperty.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <winioctl.h>
// NVMe log page ID for health info (may not be defined in all SDK versions)
#ifndef NVME_LOG_PAGE_HEALTH_INFO
#define NVME_LOG_PAGE_HEALTH_INFO 0x02
#endif
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <cstdint>
#include <string>
#include <vector>
namespace spw
{
// Health status for individual attributes or overall drive
enum class SmartStatus
{
OK,
Warning,
Critical,
Unknown,
};
// A single S.M.A.R.T. attribute
struct SmartAttribute
{
uint8_t id = 0;
std::string name;
uint8_t currentValue = 0;
uint8_t worstValue = 0;
uint8_t threshold = 0;
uint64_t rawValue = 0;
SmartStatus status = SmartStatus::Unknown;
};
// NVMe health info (from SMART/Health Information Log, NVMe spec 1.4 Figure 93)
struct NvmeHealthInfo
{
uint8_t criticalWarning = 0;
uint16_t temperature = 0; // Kelvin
uint8_t availableSpare = 0; // percentage
uint8_t availableSpareThreshold = 0; // percentage
uint8_t percentageUsed = 0;
// These are 128-bit in the spec; we store low 64 bits which is sufficient for most drives
uint64_t dataUnitsRead = 0;
uint64_t dataUnitsWritten = 0;
uint64_t hostReadCommands = 0;
uint64_t hostWriteCommands = 0;
uint64_t controllerBusyTime = 0; // minutes
uint64_t powerCycles = 0;
uint64_t powerOnHours = 0;
uint64_t unsafeShutdowns = 0;
uint64_t mediaErrors = 0;
uint64_t errorLogEntries = 0;
};
// Overall S.M.A.R.T. result for a drive
struct SmartData
{
DiskId diskId = -1;
bool isNvme = false;
SmartStatus overallHealth = SmartStatus::Unknown;
// ATA attributes (empty for NVMe)
std::vector<SmartAttribute> attributes;
// NVMe health info (zeroed for ATA)
NvmeHealthInfo nvmeHealth = {};
};
namespace SmartReader
{
// Read S.M.A.R.T. data from a disk. Automatically detects ATA vs NVMe.
// Requires an open handle with at least GENERIC_READ | GENERIC_EXECUTE.
Result<SmartData> readSmartData(HANDLE diskHandle, DiskId diskId);
// Read ATA S.M.A.R.T. attributes via IOCTL_ATA_PASS_THROUGH.
Result<SmartData> readAtaSmart(HANDLE diskHandle, DiskId diskId);
// Read NVMe health info via IOCTL_STORAGE_QUERY_PROPERTY.
Result<SmartData> readNvmeSmart(HANDLE diskHandle, DiskId diskId);
// Read ATA S.M.A.R.T. thresholds (command 0xB0, feature 0xD1).
Result<std::vector<std::pair<uint8_t, uint8_t>>> readAtaSmartThresholds(HANDLE diskHandle);
// Determine if a disk supports NVMe protocol using IOCTL_STORAGE_QUERY_PROPERTY
// with StorageAdapterProtocolSpecificProperty.
Result<bool> isNvmeDrive(HANDLE diskHandle);
// Get the human-readable name for a standard S.M.A.R.T. attribute ID.
const char* getAttributeName(uint8_t attributeId);
// Calculate the health status for an individual attribute given its value and threshold.
SmartStatus evaluateAttributeHealth(uint8_t currentValue, uint8_t worstValue, uint8_t threshold);
// Calculate overall drive health from all attributes.
SmartStatus evaluateOverallHealth(const std::vector<SmartAttribute>& attributes);
// Calculate overall NVMe health from health info.
SmartStatus evaluateNvmeHealth(const NvmeHealthInfo& health);
} // namespace SmartReader
} // namespace spw

View File

@@ -0,0 +1,441 @@
#include "VolumeHandle.h"
#include <sstream>
namespace spw
{
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
// Destructor — unlock if locked, then close
// ---------------------------------------------------------------------------
VolumeHandle::~VolumeHandle()
{
// Best-effort unlock before close; ignore errors in destructor
if (m_locked && m_handle != INVALID_HANDLE_VALUE)
{
DWORD bytesReturned = 0;
::DeviceIoControl(m_handle, FSCTL_UNLOCK_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
}
close();
}
// ---------------------------------------------------------------------------
// Move semantics
// ---------------------------------------------------------------------------
VolumeHandle::VolumeHandle(VolumeHandle&& other) noexcept
: m_handle(other.m_handle)
, m_locked(other.m_locked)
, m_path(std::move(other.m_path))
{
other.m_handle = INVALID_HANDLE_VALUE;
other.m_locked = false;
}
VolumeHandle& VolumeHandle::operator=(VolumeHandle&& other) noexcept
{
if (this != &other)
{
// Clean up current state
if (m_locked && m_handle != INVALID_HANDLE_VALUE)
{
DWORD bytesReturned = 0;
::DeviceIoControl(m_handle, FSCTL_UNLOCK_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
}
close();
m_handle = other.m_handle;
m_locked = other.m_locked;
m_path = std::move(other.m_path);
other.m_handle = INVALID_HANDLE_VALUE;
other.m_locked = false;
}
return *this;
}
// ---------------------------------------------------------------------------
// Open by drive letter: builds \\.\X: path
// ---------------------------------------------------------------------------
Result<VolumeHandle> VolumeHandle::openByLetter(wchar_t driveLetter, DiskAccessMode mode)
{
wchar_t path[] = L"\\\\.\\X:";
path[4] = driveLetter;
return openPath(path, mode);
}
// ---------------------------------------------------------------------------
// Open by GUID path. The path typically looks like:
// \\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\
// For CreateFileW we need to strip the trailing backslash if present.
// ---------------------------------------------------------------------------
Result<VolumeHandle> VolumeHandle::openByGuid(const std::wstring& volumeGuidPath, DiskAccessMode mode)
{
std::wstring path = volumeGuidPath;
// CreateFileW requires no trailing backslash for raw volume access
if (!path.empty() && path.back() == L'\\')
{
path.pop_back();
}
return openPath(path, mode);
}
// ---------------------------------------------------------------------------
// Internal open helper
// ---------------------------------------------------------------------------
Result<VolumeHandle> VolumeHandle::openPath(const std::wstring& path, DiskAccessMode mode)
{
DWORD desiredAccess = GENERIC_READ;
if (mode == DiskAccessMode::ReadWrite)
{
desiredAccess |= GENERIC_WRITE;
}
HANDLE handle = ::CreateFileW(
path.c_str(),
desiredAccess,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
const DWORD lastErr = ::GetLastError();
if (lastErr == ERROR_ACCESS_DENIED)
{
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, lastErr,
"Access denied opening volume. Run as Administrator.");
}
return makeWin32Error(ErrorCode::DiskNotFound, "Failed to open volume");
}
VolumeHandle vol;
vol.m_handle = handle;
vol.m_path = path;
return vol;
}
// ---------------------------------------------------------------------------
bool VolumeHandle::isValid() const
{
return m_handle != INVALID_HANDLE_VALUE;
}
void VolumeHandle::close()
{
if (m_handle != INVALID_HANDLE_VALUE)
{
::CloseHandle(m_handle);
m_handle = INVALID_HANDLE_VALUE;
m_locked = false;
}
}
// ---------------------------------------------------------------------------
// FSCTL_LOCK_VOLUME — exclusive access
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::lock()
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskLockFailed, "Invalid volume handle");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
m_handle,
FSCTL_LOCK_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskLockFailed,
"FSCTL_LOCK_VOLUME failed — volume may be in use");
}
m_locked = true;
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// FSCTL_UNLOCK_VOLUME
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::unlock()
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskLockFailed, "Invalid volume handle");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
m_handle,
FSCTL_UNLOCK_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskLockFailed, "FSCTL_UNLOCK_VOLUME failed");
}
m_locked = false;
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// FSCTL_DISMOUNT_VOLUME — volume must be locked first
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::dismount()
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskDismountFailed, "Invalid volume handle");
}
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
m_handle,
FSCTL_DISMOUNT_VOLUME,
nullptr, 0,
nullptr, 0,
&bytesReturned,
nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskDismountFailed, "FSCTL_DISMOUNT_VOLUME failed");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// Read raw bytes from the volume at a specific byte offset.
// The offset and byteCount should be sector-aligned for raw volume access.
// ---------------------------------------------------------------------------
Result<std::vector<uint8_t>> VolumeHandle::readBytes(uint64_t byteOffset, uint32_t byteCount) const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid volume handle");
}
if (byteCount == 0)
{
return std::vector<uint8_t>{};
}
std::vector<uint8_t> buffer(byteCount);
OVERLAPPED ov = {};
ov.Offset = static_cast<DWORD>(byteOffset & 0xFFFFFFFF);
ov.OffsetHigh = static_cast<DWORD>(byteOffset >> 32);
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(m_handle, buffer.data(), byteCount, &bytesRead, &ov);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError, "ReadFile failed on volume");
}
if (bytesRead != byteCount)
{
buffer.resize(bytesRead);
}
return buffer;
}
// ---------------------------------------------------------------------------
// Write raw bytes to the volume at a specific byte offset.
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::writeBytes(uint64_t byteOffset, const uint8_t* data, uint32_t byteCount) const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Invalid volume handle");
}
if (byteCount == 0)
{
return Result<void>::ok();
}
if (!data)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Null data pointer");
}
OVERLAPPED ov = {};
ov.Offset = static_cast<DWORD>(byteOffset & 0xFFFFFFFF);
ov.OffsetHigh = static_cast<DWORD>(byteOffset >> 32);
DWORD bytesWritten = 0;
BOOL ok = ::WriteFile(m_handle, data, byteCount, &bytesWritten, &ov);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskWriteError, "WriteFile failed on volume");
}
if (bytesWritten != byteCount)
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Partial write to volume");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// GetVolumeInformationW by drive letter
// ---------------------------------------------------------------------------
Result<VolumeFilesystemInfo> VolumeHandle::getFilesystemInfo(wchar_t driveLetter)
{
wchar_t rootPath[] = L"X:\\";
rootPath[0] = driveLetter;
wchar_t volumeName[MAX_PATH + 1] = {};
wchar_t fsName[MAX_PATH + 1] = {};
DWORD serialNumber = 0;
DWORD maxComponentLen = 0;
DWORD fsFlags = 0;
BOOL ok = ::GetVolumeInformationW(
rootPath,
volumeName, MAX_PATH,
&serialNumber,
&maxComponentLen,
&fsFlags,
fsName, MAX_PATH);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError, "GetVolumeInformationW failed");
}
VolumeFilesystemInfo info;
info.volumeLabel = volumeName;
info.filesystemName = fsName;
info.serialNumber = serialNumber;
info.maxComponentLength = maxComponentLen;
info.filesystemFlags = fsFlags;
return info;
}
// ---------------------------------------------------------------------------
// GetVolumeInformationW by GUID path.
// The GUID path MUST have a trailing backslash for this API.
// ---------------------------------------------------------------------------
Result<VolumeFilesystemInfo> VolumeHandle::getFilesystemInfoByGuid(const std::wstring& volumeGuidPath)
{
std::wstring path = volumeGuidPath;
// Ensure trailing backslash
if (!path.empty() && path.back() != L'\\')
{
path.push_back(L'\\');
}
wchar_t volumeName[MAX_PATH + 1] = {};
wchar_t fsName[MAX_PATH + 1] = {};
DWORD serialNumber = 0;
DWORD maxComponentLen = 0;
DWORD fsFlags = 0;
BOOL ok = ::GetVolumeInformationW(
path.c_str(),
volumeName, MAX_PATH,
&serialNumber,
&maxComponentLen,
&fsFlags,
fsName, MAX_PATH);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError,
"GetVolumeInformationW failed for GUID path");
}
VolumeFilesystemInfo info;
info.volumeLabel = volumeName;
info.filesystemName = fsName;
info.serialNumber = serialNumber;
info.maxComponentLength = maxComponentLen;
info.filesystemFlags = fsFlags;
return info;
}
// ---------------------------------------------------------------------------
// GetDiskFreeSpaceExW by drive letter
// ---------------------------------------------------------------------------
Result<VolumeSpaceInfo> VolumeHandle::getSpaceInfo(wchar_t driveLetter)
{
wchar_t rootPath[] = L"X:\\";
rootPath[0] = driveLetter;
ULARGE_INTEGER freeBytesAvailable = {};
ULARGE_INTEGER totalBytes = {};
ULARGE_INTEGER totalFreeBytes = {};
BOOL ok = ::GetDiskFreeSpaceExW(
rootPath,
&freeBytesAvailable,
&totalBytes,
&totalFreeBytes);
if (!ok)
{
return makeWin32Error(ErrorCode::DiskReadError, "GetDiskFreeSpaceExW failed");
}
VolumeSpaceInfo info;
info.totalBytes = totalBytes.QuadPart;
info.freeBytes = totalFreeBytes.QuadPart;
info.availableBytes = freeBytesAvailable.QuadPart;
return info;
}
// ---------------------------------------------------------------------------
// DeleteVolumeMountPointW
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::deleteMountPoint(const std::wstring& mountPoint)
{
if (!::DeleteVolumeMountPointW(mountPoint.c_str()))
{
return makeWin32Error(ErrorCode::DiskWriteError, "DeleteVolumeMountPointW failed");
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
Result<void> VolumeHandle::flushBuffers() const
{
if (!isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Invalid volume handle");
}
if (!::FlushFileBuffers(m_handle))
{
return makeWin32Error(ErrorCode::DiskWriteError, "FlushFileBuffers failed on volume");
}
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,101 @@
#pragma once
// VolumeHandle — RAII wrapper for Windows volume access via \\.\X: or volume GUID paths.
// Supports locking, dismounting, reading/writing raw volume data, and querying volume info.
#include <windows.h>
#include <winioctl.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <cstdint>
#include <string>
#include <vector>
namespace spw
{
// Volume filesystem info from GetVolumeInformationW
struct VolumeFilesystemInfo
{
std::wstring volumeLabel;
std::wstring filesystemName; // e.g. L"NTFS", L"FAT32"
uint32_t serialNumber = 0;
uint32_t maxComponentLength = 0;
uint32_t filesystemFlags = 0;
};
// Volume space info
struct VolumeSpaceInfo
{
uint64_t totalBytes = 0;
uint64_t freeBytes = 0;
uint64_t availableBytes = 0; // available to current user (may differ from free if quotas)
};
class VolumeHandle
{
public:
VolumeHandle() = default;
~VolumeHandle();
// Non-copyable, movable
VolumeHandle(const VolumeHandle&) = delete;
VolumeHandle& operator=(const VolumeHandle&) = delete;
VolumeHandle(VolumeHandle&& other) noexcept;
VolumeHandle& operator=(VolumeHandle&& other) noexcept;
// Open volume by drive letter (e.g. L'C')
static Result<VolumeHandle> openByLetter(wchar_t driveLetter, DiskAccessMode mode);
// Open volume by GUID path (e.g. L"\\?\Volume{GUID}\")
static Result<VolumeHandle> openByGuid(const std::wstring& volumeGuidPath, DiskAccessMode mode);
bool isValid() const;
void close();
// Lock volume for exclusive access (FSCTL_LOCK_VOLUME)
Result<void> lock();
// Unlock volume (FSCTL_UNLOCK_VOLUME)
Result<void> unlock();
// Dismount volume (FSCTL_DISMOUNT_VOLUME). Volume must be locked first.
Result<void> dismount();
// Read raw bytes from the volume at a byte offset
Result<std::vector<uint8_t>> readBytes(uint64_t byteOffset, uint32_t byteCount) const;
// Write raw bytes to the volume at a byte offset
Result<void> writeBytes(uint64_t byteOffset, const uint8_t* data, uint32_t byteCount) const;
// Get volume filesystem info (label, FS name, serial, flags)
// Uses the drive root path (e.g. "C:\"), not the handle.
static Result<VolumeFilesystemInfo> getFilesystemInfo(wchar_t driveLetter);
// Get volume filesystem info by GUID path
static Result<VolumeFilesystemInfo> getFilesystemInfoByGuid(const std::wstring& volumeGuidPath);
// Get free/total space for a volume
static Result<VolumeSpaceInfo> getSpaceInfo(wchar_t driveLetter);
// Delete a volume mount point (e.g. to remove a drive letter assignment)
static Result<void> deleteMountPoint(const std::wstring& mountPoint);
// Flush buffers
Result<void> flushBuffers() const;
HANDLE nativeHandle() const { return m_handle; }
private:
// Internal open helper
static Result<VolumeHandle> openPath(const std::wstring& path, DiskAccessMode mode);
HANDLE m_handle = INVALID_HANDLE_VALUE;
bool m_locked = false;
std::wstring m_path;
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
#pragma once
// FormatEngine — Format partitions/volumes to any supported filesystem.
//
// For Windows-native formats (NTFS, FAT32<=32GB, exFAT, ReFS), delegates to
// format.com or DeviceIoControl. For Linux filesystems (ext2/3/4, swap) and
// large FAT32 (>32GB), writes on-disk structures directly.
//
// All operations lock and dismount the volume before writing.
// Supports quick format (structures only) and full format (zero + structures).
//
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../common/Constants.h"
#include "../disk/VolumeHandle.h"
#include "../disk/RawDiskHandle.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
#include <QString>
namespace spw
{
// Progress callback: (percent 0-100, status message)
using FormatProgressCallback = std::function<void(int percent, const QString& status)>;
// Options controlling format behavior
struct FormatOptions
{
FilesystemType targetFs = FilesystemType::NTFS;
std::string volumeLabel; // Volume label (max length depends on FS)
bool quickFormat = true; // false = zero entire partition first
uint32_t clusterSize = 0; // 0 = auto-select based on volume size
bool enableCompression = false; // NTFS only
bool enableJournal = true; // ext3/ext4: enable journal; ext2: ignored
// ext-family specific
uint32_t inodeSize = 256; // ext2/3/4 inode size (128 or 256)
uint32_t inodesPerGroup = 0; // 0 = auto
uint32_t blockSize = 0; // ext block size (1024/2048/4096), 0 = auto
bool enable64bit = true; // ext4: enable 64-bit feature
bool enableExtents = true; // ext4: enable extents
bool enableHugeFile = true; // ext4: enable huge_file
// Linux swap specific
uint32_t swapPageSize = 4096; // Usually 4096
// FAT32 large (>32GB) specific — bypass Windows limitation
bool forceFat32Large = false;
};
// Describes a volume to format — either by drive letter or by raw disk + offset
struct FormatTarget
{
// Option A: Format by drive letter (Windows volumes)
wchar_t driveLetter = 0;
// Option B: Format by raw disk + partition offset/size (for Linux FS, no mount point)
DiskId diskIndex = -1;
uint64_t partitionOffsetBytes = 0;
uint64_t partitionSizeBytes = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
// Returns true if targeting a drive letter
bool hasDriveLetter() const { return driveLetter != 0; }
// Returns true if targeting raw disk
bool hasRawTarget() const { return diskIndex >= 0 && partitionSizeBytes > 0; }
};
class FormatEngine
{
public:
FormatEngine() = default;
~FormatEngine() = default;
// Non-copyable
FormatEngine(const FormatEngine&) = delete;
FormatEngine& operator=(const FormatEngine&) = delete;
// Format a partition/volume with the given options.
// This is the main entry point — it dispatches to the appropriate formatter.
Result<void> format(const FormatTarget& target,
const FormatOptions& options,
FormatProgressCallback progress = nullptr);
// Query whether a filesystem type is supported for formatting
static bool isFormatSupported(FilesystemType fs);
// Get the recommended cluster/block size for a filesystem and volume size
static uint32_t recommendedClusterSize(FilesystemType fs, uint64_t volumeSizeBytes);
// Get maximum volume label length for a filesystem
static int maxLabelLength(FilesystemType fs);
private:
// ----- Windows-native formatters (delegate to format.com) -----
Result<void> formatWithWindowsTool(const FormatTarget& target,
const FormatOptions& options,
FormatProgressCallback progress);
// ----- Direct-write formatters -----
// ext2/ext3/ext4 — write superblock, group descriptors, bitmaps, inode table, root dir
Result<void> formatExt(const FormatTarget& target,
const FormatOptions& options,
FormatProgressCallback progress);
// FAT32 for volumes >32GB (Windows refuses)
Result<void> formatFat32Large(const FormatTarget& target,
const FormatOptions& options,
FormatProgressCallback progress);
// Linux swap — write swap header with UUID and SWAPSPACE2 magic
Result<void> formatLinuxSwap(const FormatTarget& target,
const FormatOptions& options,
FormatProgressCallback progress);
// ----- Helpers -----
// Zero the entire volume (for full format)
Result<void> zeroVolume(VolumeHandle& vol, uint64_t totalBytes,
FormatProgressCallback progress,
int progressStart, int progressEnd);
// Zero via raw disk handle
Result<void> zeroRaw(RawDiskHandle& disk, uint64_t offsetBytes,
uint64_t totalBytes, uint32_t sectorSize,
FormatProgressCallback progress,
int progressStart, int progressEnd);
// Lock and dismount a volume by drive letter, returning the handle
Result<VolumeHandle> lockAndDismount(wchar_t driveLetter);
// Notify the OS that partition geometry changed
static Result<void> notifyPartitionChange(DiskId diskIndex);
static Result<void> notifyPartitionChangeLetter(wchar_t driveLetter);
};
} // namespace spw

View File

@@ -0,0 +1,611 @@
#include "Checksums.h"
#include "../common/Constants.h"
#include <sstream>
#include <iomanip>
#include <memory>
// Link against bcrypt.lib for BCryptHashData, etc.
#pragma comment(lib, "bcrypt.lib")
namespace spw
{
// ---------------------------------------------------------------------------
// Hex string conversion helpers
// ---------------------------------------------------------------------------
std::string hashToHexString(const uint8_t* data, size_t length)
{
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < length; ++i)
{
oss << std::setw(2) << static_cast<unsigned>(data[i]);
}
return oss.str();
}
std::string sha256ToHex(const SHA256Hash& hash)
{
return hashToHexString(hash.data(), hash.size());
}
std::string md5ToHex(const MD5Hash& hash)
{
return hashToHexString(hash.data(), hash.size());
}
// ---------------------------------------------------------------------------
// RAII wrapper for BCrypt algorithm and hash handles.
// BCrypt is the modern Windows hashing API — it's always available on
// Windows Vista+ and doesn't require the legacy CryptoAPI.
// ---------------------------------------------------------------------------
class BcryptHasher
{
public:
~BcryptHasher()
{
if (m_hashHandle)
::BCryptDestroyHash(m_hashHandle);
if (m_algHandle)
::BCryptCloseAlgorithmProvider(m_algHandle, 0);
}
// algorithmId is e.g. BCRYPT_SHA256_ALGORITHM or BCRYPT_MD5_ALGORITHM
Result<void> init(const wchar_t* algorithmId)
{
NTSTATUS status = ::BCryptOpenAlgorithmProvider(
&m_algHandle, algorithmId, nullptr, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"BCryptOpenAlgorithmProvider failed");
}
// Query the hash object size so we can allocate the internal state buffer
DWORD hashObjectSize = 0;
DWORD cbData = 0;
status = ::BCryptGetProperty(
m_algHandle, BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&hashObjectSize),
sizeof(hashObjectSize), &cbData, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"BCryptGetProperty(OBJECT_LENGTH) failed");
}
// Query the hash output length
status = ::BCryptGetProperty(
m_algHandle, BCRYPT_HASH_LENGTH,
reinterpret_cast<PUCHAR>(&m_hashLength),
sizeof(m_hashLength), &cbData, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"BCryptGetProperty(HASH_LENGTH) failed");
}
m_hashObject.resize(hashObjectSize);
status = ::BCryptCreateHash(
m_algHandle, &m_hashHandle,
m_hashObject.data(), hashObjectSize,
nullptr, 0, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"BCryptCreateHash failed");
}
return Result<void>::ok();
}
Result<void> update(const uint8_t* data, size_t length)
{
// BCryptHashData takes a non-const pointer but doesn't modify the data.
// The const_cast is safe here.
NTSTATUS status = ::BCryptHashData(
m_hashHandle,
const_cast<PUCHAR>(data),
static_cast<ULONG>(length), 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown, "BCryptHashData failed");
}
return Result<void>::ok();
}
Result<void> finish(uint8_t* outputHash, size_t outputLength)
{
if (outputLength < m_hashLength)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Output buffer too small for hash result");
}
NTSTATUS status = ::BCryptFinishHash(
m_hashHandle, outputHash, m_hashLength, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::Unknown, "BCryptFinishHash failed");
}
return Result<void>::ok();
}
DWORD hashLength() const { return m_hashLength; }
private:
BCRYPT_ALG_HANDLE m_algHandle = nullptr;
BCRYPT_HASH_HANDLE m_hashHandle = nullptr;
std::vector<uint8_t> m_hashObject;
DWORD m_hashLength = 0;
};
// ---------------------------------------------------------------------------
// Internal: hash a file using BCrypt with a given algorithm
// ---------------------------------------------------------------------------
static Result<std::vector<uint8_t>> hashFileGeneric(
const wchar_t* algorithmId,
const std::wstring& filePath,
HashProgressCallback progressCb)
{
// Open file for sequential reading
HANDLE hFile = ::CreateFileW(
filePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return ErrorInfo::fromWin32(ErrorCode::FileNotFound,
::GetLastError(), "Failed to open file for hashing");
}
// Get file size for progress reporting
LARGE_INTEGER fileSize;
if (!::GetFileSizeEx(hFile, &fileSize))
{
::CloseHandle(hFile);
return ErrorInfo::fromWin32(ErrorCode::ImageReadError,
::GetLastError(), "Failed to get file size");
}
BcryptHasher hasher;
auto initResult = hasher.init(algorithmId);
if (initResult.isError())
{
::CloseHandle(hFile);
return initResult.error();
}
// Read in 4 MiB chunks — same chunk size as our imaging pipeline
constexpr DWORD kReadBufSize = IMAGE_CHUNK_SIZE;
std::vector<uint8_t> readBuffer(kReadBufSize);
uint64_t totalRead = 0;
for (;;)
{
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, readBuffer.data(), kReadBufSize, &bytesRead, nullptr);
if (!ok)
{
::CloseHandle(hFile);
return ErrorInfo::fromWin32(ErrorCode::ImageReadError,
::GetLastError(), "ReadFile failed during hashing");
}
if (bytesRead == 0)
break; // EOF
auto updateResult = hasher.update(readBuffer.data(), bytesRead);
if (updateResult.isError())
{
::CloseHandle(hFile);
return updateResult.error();
}
totalRead += bytesRead;
if (progressCb)
{
if (!progressCb(totalRead, static_cast<uint64_t>(fileSize.QuadPart)))
{
::CloseHandle(hFile);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Hash operation canceled by user");
}
}
}
::CloseHandle(hFile);
std::vector<uint8_t> hash(hasher.hashLength());
auto finishResult = hasher.finish(hash.data(), hash.size());
if (finishResult.isError())
return finishResult.error();
return hash;
}
// ---------------------------------------------------------------------------
// Internal: hash a range of sectors from a raw disk
// ---------------------------------------------------------------------------
static Result<std::vector<uint8_t>> hashDiskRangeGeneric(
const wchar_t* algorithmId,
const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb)
{
if (!disk.isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid disk handle");
}
if (sectorCount == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Cannot hash zero sectors");
}
BcryptHasher hasher;
auto initResult = hasher.init(algorithmId);
if (initResult.isError())
return initResult.error();
const uint64_t totalBytes = sectorCount * sectorSize;
// Read in chunks of IMAGE_CHUNK_SIZE bytes, rounded down to sector boundary
const SectorCount sectorsPerChunk = IMAGE_CHUNK_SIZE / sectorSize;
SectorOffset currentLba = startLba;
SectorCount remaining = sectorCount;
uint64_t bytesProcessed = 0;
while (remaining > 0)
{
const SectorCount chunkSectors = (remaining > sectorsPerChunk)
? sectorsPerChunk : remaining;
auto readResult = disk.readSectors(currentLba, chunkSectors, sectorSize);
if (readResult.isError())
return readResult.error();
const auto& data = readResult.value();
auto updateResult = hasher.update(data.data(), data.size());
if (updateResult.isError())
return updateResult.error();
currentLba += chunkSectors;
remaining -= chunkSectors;
bytesProcessed += data.size();
if (progressCb)
{
if (!progressCb(bytesProcessed, totalBytes))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Hash operation canceled by user");
}
}
}
std::vector<uint8_t> hash(hasher.hashLength());
auto finishResult = hasher.finish(hash.data(), hash.size());
if (finishResult.isError())
return finishResult.error();
return hash;
}
// ---------------------------------------------------------------------------
// CRC32 lookup table — computed at compile time using the standard polynomial.
// Polynomial: 0xEDB88320 (reversed representation of ISO 3309 / V.42).
// ---------------------------------------------------------------------------
static constexpr uint32_t kCrc32Polynomial = 0xEDB88320u;
struct Crc32Table
{
uint32_t entries[256] = {};
constexpr Crc32Table()
{
for (uint32_t i = 0; i < 256; ++i)
{
uint32_t crc = i;
for (int bit = 0; bit < 8; ++bit)
{
if (crc & 1)
crc = (crc >> 1) ^ kCrc32Polynomial;
else
crc >>= 1;
}
entries[i] = crc;
}
}
};
static constexpr Crc32Table kCrc32Table{};
namespace Checksums
{
// ---------------------------------------------------------------------------
// SHA-256
// ---------------------------------------------------------------------------
Result<SHA256Hash> sha256Buffer(const uint8_t* data, size_t length)
{
if (!data && length > 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Null data pointer with non-zero length");
}
BcryptHasher hasher;
auto initResult = hasher.init(BCRYPT_SHA256_ALGORITHM);
if (initResult.isError())
return initResult.error();
if (length > 0)
{
auto updateResult = hasher.update(data, length);
if (updateResult.isError())
return updateResult.error();
}
SHA256Hash hash = {};
auto finishResult = hasher.finish(hash.data(), hash.size());
if (finishResult.isError())
return finishResult.error();
return hash;
}
Result<SHA256Hash> sha256File(const std::wstring& filePath,
HashProgressCallback progressCb)
{
auto result = hashFileGeneric(BCRYPT_SHA256_ALGORITHM, filePath, progressCb);
if (result.isError())
return result.error();
SHA256Hash hash = {};
const auto& vec = result.value();
if (vec.size() >= 32)
std::memcpy(hash.data(), vec.data(), 32);
return hash;
}
Result<SHA256Hash> sha256DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb)
{
auto result = hashDiskRangeGeneric(
BCRYPT_SHA256_ALGORITHM, disk, startLba, sectorCount, sectorSize, progressCb);
if (result.isError())
return result.error();
SHA256Hash hash = {};
const auto& vec = result.value();
if (vec.size() >= 32)
std::memcpy(hash.data(), vec.data(), 32);
return hash;
}
// ---------------------------------------------------------------------------
// MD5
// ---------------------------------------------------------------------------
Result<MD5Hash> md5Buffer(const uint8_t* data, size_t length)
{
if (!data && length > 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Null data pointer with non-zero length");
}
BcryptHasher hasher;
auto initResult = hasher.init(BCRYPT_MD5_ALGORITHM);
if (initResult.isError())
return initResult.error();
if (length > 0)
{
auto updateResult = hasher.update(data, length);
if (updateResult.isError())
return updateResult.error();
}
MD5Hash hash = {};
auto finishResult = hasher.finish(hash.data(), hash.size());
if (finishResult.isError())
return finishResult.error();
return hash;
}
Result<MD5Hash> md5File(const std::wstring& filePath,
HashProgressCallback progressCb)
{
auto result = hashFileGeneric(BCRYPT_MD5_ALGORITHM, filePath, progressCb);
if (result.isError())
return result.error();
MD5Hash hash = {};
const auto& vec = result.value();
if (vec.size() >= 16)
std::memcpy(hash.data(), vec.data(), 16);
return hash;
}
Result<MD5Hash> md5DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb)
{
auto result = hashDiskRangeGeneric(
BCRYPT_MD5_ALGORITHM, disk, startLba, sectorCount, sectorSize, progressCb);
if (result.isError())
return result.error();
MD5Hash hash = {};
const auto& vec = result.value();
if (vec.size() >= 16)
std::memcpy(hash.data(), vec.data(), 16);
return hash;
}
// ---------------------------------------------------------------------------
// CRC32
// ---------------------------------------------------------------------------
uint32_t crc32Update(uint32_t previousCrc, const uint8_t* data, size_t length)
{
// Standard CRC32 algorithm: XOR-in, table lookup, XOR-out.
// The initial value is the bitwise inverse of the previous CRC so that
// the first call with previousCrc=0 starts with 0xFFFFFFFF as required.
uint32_t crc = previousCrc ^ 0xFFFFFFFF;
for (size_t i = 0; i < length; ++i)
{
const uint8_t tableIndex = static_cast<uint8_t>(crc ^ data[i]);
crc = (crc >> 8) ^ kCrc32Table.entries[tableIndex];
}
return crc ^ 0xFFFFFFFF;
}
uint32_t crc32Buffer(const uint8_t* data, size_t length)
{
return crc32Update(0, data, length);
}
Result<uint32_t> crc32File(const std::wstring& filePath,
HashProgressCallback progressCb)
{
HANDLE hFile = ::CreateFileW(
filePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return ErrorInfo::fromWin32(ErrorCode::FileNotFound,
::GetLastError(), "Failed to open file for CRC32");
}
LARGE_INTEGER fileSize;
if (!::GetFileSizeEx(hFile, &fileSize))
{
::CloseHandle(hFile);
return ErrorInfo::fromWin32(ErrorCode::ImageReadError,
::GetLastError(), "Failed to get file size");
}
constexpr DWORD kReadBufSize = IMAGE_CHUNK_SIZE;
std::vector<uint8_t> readBuffer(kReadBufSize);
uint32_t crc = 0;
uint64_t totalRead = 0;
for (;;)
{
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, readBuffer.data(), kReadBufSize, &bytesRead, nullptr);
if (!ok)
{
::CloseHandle(hFile);
return ErrorInfo::fromWin32(ErrorCode::ImageReadError,
::GetLastError(), "ReadFile failed during CRC32");
}
if (bytesRead == 0)
break;
crc = crc32Update(crc, readBuffer.data(), bytesRead);
totalRead += bytesRead;
if (progressCb)
{
if (!progressCb(totalRead, static_cast<uint64_t>(fileSize.QuadPart)))
{
::CloseHandle(hFile);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"CRC32 operation canceled by user");
}
}
}
::CloseHandle(hFile);
return crc;
}
Result<uint32_t> crc32DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb)
{
if (!disk.isValid())
{
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Invalid disk handle");
}
if (sectorCount == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Cannot CRC32 zero sectors");
}
const uint64_t totalBytes = sectorCount * sectorSize;
const SectorCount sectorsPerChunk = IMAGE_CHUNK_SIZE / sectorSize;
SectorOffset currentLba = startLba;
SectorCount remaining = sectorCount;
uint64_t bytesProcessed = 0;
uint32_t crc = 0;
while (remaining > 0)
{
const SectorCount chunkSectors = (remaining > sectorsPerChunk)
? sectorsPerChunk : remaining;
auto readResult = disk.readSectors(currentLba, chunkSectors, sectorSize);
if (readResult.isError())
return readResult.error();
const auto& data = readResult.value();
crc = crc32Update(crc, data.data(), data.size());
currentLba += chunkSectors;
remaining -= chunkSectors;
bytesProcessed += data.size();
if (progressCb)
{
if (!progressCb(bytesProcessed, totalBytes))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"CRC32 operation canceled by user");
}
}
}
return crc;
}
} // namespace Checksums
} // namespace spw

View File

@@ -0,0 +1,97 @@
#pragma once
// Checksums — Cryptographic and non-cryptographic hash utilities for disk imaging.
// Uses Windows BCrypt API for SHA-256 and MD5. CRC32 is a pure software implementation.
// All operations support progress callbacks for hashing large disk regions.
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <bcrypt.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include <array>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Fixed-size hash results
using SHA256Hash = std::array<uint8_t, 32>;
using MD5Hash = std::array<uint8_t, 16>;
// Progress callback: (bytesProcessed, totalBytes) -> return false to cancel
using HashProgressCallback = std::function<bool(uint64_t bytesProcessed, uint64_t totalBytes)>;
// Convert hash bytes to lowercase hex string
std::string hashToHexString(const uint8_t* data, size_t length);
std::string sha256ToHex(const SHA256Hash& hash);
std::string md5ToHex(const MD5Hash& hash);
namespace Checksums
{
// ---------------------------------------------------------------------------
// SHA-256
// ---------------------------------------------------------------------------
// Hash an in-memory buffer
Result<SHA256Hash> sha256Buffer(const uint8_t* data, size_t length);
// Hash an entire file on disk
Result<SHA256Hash> sha256File(const std::wstring& filePath,
HashProgressCallback progressCb = nullptr);
// Hash a range of sectors from a raw disk
Result<SHA256Hash> sha256DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb = nullptr);
// ---------------------------------------------------------------------------
// MD5 (for legacy image verification)
// ---------------------------------------------------------------------------
Result<MD5Hash> md5Buffer(const uint8_t* data, size_t length);
Result<MD5Hash> md5File(const std::wstring& filePath,
HashProgressCallback progressCb = nullptr);
Result<MD5Hash> md5DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb = nullptr);
// ---------------------------------------------------------------------------
// CRC32
// ---------------------------------------------------------------------------
// CRC32 (ISO 3309 / ITU-T V.42, same polynomial as zlib)
uint32_t crc32Buffer(const uint8_t* data, size_t length);
// Incremental CRC32: pass previous CRC (or 0 for first call)
uint32_t crc32Update(uint32_t previousCrc, const uint8_t* data, size_t length);
Result<uint32_t> crc32File(const std::wstring& filePath,
HashProgressCallback progressCb = nullptr);
Result<uint32_t> crc32DiskRange(const RawDiskHandle& disk,
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
HashProgressCallback progressCb = nullptr);
} // namespace Checksums
} // namespace spw

View File

@@ -0,0 +1,844 @@
#include "DiskCloner.h"
#include "Checksums.h"
#include "../common/Constants.h"
#include <algorithm>
#include <cstring>
#include <sstream>
namespace spw
{
// ---------------------------------------------------------------------------
// Helper: Win32 error with GetLastError()
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
// Cancel support
// ---------------------------------------------------------------------------
void DiskCloner::requestCancel()
{
m_cancelRequested.store(true, std::memory_order_release);
}
bool DiskCloner::isCancelRequested() const
{
return m_cancelRequested.load(std::memory_order_acquire);
}
// ---------------------------------------------------------------------------
// Progress reporting with speed/ETA
// ---------------------------------------------------------------------------
bool DiskCloner::reportProgress(
CloneProgressCallback& cb,
CloneProgress::Phase phase,
uint64_t bytesTransferred,
uint64_t totalBytes,
LARGE_INTEGER startTime,
LARGE_INTEGER perfFreq)
{
if (!cb)
return true;
CloneProgress progress;
progress.phase = phase;
progress.bytesTransferred = bytesTransferred;
progress.totalBytes = totalBytes;
if (totalBytes > 0)
{
progress.percentComplete =
static_cast<double>(bytesTransferred) / static_cast<double>(totalBytes) * 100.0;
}
// Calculate speed and ETA using high-resolution performance counter
LARGE_INTEGER now;
::QueryPerformanceCounter(&now);
const double elapsedSec =
static_cast<double>(now.QuadPart - startTime.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
if (elapsedSec > 0.0)
{
progress.speedBytesPerSec =
static_cast<double>(bytesTransferred) / elapsedSec;
if (progress.speedBytesPerSec > 0.0 && bytesTransferred < totalBytes)
{
const double remainingBytes =
static_cast<double>(totalBytes - bytesTransferred);
progress.etaSeconds = remainingBytes / progress.speedBytesPerSec;
}
}
return cb(progress);
}
// ---------------------------------------------------------------------------
// Lock destination volumes
// ---------------------------------------------------------------------------
Result<std::vector<HANDLE>> DiskCloner::lockDestinationVolumes(
const std::vector<wchar_t>& volumeLetters)
{
std::vector<HANDLE> lockedHandles;
for (wchar_t letter : volumeLetters)
{
// Dismount first — this invalidates all open file handles on the volume
auto dismountResult = RawDiskHandle::dismountVolume(letter);
if (dismountResult.isError())
{
// Non-fatal: volume might not be mounted. Log but continue.
}
// Lock the volume for exclusive access
auto lockResult = RawDiskHandle::lockVolume(letter);
if (lockResult.isError())
{
// Unlock anything we already locked
unlockVolumes(lockedHandles);
return ErrorInfo::fromCode(ErrorCode::DiskLockFailed,
std::string("Failed to lock volume ") +
static_cast<char>(letter) + ":");
}
lockedHandles.push_back(lockResult.value());
}
return lockedHandles;
}
// ---------------------------------------------------------------------------
// Unlock volumes
// ---------------------------------------------------------------------------
void DiskCloner::unlockVolumes(std::vector<HANDLE>& lockedHandles)
{
for (HANDLE h : lockedHandles)
{
if (h != INVALID_HANDLE_VALUE)
{
RawDiskHandle::unlockVolume(h);
::CloseHandle(h);
}
}
lockedHandles.clear();
}
// ---------------------------------------------------------------------------
// Main clone entry point
// ---------------------------------------------------------------------------
Result<void> DiskCloner::clone(const CloneConfig& config,
CloneProgressCallback progressCb)
{
m_cancelRequested.store(false, std::memory_order_release);
// Validate configuration
if (config.sourceDiskId < 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Invalid source disk ID");
}
if (config.destDiskId < 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Invalid destination disk ID");
}
if (config.sourceDiskId == config.destDiskId)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Source and destination cannot be the same disk");
}
// Open source (read-only) and destination (read-write)
auto srcResult = RawDiskHandle::open(config.sourceDiskId, DiskAccessMode::ReadOnly);
if (srcResult.isError())
return srcResult.error();
auto dstResult = RawDiskHandle::open(config.destDiskId, DiskAccessMode::ReadWrite);
if (dstResult.isError())
return dstResult.error();
auto& srcDisk = srcResult.value();
auto& dstDisk = dstResult.value();
// Get geometry for both disks
auto srcGeom = srcDisk.getGeometry();
if (srcGeom.isError())
return srcGeom.error();
auto dstGeom = dstDisk.getGeometry();
if (dstGeom.isError())
return dstGeom.error();
const uint32_t srcSectorSize = srcGeom.value().bytesPerSector;
const uint32_t dstSectorSize = dstGeom.value().bytesPerSector;
const uint64_t srcTotalBytes = srcGeom.value().totalBytes;
const uint64_t dstTotalBytes = dstGeom.value().totalBytes;
// Determine the byte range to clone
uint64_t srcOffset = config.sourceOffsetBytes;
uint64_t dstOffset = config.destOffsetBytes;
uint64_t cloneLength = config.sourceLengthBytes;
if (cloneLength == 0)
{
// Clone entire source disk
if (srcOffset > srcTotalBytes)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Source offset exceeds disk size");
}
cloneLength = srcTotalBytes - srcOffset;
}
// Validate source range fits in source disk
if (srcOffset + cloneLength > srcTotalBytes)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Source range exceeds source disk size");
}
// Validate destination has enough space
if (dstOffset + cloneLength > dstTotalBytes)
{
if (!config.allowTruncation)
{
return ErrorInfo::fromCode(ErrorCode::InsufficientDiskSpace,
"Destination disk is too small for the clone operation");
}
// Truncate to what fits
cloneLength = dstTotalBytes - dstOffset;
}
// Ensure offsets are aligned to the larger of the two sector sizes
const uint32_t alignmentSize = std::max(srcSectorSize, dstSectorSize);
if (srcOffset % alignmentSize != 0 || dstOffset % alignmentSize != 0)
{
return ErrorInfo::fromCode(ErrorCode::AlignmentError,
"Source and destination offsets must be sector-aligned");
}
// Lock and dismount destination volumes
std::vector<HANDLE> lockedVolumes;
if (!config.destVolumeLetters.empty())
{
auto lockResult = lockDestinationVolumes(config.destVolumeLetters);
if (lockResult.isError())
return lockResult.error();
lockedVolumes = std::move(lockResult.value());
}
// Perform the clone
Result<void> cloneResult = Result<void>::ok();
if (config.mode == CloneMode::Smart)
{
cloneResult = cloneSmart(
srcDisk, srcSectorSize, dstDisk, dstSectorSize,
srcOffset, cloneLength, dstOffset,
config.bufferSize, progressCb);
}
else
{
cloneResult = cloneRaw(
srcDisk, srcSectorSize, dstDisk, dstSectorSize,
srcOffset, cloneLength, dstOffset,
config.bufferSize, progressCb);
}
if (cloneResult.isError())
{
unlockVolumes(lockedVolumes);
return cloneResult;
}
// Flush destination disk to ensure all writes are committed
auto flushResult = dstDisk.flushBuffers();
if (flushResult.isError())
{
unlockVolumes(lockedVolumes);
return flushResult;
}
// Verification pass
if (config.verifyAfterClone)
{
auto verifyResult = verifyClone(
srcDisk, srcSectorSize, dstDisk, dstSectorSize,
srcOffset, cloneLength, dstOffset,
config.bufferSize, progressCb);
if (verifyResult.isError())
{
unlockVolumes(lockedVolumes);
return verifyResult;
}
}
// Report completion
if (progressCb)
{
CloneProgress done;
done.phase = CloneProgress::Phase::Complete;
done.bytesTransferred = cloneLength;
done.totalBytes = cloneLength;
done.percentComplete = 100.0;
progressCb(done);
}
unlockVolumes(lockedVolumes);
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// Raw sector-by-sector clone.
// Handles mismatched sector sizes by using an intermediate buffer aligned
// to the LCM of both sector sizes.
// ---------------------------------------------------------------------------
Result<void> DiskCloner::cloneRaw(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb)
{
// The I/O buffer must be a multiple of both sector sizes.
// Find the LCM of the two sector sizes and round bufferSize up.
// For 512 and 4096, LCM = 4096. For matching sizes, LCM = sectorSize.
const uint32_t maxSectorSize = std::max(srcSectorSize, dstSectorSize);
// Round buffer size down to a multiple of maxSectorSize
const uint32_t alignedBufSize =
(bufferSize / maxSectorSize) * maxSectorSize;
if (alignedBufSize == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Buffer size too small for sector alignment");
}
std::vector<uint8_t> ioBuffer(alignedBufSize);
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesRemaining = lengthBytes;
uint64_t bytesTransferred = 0;
uint64_t srcPos = srcOffsetBytes;
uint64_t dstPos = dstOffsetBytes;
while (bytesRemaining > 0)
{
if (isCancelRequested())
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Clone canceled by user");
}
// Determine chunk size for this iteration
const uint64_t chunkBytes = std::min(
static_cast<uint64_t>(alignedBufSize), bytesRemaining);
// Read from source. We use the source sector size for addressing.
const SectorOffset srcLba = srcPos / srcSectorSize;
const SectorCount srcSectors = static_cast<SectorCount>(
(chunkBytes + srcSectorSize - 1) / srcSectorSize);
auto readResult = src.readSectors(srcLba, srcSectors, srcSectorSize);
if (readResult.isError())
return readResult.error();
const auto& readData = readResult.value();
// The actual number of bytes we can write is the minimum of
// what we read and what we need
const size_t bytesToWrite = static_cast<size_t>(
std::min(static_cast<uint64_t>(readData.size()), chunkBytes));
// If sector sizes differ, we still write sector-aligned chunks.
// Pad the last chunk with zeros if needed.
const size_t alignedWriteSize =
((bytesToWrite + dstSectorSize - 1) / dstSectorSize) * dstSectorSize;
// Prepare write buffer (may need zero-padding at the end)
if (alignedWriteSize > readData.size())
{
std::memcpy(ioBuffer.data(), readData.data(), readData.size());
std::memset(ioBuffer.data() + readData.size(), 0,
alignedWriteSize - readData.size());
}
const uint8_t* writePtr =
(alignedWriteSize > readData.size()) ? ioBuffer.data() : readData.data();
// Write to destination
const SectorOffset dstLba = dstPos / dstSectorSize;
const SectorCount dstSectors = static_cast<SectorCount>(
alignedWriteSize / dstSectorSize);
auto writeResult = dst.writeSectors(dstLba, writePtr, dstSectors, dstSectorSize);
if (writeResult.isError())
return writeResult.error();
srcPos += bytesToWrite;
dstPos += bytesToWrite;
bytesTransferred += bytesToWrite;
bytesRemaining -= bytesToWrite;
if (!reportProgress(progressCb, CloneProgress::Phase::Cloning,
bytesTransferred, lengthBytes, startTime, perfFreq))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Clone canceled by user");
}
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// Smart clone — reads NTFS volume bitmap to skip free clusters.
// For non-NTFS volumes, falls back to raw clone.
//
// The NTFS bitmap approach: use FSCTL_GET_VOLUME_BITMAP on the source
// volume to get a bitmap of allocated clusters. Only copy clusters that
// are marked as in-use.
// ---------------------------------------------------------------------------
Result<void> DiskCloner::cloneSmart(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb)
{
// To get the volume bitmap, we need a volume handle, not a raw disk handle.
// The source offset tells us where the partition starts on disk.
// We need to figure out if this partition's volume is accessible.
// Try to open the volume by scanning for a volume whose extents
// match our source offset. Use the partition layout from the source disk.
auto layoutResult = src.getDriveLayout();
if (layoutResult.isError())
{
// Can't get layout — fall back to raw
return cloneRaw(src, srcSectorSize, dst, dstSectorSize,
srcOffsetBytes, lengthBytes, dstOffsetBytes,
bufferSize, progressCb);
}
// Find the partition that matches our source range
wchar_t volumeLetter = L'\0';
const auto& layout = layoutResult.value();
for (const auto& part : layout.partitions)
{
if (part.startingOffset == srcOffsetBytes &&
part.partitionLength == lengthBytes)
{
// Found a matching partition. Now we need its drive letter.
// Use FindFirstVolumeW/GetVolumePathNamesForVolumeNameW to
// map disk extents to volume letters. This is complex, so
// we take a simpler approach: iterate A-Z and check if the
// volume's disk extents match.
for (wchar_t letter = L'A'; letter <= L'Z'; ++letter)
{
wchar_t volPath[] = L"\\\\.\\X:";
volPath[4] = letter;
HANDLE hVol = ::CreateFileW(
volPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hVol == INVALID_HANDLE_VALUE)
continue;
// Query IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
uint8_t extBuf[256] = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
nullptr, 0, extBuf, sizeof(extBuf),
&bytesReturned, nullptr);
if (ok)
{
const auto* extents =
reinterpret_cast<const VOLUME_DISK_EXTENTS*>(extBuf);
if (extents->NumberOfDiskExtents >= 1)
{
const auto& ext = extents->Extents[0];
if (ext.DiskNumber == static_cast<DWORD>(src.diskId()) &&
static_cast<uint64_t>(ext.StartingOffset.QuadPart) == srcOffsetBytes)
{
volumeLetter = letter;
::CloseHandle(hVol);
break;
}
}
}
::CloseHandle(hVol);
}
break;
}
}
if (volumeLetter == L'\0')
{
// Could not find a volume for smart copy — fall back to raw
return cloneRaw(src, srcSectorSize, dst, dstSectorSize,
srcOffsetBytes, lengthBytes, dstOffsetBytes,
bufferSize, progressCb);
}
// Open the volume to get the allocation bitmap
wchar_t volPathBuf[] = L"\\\\.\\X:";
volPathBuf[4] = volumeLetter;
HANDLE hVolume = ::CreateFileW(
volPathBuf, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hVolume == INVALID_HANDLE_VALUE)
{
// Fall back to raw
return cloneRaw(src, srcSectorSize, dst, dstSectorSize,
srcOffsetBytes, lengthBytes, dstOffsetBytes,
bufferSize, progressCb);
}
// Query cluster size via FSCTL_GET_NTFS_VOLUME_DATA
NTFS_VOLUME_DATA_BUFFER ntfsData = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
hVolume, FSCTL_GET_NTFS_VOLUME_DATA,
nullptr, 0, &ntfsData, sizeof(ntfsData),
&bytesReturned, nullptr);
if (!ok)
{
::CloseHandle(hVolume);
// Not NTFS — fall back to raw
return cloneRaw(src, srcSectorSize, dst, dstSectorSize,
srcOffsetBytes, lengthBytes, dstOffsetBytes,
bufferSize, progressCb);
}
const uint32_t bytesPerCluster =
static_cast<uint32_t>(ntfsData.BytesPerCluster);
const int64_t totalClusters = ntfsData.TotalClusters.QuadPart;
// Allocate bitmap buffer. Each bit represents one cluster.
// Add some padding for the VOLUME_BITMAP_BUFFER header.
const size_t bitmapByteCount =
static_cast<size_t>((totalClusters + 7) / 8);
const size_t bitmapBufSize =
sizeof(VOLUME_BITMAP_BUFFER) + bitmapByteCount;
std::vector<uint8_t> bitmapBuf(bitmapBufSize, 0);
STARTING_LCN_INPUT_BUFFER startLcn = {};
startLcn.StartingLcn.QuadPart = 0;
ok = ::DeviceIoControl(
hVolume, FSCTL_GET_VOLUME_BITMAP,
&startLcn, sizeof(startLcn),
bitmapBuf.data(), static_cast<DWORD>(bitmapBuf.size()),
&bytesReturned, nullptr);
::CloseHandle(hVolume);
if (!ok)
{
// Bitmap query failed — fall back to raw
return cloneRaw(src, srcSectorSize, dst, dstSectorSize,
srcOffsetBytes, lengthBytes, dstOffsetBytes,
bufferSize, progressCb);
}
const auto* bitmap = reinterpret_cast<const VOLUME_BITMAP_BUFFER*>(bitmapBuf.data());
const uint8_t* bitmapData =
bitmapBuf.data() + offsetof(VOLUME_BITMAP_BUFFER, Buffer);
// Count allocated clusters for accurate progress reporting
uint64_t allocatedClusters = 0;
for (int64_t cluster = 0; cluster < totalClusters; ++cluster)
{
const size_t byteIdx = static_cast<size_t>(cluster / 8);
const uint8_t bitMask = static_cast<uint8_t>(1u << (cluster % 8));
if (bitmapData[byteIdx] & bitMask)
++allocatedClusters;
}
const uint64_t totalBytesToCopy = allocatedClusters * bytesPerCluster;
const uint32_t maxSectorSize = std::max(srcSectorSize, dstSectorSize);
const uint32_t alignedBufSize =
(bufferSize / maxSectorSize) * maxSectorSize;
if (alignedBufSize == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Buffer size too small for sector alignment");
}
// Number of clusters we can batch into one I/O operation
const uint32_t clustersPerChunk = alignedBufSize / bytesPerCluster;
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesTransferred = 0;
int64_t cluster = 0;
// Also need to zero out the destination for unallocated regions.
// We write zeros for ranges of unallocated clusters.
std::vector<uint8_t> zeroBuf(alignedBufSize, 0);
while (cluster < totalClusters)
{
if (isCancelRequested())
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Clone canceled by user");
}
// Find the next run of allocated clusters
const size_t byteIdx = static_cast<size_t>(cluster / 8);
const uint8_t bitMask = static_cast<uint8_t>(1u << (cluster % 8));
const bool isAllocated = (bitmapData[byteIdx] & bitMask) != 0;
if (!isAllocated)
{
// Write zeros to destination for this cluster
const uint64_t clusterDiskOffset =
srcOffsetBytes + static_cast<uint64_t>(cluster) * bytesPerCluster;
const uint64_t dstClusterOffset =
dstOffsetBytes + static_cast<uint64_t>(cluster) * bytesPerCluster;
// Find how many consecutive unallocated clusters we have
int64_t runLen = 0;
while (cluster + runLen < totalClusters &&
runLen < static_cast<int64_t>(clustersPerChunk))
{
const size_t bi = static_cast<size_t>((cluster + runLen) / 8);
const uint8_t bm =
static_cast<uint8_t>(1u << ((cluster + runLen) % 8));
if (bitmapData[bi] & bm)
break;
++runLen;
}
// Write zeros to destination for unallocated range
const uint64_t zeroBytes =
static_cast<uint64_t>(runLen) * bytesPerCluster;
uint64_t zeroRemaining = zeroBytes;
uint64_t zeroPos = dstClusterOffset;
while (zeroRemaining > 0)
{
const uint64_t writeChunk = std::min(
static_cast<uint64_t>(alignedBufSize), zeroRemaining);
const SectorOffset dstLba = zeroPos / dstSectorSize;
const SectorCount dstSectors =
static_cast<SectorCount>(writeChunk / dstSectorSize);
auto writeResult = dst.writeSectors(
dstLba, zeroBuf.data(), dstSectors, dstSectorSize);
if (writeResult.isError())
return writeResult.error();
zeroPos += writeChunk;
zeroRemaining -= writeChunk;
}
cluster += runLen;
continue;
}
// Find how many consecutive allocated clusters we have
int64_t runLen = 0;
while (cluster + runLen < totalClusters &&
runLen < static_cast<int64_t>(clustersPerChunk))
{
const size_t bi = static_cast<size_t>((cluster + runLen) / 8);
const uint8_t bm =
static_cast<uint8_t>(1u << ((cluster + runLen) % 8));
if (!(bitmapData[bi] & bm))
break;
++runLen;
}
// Copy this run of allocated clusters
const uint64_t runBytes = static_cast<uint64_t>(runLen) * bytesPerCluster;
const uint64_t srcClusterOffset =
srcOffsetBytes + static_cast<uint64_t>(cluster) * bytesPerCluster;
const uint64_t dstClusterOffset =
dstOffsetBytes + static_cast<uint64_t>(cluster) * bytesPerCluster;
// Read from source
const SectorOffset srcLba = srcClusterOffset / srcSectorSize;
const SectorCount srcSectors =
static_cast<SectorCount>(runBytes / srcSectorSize);
// May need to break into multiple reads if run is larger than buffer
uint64_t runRemaining = runBytes;
uint64_t srcRunPos = srcClusterOffset;
uint64_t dstRunPos = dstClusterOffset;
while (runRemaining > 0)
{
if (isCancelRequested())
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Clone canceled by user");
}
const uint64_t chunkBytes = std::min(
static_cast<uint64_t>(alignedBufSize), runRemaining);
const SectorOffset readLba = srcRunPos / srcSectorSize;
const SectorCount readSectors =
static_cast<SectorCount>(chunkBytes / srcSectorSize);
auto readResult = src.readSectors(readLba, readSectors, srcSectorSize);
if (readResult.isError())
return readResult.error();
const auto& data = readResult.value();
// Write to destination
const SectorOffset writeLba = dstRunPos / dstSectorSize;
const SectorCount writeSectors =
static_cast<SectorCount>(
((data.size() + dstSectorSize - 1) / dstSectorSize));
auto writeResult = dst.writeSectors(
writeLba, data.data(), writeSectors, dstSectorSize);
if (writeResult.isError())
return writeResult.error();
srcRunPos += chunkBytes;
dstRunPos += chunkBytes;
runRemaining -= chunkBytes;
bytesTransferred += chunkBytes;
if (!reportProgress(progressCb, CloneProgress::Phase::Cloning,
bytesTransferred, totalBytesToCopy,
startTime, perfFreq))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Clone canceled by user");
}
}
cluster += runLen;
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// Verification: read back both source and destination in chunks and
// compare SHA-256 hashes chunk by chunk.
// ---------------------------------------------------------------------------
Result<void> DiskCloner::verifyClone(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb)
{
const uint32_t maxSectorSize = std::max(srcSectorSize, dstSectorSize);
const uint32_t alignedBufSize =
(bufferSize / maxSectorSize) * maxSectorSize;
if (alignedBufSize == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Buffer size too small for sector alignment");
}
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesRemaining = lengthBytes;
uint64_t bytesVerified = 0;
uint64_t srcPos = srcOffsetBytes;
uint64_t dstPos = dstOffsetBytes;
while (bytesRemaining > 0)
{
if (isCancelRequested())
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Verification canceled by user");
}
const uint64_t chunkBytes = std::min(
static_cast<uint64_t>(alignedBufSize), bytesRemaining);
// Read source chunk
const SectorOffset srcLba = srcPos / srcSectorSize;
const SectorCount srcSectors =
static_cast<SectorCount>(
(chunkBytes + srcSectorSize - 1) / srcSectorSize);
auto srcRead = src.readSectors(srcLba, srcSectors, srcSectorSize);
if (srcRead.isError())
return srcRead.error();
// Read destination chunk
const SectorOffset dstLba = dstPos / dstSectorSize;
const SectorCount dstSectors =
static_cast<SectorCount>(
(chunkBytes + dstSectorSize - 1) / dstSectorSize);
auto dstRead = dst.readSectors(dstLba, dstSectors, dstSectorSize);
if (dstRead.isError())
return dstRead.error();
// Compare the relevant portion (up to chunkBytes)
const size_t compareLen = static_cast<size_t>(chunkBytes);
if (srcRead.value().size() < compareLen ||
dstRead.value().size() < compareLen)
{
return ErrorInfo::fromCode(ErrorCode::ImageChecksumMismatch,
"Verification read returned fewer bytes than expected");
}
if (std::memcmp(srcRead.value().data(),
dstRead.value().data(), compareLen) != 0)
{
std::ostringstream oss;
oss << "Verification mismatch at offset "
<< srcPos << " (chunk size " << compareLen << " bytes)";
return ErrorInfo::fromCode(ErrorCode::ImageChecksumMismatch,
oss.str());
}
srcPos += chunkBytes;
dstPos += chunkBytes;
bytesVerified += chunkBytes;
bytesRemaining -= chunkBytes;
if (!reportProgress(progressCb, CloneProgress::Phase::Verifying,
bytesVerified, lengthBytes, startTime, perfFreq))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Verification canceled by user");
}
}
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,156 @@
#pragma once
// DiskCloner — Sector-level disk and partition cloning engine.
// Supports raw (sector-by-sector) and smart (filesystem-aware) cloning,
// mismatched sector size handling, verification passes, and progress reporting.
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Progress info reported during cloning
struct CloneProgress
{
uint64_t bytesTransferred = 0;
uint64_t totalBytes = 0;
double speedBytesPerSec = 0.0;
double etaSeconds = 0.0;
double percentComplete = 0.0;
// Phase tracking
enum class Phase
{
Preparing,
Cloning,
Verifying,
Complete,
Failed,
};
Phase phase = Phase::Preparing;
};
// Callback: return false to cancel the operation
using CloneProgressCallback = std::function<bool(const CloneProgress& progress)>;
// Cloning mode
enum class CloneMode
{
// Sector-by-sector: copies every sector including free space.
// Works for any filesystem or raw data. Slowest but most faithful.
Raw,
// Smart: reads filesystem allocation bitmap and skips unallocated sectors.
// Only works for NTFS (via FSCTL_GET_VOLUME_BITMAP). Falls back to Raw
// for unsupported filesystems.
Smart,
};
// Configuration for a clone operation
struct CloneConfig
{
// Source disk (must be opened read-only or read-write)
DiskId sourceDiskId = -1;
// Destination disk (will be opened read-write)
DiskId destDiskId = -1;
// If set, clone only this byte range (for partition cloning).
// If both are 0, the entire source disk is cloned.
uint64_t sourceOffsetBytes = 0;
uint64_t sourceLengthBytes = 0; // 0 = entire disk
uint64_t destOffsetBytes = 0;
// Cloning strategy
CloneMode mode = CloneMode::Raw;
// Verify after cloning by reading back and comparing hashes
bool verifyAfterClone = true;
// I/O buffer size (default 4 MiB)
uint32_t bufferSize = 4 * 1024 * 1024;
// Volume letters on the destination disk to lock/dismount before writing.
// If empty, the cloner will attempt to auto-detect volumes.
std::vector<wchar_t> destVolumeLetters;
// If true, force clone even if destination is smaller than source
// (truncates data — dangerous, but useful for known-smaller content)
bool allowTruncation = false;
};
class DiskCloner
{
public:
DiskCloner() = default;
~DiskCloner() = default;
// Non-copyable
DiskCloner(const DiskCloner&) = delete;
DiskCloner& operator=(const DiskCloner&) = delete;
// Execute a clone operation. Blocks until complete or canceled.
Result<void> clone(const CloneConfig& config,
CloneProgressCallback progressCb = nullptr);
// Request cancellation (thread-safe)
void requestCancel();
// Check if a cancel has been requested
bool isCancelRequested() const;
private:
std::atomic<bool> m_cancelRequested{false};
// Internal: lock and dismount destination volumes
Result<std::vector<HANDLE>> lockDestinationVolumes(
const std::vector<wchar_t>& volumeLetters);
// Internal: unlock previously locked volumes
void unlockVolumes(std::vector<HANDLE>& lockedHandles);
// Internal: perform raw sector-by-sector copy
Result<void> cloneRaw(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb);
// Internal: perform smart copy (NTFS bitmap-aware)
Result<void> cloneSmart(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb);
// Internal: verification pass — read back both sides and compare
Result<void> verifyClone(
RawDiskHandle& src, uint32_t srcSectorSize,
RawDiskHandle& dst, uint32_t dstSectorSize,
uint64_t srcOffsetBytes, uint64_t lengthBytes, uint64_t dstOffsetBytes,
uint32_t bufferSize, CloneProgressCallback progressCb);
// Internal: report progress with speed and ETA calculation
bool reportProgress(CloneProgressCallback& cb,
CloneProgress::Phase phase,
uint64_t bytesTransferred, uint64_t totalBytes,
LARGE_INTEGER startTime, LARGE_INTEGER perfFreq);
};
} // namespace spw

View File

@@ -0,0 +1,823 @@
#include "ImageCreator.h"
#include "../common/Constants.h"
#include <algorithm>
#include <cstring>
#include <sstream>
// RtlCompressBuffer / RtlDecompressBuffer are exported by ntdll.dll.
// We load them at runtime to avoid a hard link-time dependency.
// These functions implement LZNT1 compression, which is the same algorithm
// NTFS uses internally for file compression.
typedef NTSTATUS(WINAPI* RtlCompressBufferFn)(
USHORT CompressionFormatAndEngine,
PUCHAR UncompressedBuffer,
ULONG UncompressedBufferSize,
PUCHAR CompressedBuffer,
ULONG CompressedBufferSize,
ULONG UncompressedChunkSize,
PULONG FinalCompressedSize,
PVOID WorkSpace);
typedef NTSTATUS(WINAPI* RtlGetCompressionWorkSpaceSizeFn)(
USHORT CompressionFormatAndEngine,
PULONG CompressBufferWorkSpaceSize,
PULONG CompressFragmentWorkSpaceSize);
// Compression format constants from ntifs.h
#ifndef COMPRESSION_FORMAT_LZNT1
#define COMPRESSION_FORMAT_LZNT1 0x0002
#endif
#ifndef COMPRESSION_ENGINE_STANDARD
#define COMPRESSION_ENGINE_STANDARD 0x0000
#endif
#ifndef COMPRESSION_ENGINE_MAXIMUM
#define COMPRESSION_ENGINE_MAXIMUM 0x0100
#endif
namespace spw
{
// ---------------------------------------------------------------------------
// Helper: Win32 error
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
// Cancel support
// ---------------------------------------------------------------------------
void ImageCreator::requestCancel()
{
m_cancelRequested.store(true, std::memory_order_release);
}
bool ImageCreator::isCancelRequested() const
{
return m_cancelRequested.load(std::memory_order_acquire);
}
// ---------------------------------------------------------------------------
// Check if a buffer is entirely zeros (fast scan with 64-bit words)
// ---------------------------------------------------------------------------
bool ImageCreator::isAllZeros(const uint8_t* data, size_t length)
{
// Check 8 bytes at a time for speed
const size_t wordCount = length / 8;
const uint64_t* wordPtr = reinterpret_cast<const uint64_t*>(data);
for (size_t i = 0; i < wordCount; ++i)
{
if (wordPtr[i] != 0)
return false;
}
// Check remaining bytes
for (size_t i = wordCount * 8; i < length; ++i)
{
if (data[i] != 0)
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// Populate SPW header with disk metadata
// ---------------------------------------------------------------------------
void ImageCreator::populateDiskMetadata(
SpwImageHeader& header,
const RawDiskHandle& disk,
const DiskGeometryInfo& geom)
{
// Zero out metadata fields
std::memset(header.diskModel, 0, sizeof(header.diskModel));
std::memset(header.diskSerial, 0, sizeof(header.diskSerial));
header.sourceDiskSize = geom.totalBytes;
header.sourceSectorSize = geom.bytesPerSector;
// Try to get the partition table type from the drive layout
auto layoutResult = disk.getDriveLayout();
if (layoutResult.isOk())
{
header.partitionTableType =
static_cast<uint32_t>(layoutResult.value().partitionStyle);
}
// Disk model and serial would ideally come from STORAGE_DEVICE_DESCRIPTOR
// via IOCTL_STORAGE_QUERY_PROPERTY. We query it here.
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageDeviceProperty;
query.QueryType = PropertyStandardQuery;
uint8_t descBuf[1024] = {};
DWORD bytesReturned = 0;
BOOL ok = ::DeviceIoControl(
disk.nativeHandle(),
IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
descBuf, sizeof(descBuf),
&bytesReturned, nullptr);
if (ok && bytesReturned >= sizeof(STORAGE_DEVICE_DESCRIPTOR))
{
const auto* desc =
reinterpret_cast<const STORAGE_DEVICE_DESCRIPTOR*>(descBuf);
// VendorId and ProductId are offsets into the buffer
if (desc->ProductIdOffset != 0 &&
desc->ProductIdOffset < bytesReturned)
{
const char* productId =
reinterpret_cast<const char*>(descBuf) + desc->ProductIdOffset;
// strncpy is safe here because we zero-initialized diskModel
strncpy(header.diskModel, productId, sizeof(header.diskModel) - 1);
}
if (desc->SerialNumberOffset != 0 &&
desc->SerialNumberOffset < bytesReturned)
{
const char* serial =
reinterpret_cast<const char*>(descBuf) + desc->SerialNumberOffset;
strncpy(header.diskSerial, serial, sizeof(header.diskSerial) - 1);
}
}
}
// ---------------------------------------------------------------------------
// LZNT1 compression via ntdll.dll
// ---------------------------------------------------------------------------
Result<std::vector<uint8_t>> ImageCreator::compressLZNT1(
const uint8_t* uncompressedData, size_t uncompressedSize)
{
// Load ntdll.dll functions on first call
static HMODULE hNtdll = ::GetModuleHandleW(L"ntdll.dll");
static auto pRtlCompressBuffer =
reinterpret_cast<RtlCompressBufferFn>(
::GetProcAddress(hNtdll, "RtlCompressBuffer"));
static auto pRtlGetCompressionWorkSpaceSize =
reinterpret_cast<RtlGetCompressionWorkSpaceSizeFn>(
::GetProcAddress(hNtdll, "RtlGetCompressionWorkSpaceSize"));
if (!pRtlCompressBuffer || !pRtlGetCompressionWorkSpaceSize)
{
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"LZNT1 compression functions not available in ntdll.dll");
}
const USHORT compressionFormat =
COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_STANDARD;
// Get workspace size
ULONG workSpaceSize = 0;
ULONG fragmentWorkSpaceSize = 0;
NTSTATUS status = pRtlGetCompressionWorkSpaceSize(
compressionFormat, &workSpaceSize, &fragmentWorkSpaceSize);
if (status != 0) // STATUS_SUCCESS = 0
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"RtlGetCompressionWorkSpaceSize failed");
}
std::vector<uint8_t> workSpace(workSpaceSize);
// Worst case: compressed data could be slightly larger than input.
// Allocate input size + 10% + 256 bytes for safety.
const size_t outputBufSize = uncompressedSize + (uncompressedSize / 10) + 256;
std::vector<uint8_t> compressedBuffer(outputBufSize);
ULONG finalCompressedSize = 0;
status = pRtlCompressBuffer(
compressionFormat,
const_cast<PUCHAR>(uncompressedData),
static_cast<ULONG>(uncompressedSize),
compressedBuffer.data(),
static_cast<ULONG>(compressedBuffer.size()),
4096, // Uncompressed chunk size parameter for LZNT1
&finalCompressedSize,
workSpace.data());
if (status != 0)
{
return ErrorInfo::fromCode(ErrorCode::Unknown,
"RtlCompressBuffer (LZNT1) failed");
}
compressedBuffer.resize(finalCompressedSize);
return compressedBuffer;
}
// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------
Result<void> ImageCreator::createImage(
const ImageCreateConfig& config,
ImageCreateProgressCallback progressCb)
{
m_cancelRequested.store(false, std::memory_order_release);
if (config.sourceDiskId < 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Invalid source disk ID");
}
if (config.outputFilePath.empty())
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Output file path is empty");
}
// Open source disk
auto srcResult = RawDiskHandle::open(config.sourceDiskId, DiskAccessMode::ReadOnly);
if (srcResult.isError())
return srcResult.error();
auto& srcDisk = srcResult.value();
auto geomResult = srcDisk.getGeometry();
if (geomResult.isError())
return geomResult.error();
const auto& geom = geomResult.value();
const uint32_t sectorSize = geom.bytesPerSector;
// Determine range to image
uint64_t srcOffset = config.sourceOffsetBytes;
uint64_t length = config.sourceLengthBytes;
if (length == 0)
{
if (srcOffset > geom.totalBytes)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Source offset exceeds disk size");
}
length = geom.totalBytes - srcOffset;
}
if (srcOffset + length > geom.totalBytes)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Source range exceeds disk size");
}
// Ensure offset is sector-aligned
if (srcOffset % sectorSize != 0)
{
return ErrorInfo::fromCode(ErrorCode::AlignmentError,
"Source offset must be sector-aligned");
}
if (config.format == ImageFormat::Raw)
{
return createRawImage(srcDisk, sectorSize, srcOffset, length,
config.outputFilePath, config.chunkSize,
progressCb);
}
else
{
return createSpwImage(srcDisk, sectorSize, srcOffset, length,
config, progressCb);
}
}
// ---------------------------------------------------------------------------
// Raw image: dd-style byte copy to file
// ---------------------------------------------------------------------------
Result<void> ImageCreator::createRawImage(
RawDiskHandle& srcDisk, uint32_t sectorSize,
uint64_t srcOffset, uint64_t length,
const std::wstring& outputPath, uint32_t chunkSize,
ImageCreateProgressCallback progressCb)
{
// Create output file
HANDLE hFile = ::CreateFileW(
outputPath.c_str(),
GENERIC_WRITE,
0, // No sharing during image creation
nullptr,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::FileCreateFailed,
"Failed to create output image file");
}
// Align chunk size to sector boundary
const uint32_t alignedChunk = (chunkSize / sectorSize) * sectorSize;
if (alignedChunk == 0)
{
::CloseHandle(hFile);
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Chunk size too small for sector alignment");
}
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesRemaining = length;
uint64_t bytesProcessed = 0;
uint64_t srcPos = srcOffset;
while (bytesRemaining > 0)
{
if (isCancelRequested())
{
::CloseHandle(hFile);
::DeleteFileW(outputPath.c_str());
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Image creation canceled");
}
const uint64_t readBytes = std::min(
static_cast<uint64_t>(alignedChunk), bytesRemaining);
const SectorOffset lba = srcPos / sectorSize;
const SectorCount sectors =
static_cast<SectorCount>((readBytes + sectorSize - 1) / sectorSize);
auto readResult = srcDisk.readSectors(lba, sectors, sectorSize);
if (readResult.isError())
{
::CloseHandle(hFile);
::DeleteFileW(outputPath.c_str());
return readResult.error();
}
const auto& data = readResult.value();
// Write only the bytes we need (may be less than sector-aligned read)
const DWORD writeSize = static_cast<DWORD>(
std::min(static_cast<uint64_t>(data.size()), readBytes));
DWORD bytesWritten = 0;
BOOL ok = ::WriteFile(hFile, data.data(), writeSize, &bytesWritten, nullptr);
if (!ok || bytesWritten != writeSize)
{
::CloseHandle(hFile);
::DeleteFileW(outputPath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write to image file");
}
srcPos += readBytes;
bytesProcessed += readBytes;
bytesRemaining -= readBytes;
if (progressCb)
{
ImageCreateProgress progress;
progress.bytesProcessed = bytesProcessed;
progress.totalBytes = length;
progress.percentComplete =
static_cast<double>(bytesProcessed) / static_cast<double>(length) * 100.0;
LARGE_INTEGER now;
::QueryPerformanceCounter(&now);
const double elapsed =
static_cast<double>(now.QuadPart - startTime.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
if (elapsed > 0.0)
{
progress.speedBytesPerSec =
static_cast<double>(bytesProcessed) / elapsed;
if (progress.speedBytesPerSec > 0.0 && bytesProcessed < length)
{
progress.etaSeconds =
static_cast<double>(length - bytesProcessed) /
progress.speedBytesPerSec;
}
}
if (!progressCb(progress))
{
::CloseHandle(hFile);
::DeleteFileW(outputPath.c_str());
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Image creation canceled");
}
}
}
::CloseHandle(hFile);
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// SPW compressed image creation.
// File layout:
// [SpwImageHeader] — fixed size
// [SpwChunkEntry * chunkCount] — chunk table
// [compressed chunk data...] — variable-size compressed blocks
//
// We write the header and chunk table as placeholders first, then write
// compressed chunks sequentially, recording offsets in the chunk table.
// Finally, we seek back and overwrite the header (with SHA-256) and
// chunk table with the actual values.
// ---------------------------------------------------------------------------
Result<void> ImageCreator::createSpwImage(
RawDiskHandle& srcDisk, uint32_t sectorSize,
uint64_t srcOffset, uint64_t length,
const ImageCreateConfig& config,
ImageCreateProgressCallback progressCb)
{
// Calculate chunk count
const uint32_t chunkSize = config.chunkSize;
const uint32_t chunkCount = static_cast<uint32_t>(
(length + chunkSize - 1) / chunkSize);
if (chunkCount == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Nothing to image (zero length)");
}
// Create output file
HANDLE hFile = ::CreateFileW(
config.outputFilePath.c_str(),
GENERIC_READ | GENERIC_WRITE, // Need read for seek-back
0, nullptr, CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::FileCreateFailed,
"Failed to create SPW image file");
}
// Get disk geometry for metadata
auto geomResult = srcDisk.getGeometry();
if (geomResult.isError())
{
::CloseHandle(hFile);
return geomResult.error();
}
// Build header
SpwImageHeader header = {};
std::memcpy(header.magic, SPW_IMAGE_MAGIC, 8);
header.headerSize = sizeof(SpwImageHeader);
header.version = 1;
header.imageDataSize = length;
header.chunkSize = chunkSize;
header.chunkCount = chunkCount;
header.compressionType = config.enableCompression ? 1 : 0; // 1 = LZNT1
header.compressionLevel = 0;
header.flags = config.enableSparse ? 1u : 0u;
// Set creation timestamp
FILETIME ft;
::GetSystemTimeAsFileTime(&ft);
header.creationTimestamp =
(static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
populateDiskMetadata(header, srcDisk, geomResult.value());
// Allocate chunk table (will be filled in as we process chunks)
std::vector<SpwChunkEntry> chunkTable(chunkCount);
std::memset(chunkTable.data(), 0,
chunkCount * sizeof(SpwChunkEntry));
// Write placeholder header
const uint64_t headerOffset = 0;
const uint64_t chunkTableOffset = sizeof(SpwImageHeader);
const uint64_t chunkTableSize =
static_cast<uint64_t>(chunkCount) * sizeof(SpwChunkEntry);
const uint64_t dataStartOffset = chunkTableOffset + chunkTableSize;
DWORD bytesWritten = 0;
// Write header placeholder
BOOL ok = ::WriteFile(hFile, &header, sizeof(header), &bytesWritten, nullptr);
if (!ok || bytesWritten != sizeof(header))
{
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write SPW header");
}
// Write chunk table placeholder
ok = ::WriteFile(hFile, chunkTable.data(),
static_cast<DWORD>(chunkTableSize), &bytesWritten, nullptr);
if (!ok || bytesWritten != static_cast<DWORD>(chunkTableSize))
{
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write SPW chunk table placeholder");
}
// Initialize SHA-256 hasher for the uncompressed data
// We'll compute it chunk by chunk
// Using BCrypt directly for incremental hashing
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
NTSTATUS ntStatus = ::BCryptOpenAlgorithmProvider(
&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0);
if (!BCRYPT_SUCCESS(ntStatus))
{
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return ErrorInfo::fromCode(ErrorCode::Unknown,
"Failed to open SHA-256 algorithm provider");
}
DWORD hashObjectSize = 0;
DWORD cbData = 0;
::BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&hashObjectSize),
sizeof(hashObjectSize), &cbData, 0);
std::vector<uint8_t> hashObject(hashObjectSize);
ntStatus = ::BCryptCreateHash(
hAlg, &hHash, hashObject.data(), hashObjectSize,
nullptr, 0, 0);
if (!BCRYPT_SUCCESS(ntStatus))
{
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return ErrorInfo::fromCode(ErrorCode::Unknown,
"Failed to create SHA-256 hash object");
}
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t currentFileOffset = dataStartOffset;
uint64_t bytesProcessed = 0;
uint64_t totalCompressedBytes = 0;
uint32_t sparseCount = 0;
const uint32_t sectorsPerChunk = chunkSize / sectorSize;
for (uint32_t chunkIdx = 0; chunkIdx < chunkCount; ++chunkIdx)
{
if (isCancelRequested())
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Image creation canceled");
}
// Calculate how many bytes to read for this chunk
const uint64_t chunkOffset =
srcOffset + static_cast<uint64_t>(chunkIdx) * chunkSize;
const uint64_t remaining = length - bytesProcessed;
const uint32_t thisChunkSize = static_cast<uint32_t>(
std::min(static_cast<uint64_t>(chunkSize), remaining));
// Read from disk
const SectorOffset lba = chunkOffset / sectorSize;
const SectorCount sectors = static_cast<SectorCount>(
(thisChunkSize + sectorSize - 1) / sectorSize);
auto readResult = srcDisk.readSectors(lba, sectors, sectorSize);
if (readResult.isError())
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return readResult.error();
}
const auto& rawData = readResult.value();
const size_t rawSize = std::min(
static_cast<size_t>(thisChunkSize), rawData.size());
// Update SHA-256 with uncompressed data
::BCryptHashData(hHash, const_cast<PUCHAR>(rawData.data()),
static_cast<ULONG>(rawSize), 0);
// CRC32 for this chunk
const uint32_t chunkCrc = Checksums::crc32Buffer(rawData.data(), rawSize);
// Check for sparse (all-zero) chunks
SpwChunkEntry& entry = chunkTable[chunkIdx];
entry.uncompressedSize = static_cast<uint32_t>(rawSize);
entry.crc32 = chunkCrc;
if (config.enableSparse && isAllZeros(rawData.data(), rawSize))
{
// Sparse chunk — don't store data
entry.fileOffset = 0;
entry.compressedSize = 0;
entry.flags = 1; // Sparse flag
++sparseCount;
}
else if (config.enableCompression)
{
// Compress with LZNT1
auto compResult = compressLZNT1(rawData.data(), rawSize);
if (compResult.isError())
{
// Compression failed — store uncompressed
entry.fileOffset = currentFileOffset;
entry.compressedSize = static_cast<uint32_t>(rawSize);
entry.flags = 0;
ok = ::WriteFile(hFile, rawData.data(),
static_cast<DWORD>(rawSize),
&bytesWritten, nullptr);
if (!ok)
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write uncompressed chunk");
}
currentFileOffset += rawSize;
totalCompressedBytes += rawSize;
}
else
{
const auto& compressed = compResult.value();
// Only use compressed version if it's actually smaller
if (compressed.size() < rawSize)
{
entry.fileOffset = currentFileOffset;
entry.compressedSize =
static_cast<uint32_t>(compressed.size());
entry.flags = 0;
ok = ::WriteFile(hFile, compressed.data(),
static_cast<DWORD>(compressed.size()),
&bytesWritten, nullptr);
if (!ok)
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write compressed chunk");
}
currentFileOffset += compressed.size();
totalCompressedBytes += compressed.size();
}
else
{
// Compressed is larger — store uncompressed
entry.fileOffset = currentFileOffset;
entry.compressedSize = static_cast<uint32_t>(rawSize);
entry.flags = 0;
ok = ::WriteFile(hFile, rawData.data(),
static_cast<DWORD>(rawSize),
&bytesWritten, nullptr);
if (!ok)
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write uncompressed chunk");
}
currentFileOffset += rawSize;
totalCompressedBytes += rawSize;
}
}
}
else
{
// No compression — store raw
entry.fileOffset = currentFileOffset;
entry.compressedSize = static_cast<uint32_t>(rawSize);
entry.flags = 0;
ok = ::WriteFile(hFile, rawData.data(),
static_cast<DWORD>(rawSize),
&bytesWritten, nullptr);
if (!ok)
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to write raw chunk");
}
currentFileOffset += rawSize;
totalCompressedBytes += rawSize;
}
bytesProcessed += rawSize;
// Report progress
if (progressCb)
{
ImageCreateProgress progress;
progress.bytesProcessed = bytesProcessed;
progress.totalBytes = length;
progress.compressedBytes = totalCompressedBytes;
progress.percentComplete =
static_cast<double>(bytesProcessed) /
static_cast<double>(length) * 100.0;
if (totalCompressedBytes > 0)
{
progress.compressionRatio =
static_cast<double>(bytesProcessed) /
static_cast<double>(totalCompressedBytes);
}
LARGE_INTEGER now;
::QueryPerformanceCounter(&now);
const double elapsed =
static_cast<double>(now.QuadPart - startTime.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
if (elapsed > 0.0)
{
progress.speedBytesPerSec =
static_cast<double>(bytesProcessed) / elapsed;
if (progress.speedBytesPerSec > 0.0 && bytesProcessed < length)
{
progress.etaSeconds =
static_cast<double>(length - bytesProcessed) /
progress.speedBytesPerSec;
}
}
if (!progressCb(progress))
{
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
::CloseHandle(hFile);
::DeleteFileW(config.outputFilePath.c_str());
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Image creation canceled");
}
}
}
// Finalize SHA-256
::BCryptFinishHash(hHash, header.sha256, 32, 0);
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
// Update header with final values
header.sparseChunkCount = sparseCount;
// Seek back to beginning and rewrite header with SHA-256 and final metadata
LARGE_INTEGER seekPos;
seekPos.QuadPart = 0;
if (!::SetFilePointerEx(hFile, seekPos, nullptr, FILE_BEGIN))
{
::CloseHandle(hFile);
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to seek to header position");
}
ok = ::WriteFile(hFile, &header, sizeof(header), &bytesWritten, nullptr);
if (!ok || bytesWritten != sizeof(header))
{
::CloseHandle(hFile);
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to rewrite SPW header");
}
// Rewrite chunk table with actual offsets and sizes
ok = ::WriteFile(hFile, chunkTable.data(),
static_cast<DWORD>(chunkTableSize),
&bytesWritten, nullptr);
if (!ok || bytesWritten != static_cast<DWORD>(chunkTableSize))
{
::CloseHandle(hFile);
return makeWin32Error(ErrorCode::ImageWriteError,
"Failed to rewrite SPW chunk table");
}
::CloseHandle(hFile);
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,177 @@
#pragma once
// ImageCreator — Creates disk/partition images in raw (.img) or compressed SPW format.
// SPW format: [SPWIMG01 magic][header][chunk table][LZNT1-compressed 4MB chunks]
// Each chunk is independently compressed for random-access decompression.
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include "Checksums.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Image format to create
enum class ImageFormat
{
// Raw byte-for-byte copy (.img)
Raw,
// Compressed SPW format with metadata header (.spw)
SPW,
};
// Progress info for image creation
struct ImageCreateProgress
{
uint64_t bytesProcessed = 0;
uint64_t totalBytes = 0;
uint64_t compressedBytes = 0; // For SPW format: total compressed output so far
double speedBytesPerSec = 0.0;
double etaSeconds = 0.0;
double percentComplete = 0.0;
double compressionRatio = 0.0; // e.g. 2.5 means 2.5:1 compression
};
using ImageCreateProgressCallback =
std::function<bool(const ImageCreateProgress& progress)>;
// ---------------------------------------------------------------------------
// SPW image file on-disk structures.
// All multi-byte fields are little-endian.
// ---------------------------------------------------------------------------
#pragma pack(push, 1)
// File header — immediately follows the 8-byte magic
struct SpwImageHeader
{
uint8_t magic[8]; // "SPWIMG01"
uint32_t headerSize; // Size of this header struct in bytes
uint32_t version; // Format version (currently 1)
// Source disk metadata
char diskModel[64]; // UTF-8, null-terminated
char diskSerial[64]; // UTF-8, null-terminated
uint64_t sourceDiskSize; // Total bytes on source disk
uint32_t sourceSectorSize; // Bytes per sector (e.g. 512, 4096)
uint32_t partitionTableType;// PartitionTableType enum value
// Image metadata
uint64_t imageDataSize; // Total uncompressed data size
uint32_t chunkSize; // Uncompressed chunk size (typically 4 MiB)
uint32_t chunkCount; // Number of chunks in the image
uint32_t compressionType; // 0=none, 1=LZNT1
uint32_t compressionLevel; // 0 for LZNT1 (no level control)
// Timestamps
uint64_t creationTimestamp; // Windows FILETIME (100-ns intervals since 1601)
// Integrity
uint8_t sha256[32]; // SHA-256 of uncompressed data
// Sparse support: if non-zero, a bitmap follows the chunk table
// indicating which chunks contain only zeros (skipped in file).
uint32_t sparseChunkCount; // Number of chunks that are all-zeros (not stored)
uint32_t flags; // Bit 0: sparse image
uint8_t reserved[128]; // Future expansion
};
// Chunk table entry — one per chunk, follows the header
struct SpwChunkEntry
{
uint64_t fileOffset; // Byte offset in the image file where compressed data starts
uint32_t compressedSize; // Size of compressed data (0 if chunk is sparse/all-zeros)
uint32_t uncompressedSize; // Original uncompressed size
uint32_t crc32; // CRC32 of uncompressed data for quick validation
uint32_t flags; // Bit 0: sparse (all zeros, not stored)
};
#pragma pack(pop)
static_assert(sizeof(SpwImageHeader) <= 512, "SPW header must fit in one sector");
// Configuration for image creation
struct ImageCreateConfig
{
// Source
DiskId sourceDiskId = -1;
uint64_t sourceOffsetBytes = 0; // Partition offset (0 for whole disk)
uint64_t sourceLengthBytes = 0; // 0 = entire disk
// Output
std::wstring outputFilePath;
ImageFormat format = ImageFormat::Raw;
// SPW options
bool enableCompression = true; // Use LZNT1 compression
bool enableSparse = true; // Skip all-zero chunks
// I/O
uint32_t chunkSize = 4 * 1024 * 1024; // 4 MiB
};
class ImageCreator
{
public:
ImageCreator() = default;
~ImageCreator() = default;
ImageCreator(const ImageCreator&) = delete;
ImageCreator& operator=(const ImageCreator&) = delete;
// Create an image. Blocks until complete or canceled.
Result<void> createImage(const ImageCreateConfig& config,
ImageCreateProgressCallback progressCb = nullptr);
void requestCancel();
bool isCancelRequested() const;
private:
std::atomic<bool> m_cancelRequested{false};
// Create a raw .img file (dd-style byte copy)
Result<void> createRawImage(
RawDiskHandle& srcDisk, uint32_t sectorSize,
uint64_t srcOffset, uint64_t length,
const std::wstring& outputPath, uint32_t chunkSize,
ImageCreateProgressCallback progressCb);
// Create a compressed SPW image
Result<void> createSpwImage(
RawDiskHandle& srcDisk, uint32_t sectorSize,
uint64_t srcOffset, uint64_t length,
const ImageCreateConfig& config,
ImageCreateProgressCallback progressCb);
// Compress a buffer using LZNT1 via RtlCompressBuffer
Result<std::vector<uint8_t>> compressLZNT1(
const uint8_t* uncompressedData, size_t uncompressedSize);
// Check if a buffer is entirely zeros
static bool isAllZeros(const uint8_t* data, size_t length);
// Build disk metadata strings for the SPW header
static void populateDiskMetadata(
SpwImageHeader& header,
const RawDiskHandle& disk,
const DiskGeometryInfo& geom);
};
} // namespace spw

View File

@@ -0,0 +1,780 @@
#include "ImageRestorer.h"
#include "Checksums.h"
#include "../common/Constants.h"
#include <algorithm>
#include <cstring>
#include <sstream>
// RtlDecompressBuffer from ntdll.dll — LZNT1 decompression counterpart
typedef NTSTATUS(WINAPI* RtlDecompressBufferFn)(
USHORT CompressionFormat,
PUCHAR UncompressedBuffer,
ULONG UncompressedBufferSize,
PUCHAR CompressedBuffer,
ULONG CompressedBufferSize,
PULONG FinalUncompressedSize);
#ifndef COMPRESSION_FORMAT_LZNT1
#define COMPRESSION_FORMAT_LZNT1 0x0002
#endif
namespace spw
{
// ---------------------------------------------------------------------------
static ErrorInfo makeWin32Error(ErrorCode code, const std::string& context)
{
const DWORD lastErr = ::GetLastError();
std::ostringstream oss;
oss << context << " (Win32 error " << lastErr << ")";
return ErrorInfo::fromWin32(code, lastErr, oss.str());
}
// ---------------------------------------------------------------------------
void ImageRestorer::requestCancel()
{
m_cancelRequested.store(true, std::memory_order_release);
}
bool ImageRestorer::isCancelRequested() const
{
return m_cancelRequested.load(std::memory_order_acquire);
}
// ---------------------------------------------------------------------------
// LZNT1 decompression via ntdll.dll
// ---------------------------------------------------------------------------
Result<std::vector<uint8_t>> ImageRestorer::decompressLZNT1(
const uint8_t* compressedData, size_t compressedSize,
size_t uncompressedSize)
{
static HMODULE hNtdll = ::GetModuleHandleW(L"ntdll.dll");
static auto pRtlDecompressBuffer =
reinterpret_cast<RtlDecompressBufferFn>(
::GetProcAddress(hNtdll, "RtlDecompressBuffer"));
if (!pRtlDecompressBuffer)
{
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"RtlDecompressBuffer not available in ntdll.dll");
}
std::vector<uint8_t> output(uncompressedSize);
ULONG finalSize = 0;
NTSTATUS status = pRtlDecompressBuffer(
COMPRESSION_FORMAT_LZNT1,
output.data(),
static_cast<ULONG>(uncompressedSize),
const_cast<PUCHAR>(compressedData),
static_cast<ULONG>(compressedSize),
&finalSize);
if (status != 0) // STATUS_SUCCESS = 0
{
std::ostringstream oss;
oss << "RtlDecompressBuffer failed (NTSTATUS 0x"
<< std::hex << status << ")";
return ErrorInfo::fromCode(ErrorCode::Unknown, oss.str());
}
output.resize(finalSize);
return output;
}
// ---------------------------------------------------------------------------
// Lock/dismount destination volumes
// ---------------------------------------------------------------------------
Result<std::vector<HANDLE>> ImageRestorer::lockDestinationVolumes(
const std::vector<wchar_t>& volumeLetters)
{
std::vector<HANDLE> lockedHandles;
for (wchar_t letter : volumeLetters)
{
RawDiskHandle::dismountVolume(letter);
auto lockResult = RawDiskHandle::lockVolume(letter);
if (lockResult.isError())
{
unlockVolumes(lockedHandles);
return ErrorInfo::fromCode(ErrorCode::DiskLockFailed,
std::string("Failed to lock volume ") +
static_cast<char>(letter) + ":");
}
lockedHandles.push_back(lockResult.value());
}
return lockedHandles;
}
void ImageRestorer::unlockVolumes(std::vector<HANDLE>& handles)
{
for (HANDLE h : handles)
{
if (h != INVALID_HANDLE_VALUE)
{
RawDiskHandle::unlockVolume(h);
::CloseHandle(h);
}
}
handles.clear();
}
// ---------------------------------------------------------------------------
// Detect image format by reading the first 8 bytes
// ---------------------------------------------------------------------------
Result<ImageFormat> ImageRestorer::detectFormat(const std::wstring& filePath)
{
HANDLE hFile = ::CreateFileW(
filePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::FileNotFound,
"Failed to open image file");
}
uint8_t magic[8] = {};
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, magic, 8, &bytesRead, nullptr);
::CloseHandle(hFile);
if (!ok || bytesRead < 8)
{
// Too small to have SPW header — treat as raw
return ImageFormat::Raw;
}
if (std::memcmp(magic, SPW_IMAGE_MAGIC, 8) == 0)
{
return ImageFormat::SPW;
}
return ImageFormat::Raw;
}
// ---------------------------------------------------------------------------
// Inspect an SPW image and return its metadata
// ---------------------------------------------------------------------------
Result<SpwImageInfo> ImageRestorer::inspectImage(const std::wstring& filePath)
{
auto fmtResult = detectFormat(filePath);
if (fmtResult.isError())
return fmtResult.error();
if (fmtResult.value() != ImageFormat::SPW)
{
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"File is not in SPW format");
}
HANDLE hFile = ::CreateFileW(
filePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::FileNotFound,
"Failed to open image file for inspection");
}
SpwImageHeader header = {};
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, &header, sizeof(header), &bytesRead, nullptr);
::CloseHandle(hFile);
if (!ok || bytesRead < sizeof(header))
{
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"Failed to read SPW header");
}
if (header.version != 1)
{
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"Unsupported SPW image version");
}
SpwImageInfo info;
info.diskModel = std::string(header.diskModel,
strnlen(header.diskModel, sizeof(header.diskModel)));
info.diskSerial = std::string(header.diskSerial,
strnlen(header.diskSerial, sizeof(header.diskSerial)));
info.sourceDiskSize = header.sourceDiskSize;
info.sourceSectorSize = header.sourceSectorSize;
info.partitionTableType =
static_cast<PartitionTableType>(header.partitionTableType);
info.imageDataSize = header.imageDataSize;
info.chunkCount = header.chunkCount;
info.sparseChunkCount = header.sparseChunkCount;
info.isCompressed = (header.compressionType != 0);
std::memcpy(info.sha256.data(), header.sha256, 32);
info.creationTimestamp = header.creationTimestamp;
return info;
}
// ---------------------------------------------------------------------------
// Main restore entry point
// ---------------------------------------------------------------------------
Result<void> ImageRestorer::restoreImage(
const ImageRestoreConfig& config,
ImageRestoreProgressCallback progressCb)
{
m_cancelRequested.store(false, std::memory_order_release);
if (config.inputFilePath.empty())
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Input file path is empty");
}
if (config.destDiskId < 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Invalid destination disk ID");
}
// Detect format
auto fmtResult = detectFormat(config.inputFilePath);
if (fmtResult.isError())
return fmtResult.error();
const ImageFormat format = fmtResult.value();
// Open image file
HANDLE hFile = ::CreateFileW(
config.inputFilePath.c_str(),
GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
return makeWin32Error(ErrorCode::FileNotFound,
"Failed to open image file");
}
// Get file size
LARGE_INTEGER fileSize;
if (!::GetFileSizeEx(hFile, &fileSize))
{
::CloseHandle(hFile);
return makeWin32Error(ErrorCode::ImageReadError,
"Failed to get image file size");
}
// Open destination disk
auto dstResult = RawDiskHandle::open(config.destDiskId, DiskAccessMode::ReadWrite);
if (dstResult.isError())
{
::CloseHandle(hFile);
return dstResult.error();
}
auto& dstDisk = dstResult.value();
auto geomResult = dstDisk.getGeometry();
if (geomResult.isError())
{
::CloseHandle(hFile);
return geomResult.error();
}
const uint32_t dstSectorSize = geomResult.value().bytesPerSector;
// Lock destination volumes
std::vector<HANDLE> lockedVolumes;
if (!config.destVolumeLetters.empty())
{
auto lockResult = lockDestinationVolumes(config.destVolumeLetters);
if (lockResult.isError())
{
::CloseHandle(hFile);
return lockResult.error();
}
lockedVolumes = std::move(lockResult.value());
}
Result<void> result = Result<void>::ok();
if (format == ImageFormat::Raw)
{
result = restoreRawImage(
hFile, static_cast<uint64_t>(fileSize.QuadPart),
dstDisk, dstSectorSize, config.destOffsetBytes,
config.bufferSize, progressCb);
}
else
{
// Read SPW header
SpwImageHeader header = {};
DWORD bytesRead = 0;
// Seek to beginning (should already be there, but be explicit)
LARGE_INTEGER seekPos;
seekPos.QuadPart = 0;
::SetFilePointerEx(hFile, seekPos, nullptr, FILE_BEGIN);
BOOL ok = ::ReadFile(hFile, &header, sizeof(header), &bytesRead, nullptr);
if (!ok || bytesRead < sizeof(header))
{
::CloseHandle(hFile);
unlockVolumes(lockedVolumes);
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"Failed to read SPW header");
}
if (std::memcmp(header.magic, SPW_IMAGE_MAGIC, 8) != 0 ||
header.version != 1)
{
::CloseHandle(hFile);
unlockVolumes(lockedVolumes);
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"Invalid SPW image header");
}
// Validate image fits on destination
const uint64_t dstTotalBytes = geomResult.value().totalBytes;
if (config.destOffsetBytes + header.imageDataSize > dstTotalBytes)
{
::CloseHandle(hFile);
unlockVolumes(lockedVolumes);
return ErrorInfo::fromCode(ErrorCode::InsufficientDiskSpace,
"Image is larger than destination disk");
}
// Read chunk table
const uint32_t chunkCount = header.chunkCount;
std::vector<SpwChunkEntry> chunkTable(chunkCount);
ok = ::ReadFile(hFile, chunkTable.data(),
static_cast<DWORD>(chunkCount * sizeof(SpwChunkEntry)),
&bytesRead, nullptr);
if (!ok || bytesRead <
static_cast<DWORD>(chunkCount * sizeof(SpwChunkEntry)))
{
::CloseHandle(hFile);
unlockVolumes(lockedVolumes);
return ErrorInfo::fromCode(ErrorCode::ImageCorrupt,
"Failed to read SPW chunk table");
}
result = restoreSpwImage(
hFile, header, chunkTable,
dstDisk, dstSectorSize, config.destOffsetBytes,
config.verifyAfterRestore, progressCb);
}
::CloseHandle(hFile);
// Flush writes
if (result.isOk())
{
dstDisk.flushBuffers();
}
// Report completion
if (result.isOk() && progressCb)
{
ImageRestoreProgress done;
done.phase = ImageRestoreProgress::Phase::Complete;
done.percentComplete = 100.0;
progressCb(done);
}
unlockVolumes(lockedVolumes);
return result;
}
// ---------------------------------------------------------------------------
// Restore raw .img — read file in chunks, write sectors to disk
// ---------------------------------------------------------------------------
Result<void> ImageRestorer::restoreRawImage(
HANDLE hFile, uint64_t fileSize,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint64_t dstOffset, uint32_t bufferSize,
ImageRestoreProgressCallback progressCb)
{
// Validate image fits
auto geomResult = dstDisk.getGeometry();
if (geomResult.isError())
return geomResult.error();
if (dstOffset + fileSize > geomResult.value().totalBytes)
{
return ErrorInfo::fromCode(ErrorCode::InsufficientDiskSpace,
"Image file is larger than destination disk");
}
const uint32_t alignedBufSize =
(bufferSize / dstSectorSize) * dstSectorSize;
if (alignedBufSize == 0)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Buffer size too small");
}
std::vector<uint8_t> readBuffer(alignedBufSize);
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesWritten = 0;
uint64_t dstPos = dstOffset;
while (bytesWritten < fileSize)
{
if (isCancelRequested())
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Restore canceled");
}
const uint64_t remaining = fileSize - bytesWritten;
const DWORD readSize = static_cast<DWORD>(
std::min(static_cast<uint64_t>(alignedBufSize), remaining));
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, readBuffer.data(), readSize,
&bytesRead, nullptr);
if (!ok)
{
return makeWin32Error(ErrorCode::ImageReadError,
"Failed to read from image file");
}
if (bytesRead == 0)
break; // EOF
// Pad to sector alignment if needed
const uint32_t alignedWriteSize =
((bytesRead + dstSectorSize - 1) / dstSectorSize) * dstSectorSize;
if (alignedWriteSize > bytesRead)
{
std::memset(readBuffer.data() + bytesRead, 0,
alignedWriteSize - bytesRead);
}
const SectorOffset dstLba = dstPos / dstSectorSize;
const SectorCount dstSectors =
static_cast<SectorCount>(alignedWriteSize / dstSectorSize);
auto writeResult = dstDisk.writeSectors(
dstLba, readBuffer.data(), dstSectors, dstSectorSize);
if (writeResult.isError())
return writeResult.error();
dstPos += bytesRead;
bytesWritten += bytesRead;
if (progressCb)
{
ImageRestoreProgress progress;
progress.phase = ImageRestoreProgress::Phase::Restoring;
progress.bytesWritten = bytesWritten;
progress.totalBytes = fileSize;
progress.percentComplete =
static_cast<double>(bytesWritten) /
static_cast<double>(fileSize) * 100.0;
LARGE_INTEGER now;
::QueryPerformanceCounter(&now);
const double elapsed =
static_cast<double>(now.QuadPart - startTime.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
if (elapsed > 0.0)
{
progress.speedBytesPerSec =
static_cast<double>(bytesWritten) / elapsed;
if (progress.speedBytesPerSec > 0.0)
{
progress.etaSeconds =
static_cast<double>(fileSize - bytesWritten) /
progress.speedBytesPerSec;
}
}
if (!progressCb(progress))
{
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Restore canceled");
}
}
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// Restore SPW compressed image
// ---------------------------------------------------------------------------
Result<void> ImageRestorer::restoreSpwImage(
HANDLE hFile,
const SpwImageHeader& header,
const std::vector<SpwChunkEntry>& chunkTable,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint64_t dstOffset,
bool verify,
ImageRestoreProgressCallback progressCb)
{
const uint32_t chunkSize = header.chunkSize;
const uint32_t chunkCount = header.chunkCount;
const bool isCompressed = (header.compressionType != 0);
// Initialize SHA-256 for verification
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
std::vector<uint8_t> hashObject;
if (verify)
{
NTSTATUS ntStatus = ::BCryptOpenAlgorithmProvider(
&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0);
if (BCRYPT_SUCCESS(ntStatus))
{
DWORD hashObjSize = 0;
DWORD cbData = 0;
::BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&hashObjSize),
sizeof(hashObjSize), &cbData, 0);
hashObject.resize(hashObjSize);
::BCryptCreateHash(hAlg, &hHash, hashObject.data(),
hashObjSize, nullptr, 0, 0);
}
}
LARGE_INTEGER startTime, perfFreq;
::QueryPerformanceFrequency(&perfFreq);
::QueryPerformanceCounter(&startTime);
uint64_t bytesWritten = 0;
const uint64_t totalBytes = header.imageDataSize;
std::vector<uint8_t> zeroChunk(chunkSize, 0);
for (uint32_t chunkIdx = 0; chunkIdx < chunkCount; ++chunkIdx)
{
if (isCancelRequested())
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Restore canceled");
}
const SpwChunkEntry& entry = chunkTable[chunkIdx];
const uint32_t uncompSize = entry.uncompressedSize;
const uint64_t dstChunkOffset =
dstOffset + static_cast<uint64_t>(chunkIdx) * chunkSize;
const uint8_t* writeData = nullptr;
std::vector<uint8_t> decompBuffer;
std::vector<uint8_t> rawReadBuffer;
if (entry.flags & 1)
{
// Sparse chunk — write zeros
writeData = zeroChunk.data();
// Update hash with zeros
if (hHash)
{
::BCryptHashData(hHash, const_cast<PUCHAR>(zeroChunk.data()),
uncompSize, 0);
}
}
else
{
// Seek to chunk data in the file
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(entry.fileOffset);
if (!::SetFilePointerEx(hFile, seekPos, nullptr, FILE_BEGIN))
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return makeWin32Error(ErrorCode::ImageReadError,
"Failed to seek to chunk data");
}
// Read compressed (or uncompressed) data
rawReadBuffer.resize(entry.compressedSize);
DWORD bytesRead = 0;
BOOL ok = ::ReadFile(hFile, rawReadBuffer.data(),
entry.compressedSize, &bytesRead, nullptr);
if (!ok || bytesRead < entry.compressedSize)
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return ErrorInfo::fromCode(ErrorCode::ImageReadError,
"Failed to read chunk data from image");
}
if (isCompressed && entry.compressedSize != entry.uncompressedSize)
{
// Decompress
auto decompResult = decompressLZNT1(
rawReadBuffer.data(), rawReadBuffer.size(),
uncompSize);
if (decompResult.isError())
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return decompResult.error();
}
decompBuffer = std::move(decompResult.value());
writeData = decompBuffer.data();
}
else
{
// Data is uncompressed (stored raw)
writeData = rawReadBuffer.data();
}
// Verify CRC32 of uncompressed data
const uint32_t actualCrc = Checksums::crc32Buffer(
writeData, uncompSize);
if (actualCrc != entry.crc32)
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
std::ostringstream oss;
oss << "CRC32 mismatch on chunk " << chunkIdx
<< ": expected 0x" << std::hex << entry.crc32
<< ", got 0x" << actualCrc;
return ErrorInfo::fromCode(ErrorCode::ImageChecksumMismatch,
oss.str());
}
// Update SHA-256
if (hHash)
{
::BCryptHashData(hHash, const_cast<PUCHAR>(writeData),
uncompSize, 0);
}
}
// Write to destination disk
const uint32_t alignedWriteSize =
((uncompSize + dstSectorSize - 1) / dstSectorSize) * dstSectorSize;
// If we need padding, use a temporary buffer
std::vector<uint8_t> paddedBuffer;
if (alignedWriteSize > uncompSize)
{
paddedBuffer.resize(alignedWriteSize, 0);
std::memcpy(paddedBuffer.data(), writeData, uncompSize);
writeData = paddedBuffer.data();
}
const SectorOffset dstLba = dstChunkOffset / dstSectorSize;
const SectorCount dstSectors =
static_cast<SectorCount>(alignedWriteSize / dstSectorSize);
auto writeResult = dstDisk.writeSectors(
dstLba, writeData, dstSectors, dstSectorSize);
if (writeResult.isError())
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return writeResult.error();
}
bytesWritten += uncompSize;
// Report progress
if (progressCb)
{
ImageRestoreProgress progress;
progress.phase = ImageRestoreProgress::Phase::Restoring;
progress.bytesWritten = bytesWritten;
progress.totalBytes = totalBytes;
progress.percentComplete =
static_cast<double>(bytesWritten) /
static_cast<double>(totalBytes) * 100.0;
LARGE_INTEGER now;
::QueryPerformanceCounter(&now);
const double elapsed =
static_cast<double>(now.QuadPart - startTime.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
if (elapsed > 0.0)
{
progress.speedBytesPerSec =
static_cast<double>(bytesWritten) / elapsed;
if (progress.speedBytesPerSec > 0.0)
{
progress.etaSeconds =
static_cast<double>(totalBytes - bytesWritten) /
progress.speedBytesPerSec;
}
}
if (!progressCb(progress))
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
return ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Restore canceled");
}
}
}
// SHA-256 verification
if (verify && hHash)
{
uint8_t computedHash[32] = {};
::BCryptFinishHash(hHash, computedHash, 32, 0);
::BCryptDestroyHash(hHash);
::BCryptCloseAlgorithmProvider(hAlg, 0);
// Check if the stored hash is all zeros (no hash was stored)
bool storedHashIsZero = true;
for (int i = 0; i < 32; ++i)
{
if (header.sha256[i] != 0)
{
storedHashIsZero = false;
break;
}
}
if (!storedHashIsZero)
{
if (std::memcmp(computedHash, header.sha256, 32) != 0)
{
return ErrorInfo::fromCode(ErrorCode::ImageChecksumMismatch,
"SHA-256 verification failed: restored data does not "
"match the hash stored in the image header");
}
}
// Report verification phase
if (progressCb)
{
ImageRestoreProgress progress;
progress.phase = ImageRestoreProgress::Phase::Verifying;
progress.bytesWritten = totalBytes;
progress.totalBytes = totalBytes;
progress.percentComplete = 100.0;
progressCb(progress);
}
}
else
{
if (hHash) ::BCryptDestroyHash(hHash);
if (hAlg) ::BCryptCloseAlgorithmProvider(hAlg, 0);
}
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,140 @@
#pragma once
// ImageRestorer — Restores disk/partition images from raw (.img) or SPW format.
// Handles LZNT1 decompression, SHA-256 verification, and sparse chunk expansion.
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include "ImageCreator.h" // For SpwImageHeader, SpwChunkEntry
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Progress info for image restoration
struct ImageRestoreProgress
{
uint64_t bytesWritten = 0;
uint64_t totalBytes = 0;
double speedBytesPerSec = 0.0;
double etaSeconds = 0.0;
double percentComplete = 0.0;
enum class Phase
{
Preparing,
Restoring,
Verifying,
Complete,
Failed,
};
Phase phase = Phase::Preparing;
};
using ImageRestoreProgressCallback =
std::function<bool(const ImageRestoreProgress& progress)>;
// Configuration for image restoration
struct ImageRestoreConfig
{
// Input image file
std::wstring inputFilePath;
// Destination disk
DiskId destDiskId = -1;
uint64_t destOffsetBytes = 0; // Where to start writing (0 for whole disk)
// Verify SHA-256 after restore
bool verifyAfterRestore = true;
// Volume letters to lock/dismount on destination before writing
std::vector<wchar_t> destVolumeLetters;
// I/O buffer size
uint32_t bufferSize = 4 * 1024 * 1024;
};
// Information extracted from an SPW image header (for display before restore)
struct SpwImageInfo
{
std::string diskModel;
std::string diskSerial;
uint64_t sourceDiskSize = 0;
uint32_t sourceSectorSize = 0;
PartitionTableType partitionTableType = PartitionTableType::Unknown;
uint64_t imageDataSize = 0;
uint32_t chunkCount = 0;
uint32_t sparseChunkCount = 0;
bool isCompressed = false;
SHA256Hash sha256 = {};
uint64_t creationTimestamp = 0;
};
class ImageRestorer
{
public:
ImageRestorer() = default;
~ImageRestorer() = default;
ImageRestorer(const ImageRestorer&) = delete;
ImageRestorer& operator=(const ImageRestorer&) = delete;
// Inspect an image file and return its metadata (without restoring)
static Result<SpwImageInfo> inspectImage(const std::wstring& filePath);
// Detect whether a file is raw or SPW format
static Result<ImageFormat> detectFormat(const std::wstring& filePath);
// Restore an image to disk. Blocks until complete or canceled.
Result<void> restoreImage(const ImageRestoreConfig& config,
ImageRestoreProgressCallback progressCb = nullptr);
void requestCancel();
bool isCancelRequested() const;
private:
std::atomic<bool> m_cancelRequested{false};
// Restore raw .img
Result<void> restoreRawImage(
HANDLE hFile, uint64_t fileSize,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint64_t dstOffset, uint32_t bufferSize,
ImageRestoreProgressCallback progressCb);
// Restore SPW compressed image
Result<void> restoreSpwImage(
HANDLE hFile,
const SpwImageHeader& header,
const std::vector<SpwChunkEntry>& chunkTable,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint64_t dstOffset,
bool verify,
ImageRestoreProgressCallback progressCb);
// Decompress LZNT1 data
static Result<std::vector<uint8_t>> decompressLZNT1(
const uint8_t* compressedData, size_t compressedSize,
size_t uncompressedSize);
// Lock/dismount destination volumes
Result<std::vector<HANDLE>> lockDestinationVolumes(
const std::vector<wchar_t>& volumeLetters);
void unlockVolumes(std::vector<HANDLE>& handles);
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,220 @@
#pragma once
// IsoFlasher — Flash ISO/IMG files to USB drives and SD cards.
// Supports hybrid ISO dd-write, non-hybrid ISO with FAT32 extraction,
// UEFI boot detection, and basic ISO9660 filesystem parsing.
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include "Checksums.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Progress info for flashing
struct FlashProgress
{
uint64_t bytesWritten = 0;
uint64_t totalBytes = 0;
double speedBytesPerSec = 0.0;
double etaSeconds = 0.0;
double percentComplete = 0.0;
enum class Phase
{
Preparing,
Flashing,
Verifying,
Complete,
Failed,
};
Phase phase = Phase::Preparing;
};
using FlashProgressCallback =
std::function<bool(const FlashProgress& progress)>;
// ---------------------------------------------------------------------------
// ISO9660 on-disk structures for reading ISO contents
// All offsets per ECMA-119 / ISO 9660 specification.
// ---------------------------------------------------------------------------
#pragma pack(push, 1)
// ISO 9660 Volume Descriptor (2048-byte sectors starting at LBA 16)
struct Iso9660VolumeDescriptor
{
uint8_t type; // 1 = Primary, 2 = Supplementary, 255 = Terminator
char standardId[5]; // "CD001"
uint8_t version; // 1
uint8_t unused1;
char systemId[32];
char volumeId[32];
uint8_t unused2[8];
uint32_t volumeSpaceSizeLe; // Little-endian total sectors
uint32_t volumeSpaceSizeBe; // Big-endian total sectors
uint8_t unused3[32];
uint16_t volumeSetSizeLe;
uint16_t volumeSetSizeBe;
uint16_t volumeSeqNumLe;
uint16_t volumeSeqNumBe;
uint16_t logicalBlockSizeLe; // Usually 2048
uint16_t logicalBlockSizeBe;
uint32_t pathTableSizeLe;
uint32_t pathTableSizeBe;
uint32_t pathTableLocLe; // LBA of LE path table
uint32_t pathTableOptLocLe;
uint32_t pathTableLocBe; // Big-endian path table LBA
uint32_t pathTableOptLocBe;
uint8_t rootDirRecord[34]; // Root directory record
// ... rest of fields to 2048 bytes (we only need the above)
};
// ISO 9660 Directory Record (variable length)
struct Iso9660DirRecord
{
uint8_t recordLength;
uint8_t extAttrRecordLength;
uint32_t extentLbaLe;
uint32_t extentLbaBe;
uint32_t dataSizeLe;
uint32_t dataSizeBe;
uint8_t recordingDate[7];
uint8_t fileFlags; // Bit 1: directory
uint8_t fileUnitSize;
uint8_t interleaveGap;
uint16_t volumeSeqNumLe;
uint16_t volumeSeqNumBe;
uint8_t fileIdLength;
// fileId follows (variable length, then padding byte if fileIdLength is even)
};
#pragma pack(pop)
// Parsed file entry from ISO9660
struct IsoFileEntry
{
std::string name;
uint32_t lba = 0; // Start sector in the ISO
uint32_t size = 0; // File size in bytes
bool isDirectory = false;
};
// Flashing configuration
struct FlashConfig
{
// Input file (.iso or .img)
std::wstring inputFilePath;
// Target disk (must be removable unless forceFixed is true)
DiskId targetDiskId = -1;
// Safety: refuse to flash to fixed (non-removable) disks unless forced
bool forceFixed = false;
// Verify after flash by reading back and comparing hash
bool verifyAfterFlash = true;
// Volume letters on target to lock/dismount
std::vector<wchar_t> targetVolumeLetters;
// I/O buffer size
uint32_t bufferSize = 4 * 1024 * 1024;
};
class IsoFlasher
{
public:
IsoFlasher() = default;
~IsoFlasher() = default;
IsoFlasher(const IsoFlasher&) = delete;
IsoFlasher& operator=(const IsoFlasher&) = delete;
// Flash an ISO or IMG file to a disk. Blocks until complete or canceled.
Result<void> flash(const FlashConfig& config,
FlashProgressCallback progressCb = nullptr);
void requestCancel();
bool isCancelRequested() const;
// Utility: check if an ISO file is hybrid (has valid MBR at offset 0)
static Result<bool> isHybridIso(const std::wstring& isoPath);
// Utility: check if an ISO contains UEFI boot files
static Result<bool> hasUefiBoot(const std::wstring& isoPath);
// Utility: list files in an ISO9660 image (top-level directory)
static Result<std::vector<IsoFileEntry>> listIsoContents(
const std::wstring& isoPath);
// Utility: read a file from an ISO9660 image by its path
static Result<std::vector<uint8_t>> readIsoFile(
const std::wstring& isoPath,
const std::string& filePath);
private:
std::atomic<bool> m_cancelRequested{false};
// Flash raw IMG file (dd-style write)
Result<void> flashRawImage(
HANDLE hFile, uint64_t fileSize,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint32_t bufferSize, bool verify,
FlashProgressCallback progressCb);
// Flash hybrid ISO (dd-style write, same as raw)
Result<void> flashHybridIso(
HANDLE hFile, uint64_t fileSize,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint32_t bufferSize, bool verify,
FlashProgressCallback progressCb);
// Flash non-hybrid ISO: create FAT32 partition and copy files
Result<void> flashNonHybridIso(
HANDLE hFile, uint64_t fileSize,
const std::wstring& isoPath,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint32_t bufferSize,
FlashProgressCallback progressCb);
// Verify flash by re-reading and comparing SHA-256
Result<void> verifyFlash(
HANDLE hFile, uint64_t fileSize,
RawDiskHandle& dstDisk, uint32_t dstSectorSize,
uint32_t bufferSize,
FlashProgressCallback progressCb);
// Lock/dismount target volumes
Result<std::vector<HANDLE>> lockTargetVolumes(
const std::vector<wchar_t>& volumeLetters);
void unlockVolumes(std::vector<HANDLE>& handles);
// Parse ISO9660 Primary Volume Descriptor
static Result<Iso9660VolumeDescriptor> readPVD(HANDLE hFile);
// Parse directory records from an ISO9660 directory extent
static Result<std::vector<IsoFileEntry>> parseDirectoryExtent(
HANDLE hFile, uint32_t extentLba, uint32_t extentSize);
// Find a file in the ISO by walking directories (supports paths like /EFI/BOOT/BOOTX64.EFI)
static Result<IsoFileEntry> findFileInIso(
HANDLE hFile, const std::string& path);
};
} // namespace spw

View File

@@ -0,0 +1,604 @@
// SecureErase.cpp -- Secure data erasure with multiple standard methods.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// All erase operations PERMANENTLY DESTROY DATA.
#include "SecureErase.h"
#include <algorithm>
#include <cstring>
// For BCryptGenRandom
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
namespace spw
{
// ---------------------------------------------------------------------------
// Gutmann 35-pass pattern definitions
//
// Passes 1-4 and 33-35 are random. Passes 5-31 are specific patterns
// designed to defeat MFM and RLL encoding recovery techniques (historically
// relevant for older magnetic media).
//
// Reference: Peter Gutmann, "Secure Deletion of Data from Magnetic and
// Solid-State Memory", 1996.
// ---------------------------------------------------------------------------
// Each inner vector is one pattern byte (repeated across the sector).
// An empty vector means "random data".
static const std::vector<std::vector<uint8_t>> GUTMANN_PASSES = {
{}, // Pass 1: random
{}, // Pass 2: random
{}, // Pass 3: random
{}, // Pass 4: random
{0x55, 0x55, 0x55}, // Pass 5
{0xAA, 0xAA, 0xAA}, // Pass 6
{0x92, 0x49, 0x24}, // Pass 7
{0x49, 0x24, 0x92}, // Pass 8
{0x24, 0x92, 0x49}, // Pass 9
{0x00, 0x00, 0x00}, // Pass 10
{0x11, 0x11, 0x11}, // Pass 11
{0x22, 0x22, 0x22}, // Pass 12
{0x33, 0x33, 0x33}, // Pass 13
{0x44, 0x44, 0x44}, // Pass 14
{0x55, 0x55, 0x55}, // Pass 15
{0x66, 0x66, 0x66}, // Pass 16
{0x77, 0x77, 0x77}, // Pass 17
{0x88, 0x88, 0x88}, // Pass 18
{0x99, 0x99, 0x99}, // Pass 19
{0xAA, 0xAA, 0xAA}, // Pass 20
{0xBB, 0xBB, 0xBB}, // Pass 21
{0xCC, 0xCC, 0xCC}, // Pass 22
{0xDD, 0xDD, 0xDD}, // Pass 23
{0xEE, 0xEE, 0xEE}, // Pass 24
{0xFF, 0xFF, 0xFF}, // Pass 25
{0x92, 0x49, 0x24}, // Pass 26
{0x49, 0x24, 0x92}, // Pass 27
{0x24, 0x92, 0x49}, // Pass 28
{0x6D, 0xB6, 0xDB}, // Pass 29
{0xB6, 0xDB, 0x6D}, // Pass 30
{0xDB, 0x6D, 0xB6}, // Pass 31
{}, // Pass 32: random
{}, // Pass 33: random
{}, // Pass 34: random
{}, // Pass 35: random
};
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
SecureErase::SecureErase(RawDiskHandle& disk)
: m_disk(disk)
{
}
// ---------------------------------------------------------------------------
// buildPassList -- construct the list of per-pass patterns for a given method
// ---------------------------------------------------------------------------
std::vector<std::vector<uint8_t>> SecureErase::buildPassList(const EraseConfig& config)
{
std::vector<std::vector<uint8_t>> passes;
switch (config.method)
{
case EraseMethod::ZeroFill:
passes.push_back({0x00});
break;
case EraseMethod::DoD_3Pass:
// Pass 1: 0x00, Pass 2: 0xFF, Pass 3: random
passes.push_back({0x00});
passes.push_back({0xFF});
passes.push_back({}); // empty = random
break;
case EraseMethod::DoD_7Pass:
// DoD 5220.22-M ECE (7-pass):
// Passes 1-3: DoD 3-pass (0x00, 0xFF, random)
// Pass 4: pattern 0x00
// Passes 5-7: DoD 3-pass again (0x00, 0xFF, random)
passes.push_back({0x00});
passes.push_back({0xFF});
passes.push_back({}); // random
passes.push_back({0x00});
passes.push_back({0x00});
passes.push_back({0xFF});
passes.push_back({}); // random
break;
case EraseMethod::Gutmann:
passes = GUTMANN_PASSES;
break;
case EraseMethod::RandomFill:
{
int count = std::max(config.passCount, 1);
for (int i = 0; i < count; ++i)
passes.push_back({}); // empty = random
break;
}
case EraseMethod::CustomPattern:
{
int count = std::max(config.customPatternPasses, 1);
for (int i = 0; i < count; ++i)
passes.push_back(config.customPattern);
break;
}
}
return passes;
}
// ---------------------------------------------------------------------------
// fillRandom -- fill a buffer with cryptographically secure random data
// ---------------------------------------------------------------------------
Result<void> SecureErase::fillRandom(uint8_t* buffer, uint32_t size)
{
NTSTATUS status = BCryptGenRandom(nullptr, buffer, size,
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (status != 0) // STATUS_SUCCESS == 0
return ErrorInfo::fromCode(ErrorCode::Unknown, "BCryptGenRandom failed");
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// fillPattern -- fill a buffer with a repeating pattern
// ---------------------------------------------------------------------------
void SecureErase::fillPattern(uint8_t* buffer, uint32_t bufferSize,
const std::vector<uint8_t>& pattern)
{
if (pattern.empty())
return; // Caller should use fillRandom instead
if (pattern.size() == 1)
{
// Optimize the common case: single-byte pattern
std::memset(buffer, pattern[0], bufferSize);
return;
}
// Multi-byte pattern: tile it across the buffer
size_t patLen = pattern.size();
for (uint32_t i = 0; i < bufferSize; ++i)
buffer[i] = pattern[i % patLen];
}
// ---------------------------------------------------------------------------
// lockAllVolumes -- lock and dismount every volume on this physical disk
// ---------------------------------------------------------------------------
Result<std::vector<HANDLE>> SecureErase::lockAllVolumes()
{
std::vector<HANDLE> handles;
// Enumerate volume letters A-Z and check which ones are on this disk
for (wchar_t letter = L'A'; letter <= L'Z'; ++letter)
{
// Skip if the drive letter doesn't exist
std::wstring rootPath = std::wstring(1, letter) + L":\\";
UINT driveType = GetDriveTypeW(rootPath.c_str());
if (driveType == DRIVE_NO_ROOT_DIR || driveType == DRIVE_UNKNOWN)
continue;
// Try to check if this volume is on our disk by opening the volume
// and querying its extents. This is the Win32 way to map volumes
// to physical disks.
std::wstring volumePath = L"\\\\.\\";
volumePath += letter;
volumePath += L':';
HANDLE hVolume = CreateFileW(
volumePath.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (hVolume == INVALID_HANDLE_VALUE)
continue;
// Query disk extents to see if this volume is on our disk
VOLUME_DISK_EXTENTS extents = {};
DWORD bytesReturned = 0;
BOOL ok = DeviceIoControl(
hVolume,
IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
nullptr, 0,
&extents, sizeof(extents),
&bytesReturned, nullptr);
if (!ok || extents.NumberOfDiskExtents == 0)
{
CloseHandle(hVolume);
continue;
}
// Check if any extent is on our disk
bool onOurDisk = false;
for (DWORD i = 0; i < extents.NumberOfDiskExtents; ++i)
{
if (static_cast<int>(extents.Extents[i].DiskNumber) == m_disk.diskId())
{
onOurDisk = true;
break;
}
}
if (!onOurDisk)
{
CloseHandle(hVolume);
continue;
}
// Lock the volume
ok = DeviceIoControl(hVolume, FSCTL_LOCK_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
if (!ok)
{
CloseHandle(hVolume);
// Return error: cannot lock a volume that's in use
// Clean up already-locked handles
for (auto h : handles)
{
DeviceIoControl(h, FSCTL_UNLOCK_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
CloseHandle(h);
}
return ErrorInfo::fromWin32(ErrorCode::DiskLockFailed, GetLastError(),
std::string("Cannot lock volume ") +
static_cast<char>(letter) + ":");
}
// Dismount the volume
DeviceIoControl(hVolume, FSCTL_DISMOUNT_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
handles.push_back(hVolume);
}
return handles;
}
// ---------------------------------------------------------------------------
// unlockAllVolumes
// ---------------------------------------------------------------------------
void SecureErase::unlockAllVolumes(std::vector<HANDLE>& handles)
{
DWORD bytesReturned = 0;
for (auto h : handles)
{
DeviceIoControl(h, FSCTL_UNLOCK_VOLUME,
nullptr, 0, nullptr, 0, &bytesReturned, nullptr);
CloseHandle(h);
}
handles.clear();
}
// ---------------------------------------------------------------------------
// eraseDisk -- erase the entire physical disk
// ---------------------------------------------------------------------------
Result<void> SecureErase::eraseDisk(
const EraseConfig& config,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
const uint32_t sectorSize = m_geometry.bytesPerSector;
if (sectorSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 bytes/sector");
const uint64_t totalSectors = m_geometry.totalBytes / sectorSize;
// Lock and dismount all volumes on this disk
auto lockResult = lockAllVolumes();
if (lockResult.isError())
return lockResult.error();
auto lockedHandles = std::move(lockResult.value());
auto passes = buildPassList(config);
int totalPasses = static_cast<int>(passes.size()) + (config.verify ? 1 : 0);
Result<void> finalResult = Result<void>::ok();
for (int passIdx = 0; passIdx < static_cast<int>(passes.size()); ++passIdx)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
finalResult = ErrorInfo::fromCode(ErrorCode::OperationCanceled);
break;
}
auto passResult = writePass(0, totalSectors, sectorSize, passes[passIdx],
passIdx + 1, totalPasses, progressCb, cancelFlag);
if (passResult.isError())
{
finalResult = passResult;
break;
}
// Flush after each pass
m_disk.flushBuffers();
}
// Verification pass (if requested and no errors so far)
if (finalResult.isOk() && config.verify && !passes.empty())
{
// Verify against the last pass's pattern
const auto& lastPattern = passes.back();
auto verResult = verifyPass(0, totalSectors, sectorSize, lastPattern,
totalPasses, progressCb, cancelFlag);
if (verResult.isError())
finalResult = verResult;
}
// Unlock all volumes
unlockAllVolumes(lockedHandles);
return finalResult;
}
// ---------------------------------------------------------------------------
// eraseRange -- erase a specific partition
// ---------------------------------------------------------------------------
Result<void> SecureErase::eraseRange(
SectorOffset startLba,
SectorCount sectorCount,
const EraseConfig& config,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
const uint32_t sectorSize = m_geometry.bytesPerSector;
if (sectorSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 bytes/sector");
// Lock and dismount volumes
auto lockResult = lockAllVolumes();
if (lockResult.isError())
return lockResult.error();
auto lockedHandles = std::move(lockResult.value());
auto passes = buildPassList(config);
int totalPasses = static_cast<int>(passes.size()) + (config.verify ? 1 : 0);
Result<void> finalResult = Result<void>::ok();
for (int passIdx = 0; passIdx < static_cast<int>(passes.size()); ++passIdx)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
{
finalResult = ErrorInfo::fromCode(ErrorCode::OperationCanceled);
break;
}
auto passResult = writePass(startLba, sectorCount, sectorSize, passes[passIdx],
passIdx + 1, totalPasses, progressCb, cancelFlag);
if (passResult.isError())
{
finalResult = passResult;
break;
}
m_disk.flushBuffers();
}
if (finalResult.isOk() && config.verify && !passes.empty())
{
const auto& lastPattern = passes.back();
auto verResult = verifyPass(startLba, sectorCount, sectorSize, lastPattern,
totalPasses, progressCb, cancelFlag);
if (verResult.isError())
finalResult = verResult;
}
unlockAllVolumes(lockedHandles);
return finalResult;
}
// ---------------------------------------------------------------------------
// writePass -- write a single pass of a pattern or random data across range
// ---------------------------------------------------------------------------
Result<void> SecureErase::writePass(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
const std::vector<uint8_t>& pattern,
int currentPass,
int totalPasses,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag)
{
const bool isRandom = pattern.empty();
// Use a 64 KiB write buffer. For random passes, we generate random data
// once and reuse the buffer for speed (BCryptGenRandom on every sector would
// be prohibitively slow). We re-randomize every N writes.
constexpr uint32_t BUFFER_SIZE = 64 * 1024;
const SectorCount bufferSectors = BUFFER_SIZE / sectorSize;
std::vector<uint8_t> buffer(BUFFER_SIZE);
if (isRandom)
{
auto rr = fillRandom(buffer.data(), BUFFER_SIZE);
if (rr.isError())
return rr;
}
else
{
fillPattern(buffer.data(), BUFFER_SIZE, pattern);
}
// Timing for speed calculation
LARGE_INTEGER perfFreq, perfStart, perfNow;
QueryPerformanceFrequency(&perfFreq);
QueryPerformanceCounter(&perfStart);
uint64_t bytesWritten = 0;
uint64_t totalBytes = sectorCount * sectorSize;
uint32_t randomRefreshCounter = 0;
constexpr uint32_t RANDOM_REFRESH_INTERVAL = 256; // Re-randomize every 256 writes
SectorOffset currentLba = startLba;
SectorOffset endLba = startLba + sectorCount;
while (currentLba < endLba)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
return ErrorInfo::fromCode(ErrorCode::OperationCanceled, "Erase canceled");
SectorCount remaining = endLba - currentLba;
SectorCount thisChunk = std::min(bufferSectors, remaining);
uint32_t writeSize = static_cast<uint32_t>(thisChunk) * sectorSize;
// Periodically refresh random buffer to maintain cryptographic quality
if (isRandom && ++randomRefreshCounter >= RANDOM_REFRESH_INTERVAL)
{
randomRefreshCounter = 0;
auto rr = fillRandom(buffer.data(), BUFFER_SIZE);
if (rr.isError())
return rr;
}
auto writeResult = m_disk.writeSectors(currentLba, buffer.data(), thisChunk, sectorSize);
if (writeResult.isError())
{
// Retry individual sectors on failure
for (SectorCount s = 0; s < thisChunk; ++s)
{
auto retry = m_disk.writeSectors(currentLba + s, buffer.data(), 1, sectorSize);
// If individual sector write also fails, continue anyway
// (bad sectors can't be erased but we don't want to abort the whole op)
(void)retry;
}
}
bytesWritten += writeSize;
currentLba += thisChunk;
if (progressCb)
{
QueryPerformanceCounter(&perfNow);
double elapsed = static_cast<double>(perfNow.QuadPart - perfStart.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
double speedMBps = (elapsed > 0.0)
? (static_cast<double>(bytesWritten) / (1024.0 * 1024.0)) / elapsed
: 0.0;
progressCb(currentPass, totalPasses, bytesWritten, totalBytes, speedMBps);
}
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// verifyPass -- read back and verify against the expected pattern
// ---------------------------------------------------------------------------
Result<void> SecureErase::verifyPass(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
const std::vector<uint8_t>& pattern,
int totalPasses,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag)
{
const bool isRandom = pattern.empty();
// Cannot verify random passes (we don't store the random data),
// so just do a read test to confirm sectors are readable.
constexpr uint32_t BUFFER_SIZE = 64 * 1024;
const SectorCount bufferSectors = BUFFER_SIZE / sectorSize;
// Build expected pattern buffer for non-random passes
std::vector<uint8_t> expectedBuf;
if (!isRandom)
{
expectedBuf.resize(BUFFER_SIZE);
fillPattern(expectedBuf.data(), BUFFER_SIZE, pattern);
}
LARGE_INTEGER perfFreq, perfStart, perfNow;
QueryPerformanceFrequency(&perfFreq);
QueryPerformanceCounter(&perfStart);
uint64_t bytesVerified = 0;
uint64_t totalBytes = sectorCount * sectorSize;
SectorOffset currentLba = startLba;
SectorOffset endLba = startLba + sectorCount;
while (currentLba < endLba)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
return ErrorInfo::fromCode(ErrorCode::OperationCanceled, "Verify canceled");
SectorCount remaining = endLba - currentLba;
SectorCount thisChunk = std::min(bufferSectors, remaining);
auto readResult = m_disk.readSectors(currentLba, thisChunk, sectorSize);
if (readResult.isError())
{
// Sectors unreadable after erase -- could be bad sectors
// This is not necessarily an erase failure, so log but continue
}
else if (!isRandom)
{
const auto& readData = readResult.value();
uint32_t compareSize = static_cast<uint32_t>(thisChunk) * sectorSize;
compareSize = std::min(compareSize, static_cast<uint32_t>(readData.size()));
if (std::memcmp(readData.data(), expectedBuf.data(), compareSize) != 0)
{
return ErrorInfo::fromCode(ErrorCode::DiskWriteError,
"Verification failed: data mismatch after erase");
}
}
bytesVerified += static_cast<uint64_t>(thisChunk) * sectorSize;
currentLba += thisChunk;
if (progressCb)
{
QueryPerformanceCounter(&perfNow);
double elapsed = static_cast<double>(perfNow.QuadPart - perfStart.QuadPart) /
static_cast<double>(perfFreq.QuadPart);
double speedMBps = (elapsed > 0.0)
? (static_cast<double>(bytesVerified) / (1024.0 * 1024.0)) / elapsed
: 0.0;
// Report as the verify pass (last pass)
progressCb(totalPasses, totalPasses, bytesVerified, totalBytes, speedMBps);
}
}
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,131 @@
#pragma once
// SecureErase -- Securely overwrite disk data using standard erasure methods.
//
// Supported methods:
// - Zero fill (1 pass)
// - DoD 5220.22-M (3-pass): 0x00, 0xFF, random
// - DoD 5220.22-M ECE (7-pass): extended version
// - Gutmann (35-pass): full Gutmann pattern sequence
// - Random fill (configurable N passes)
// - Custom pattern (user-defined byte pattern, configurable passes)
//
// Each method includes an optional verification pass. Before erasing,
// all volumes on the target disk/partition are locked and dismounted.
//
// Uses BCryptGenRandom for cryptographically secure random data.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// All erase operations PERMANENTLY DESTROY DATA and are
// IRREVERSIBLE. Use with extreme caution.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Erasure method
enum class EraseMethod
{
ZeroFill, // 1 pass: all zeros
DoD_3Pass, // DoD 5220.22-M: 0x00, 0xFF, random
DoD_7Pass, // DoD 5220.22-M ECE: 7-pass extended
Gutmann, // Gutmann 35-pass
RandomFill, // N passes of CSPRNG data
CustomPattern, // User-defined byte pattern
};
// Configuration for a secure erase operation
struct EraseConfig
{
EraseMethod method = EraseMethod::ZeroFill;
int passCount = 1; // Only used for RandomFill
bool verify = true; // Verify after last pass
std::vector<uint8_t> customPattern; // Only used for CustomPattern
int customPatternPasses = 1; // Passes for custom pattern
};
// Progress callback.
// Parameters: (currentPass, totalPasses, bytesWritten, totalBytes, speedMBps)
using EraseProgress = std::function<void(int currentPass,
int totalPasses,
uint64_t bytesWritten,
uint64_t totalBytes,
double speedMBps)>;
class SecureErase
{
public:
explicit SecureErase(RawDiskHandle& disk);
// Erase the entire disk
Result<void> eraseDisk(
const EraseConfig& config,
EraseProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
// Erase a specific partition (range of sectors)
Result<void> eraseRange(
SectorOffset startLba,
SectorCount sectorCount,
const EraseConfig& config,
EraseProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
private:
// Build the list of passes (pattern byte sequences) for the chosen method
static std::vector<std::vector<uint8_t>> buildPassList(const EraseConfig& config);
// Write a single pass of a given pattern across the range
Result<void> writePass(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
const std::vector<uint8_t>& pattern, // Empty means random data
int currentPass,
int totalPasses,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag);
// Verification pass: read back and verify against expected pattern
Result<void> verifyPass(
SectorOffset startLba,
SectorCount sectorCount,
uint32_t sectorSize,
const std::vector<uint8_t>& pattern,
int totalPasses,
EraseProgress progressCb,
std::atomic<bool>* cancelFlag);
// Fill a buffer with CSPRNG random data using BCryptGenRandom
static Result<void> fillRandom(uint8_t* buffer, uint32_t size);
// Fill a buffer with a repeating pattern
static void fillPattern(uint8_t* buffer, uint32_t bufferSize,
const std::vector<uint8_t>& pattern);
// Lock and dismount all volumes on this disk
Result<std::vector<HANDLE>> lockAllVolumes();
void unlockAllVolumes(std::vector<HANDLE>& handles);
RawDiskHandle& m_disk;
DiskGeometryInfo m_geometry = {};
};
} // namespace spw

View File

@@ -0,0 +1,128 @@
#pragma once
// Operation — Abstract base class for all disk operations.
//
// Operations follow a GParted-style pattern: they are queued first, then
// applied sequentially. Each operation knows how to execute itself and
// (where possible) undo itself.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <functional>
#include <memory>
#include <string>
#include <QString>
namespace spw
{
// Progress callback for individual operations: (percent 0-100, status message)
using ProgressCallback = std::function<void(int percent, const QString& status)>;
class Operation
{
public:
// All supported operation types
enum class Type
{
CreatePartition,
DeletePartition,
ResizePartition,
MovePartition,
FormatPartition,
SetLabel,
SetFlags,
Clone,
CreateImage,
RestoreImage,
FlashImage,
SecureErase,
RepairBoot,
CheckFilesystem,
};
// Execution state tracking
enum class State
{
Pending, // Queued, not yet executed
Running, // Currently executing
Completed, // Finished successfully
Failed, // Finished with error
Undone, // Successfully undone
};
virtual ~Operation() = default;
// What kind of operation is this?
virtual Type type() const = 0;
// Human-readable description for the UI
virtual QString description() const = 0;
// Execute the operation. Progress is reported via the callback.
// Returns Result<void> — success or an error.
virtual Result<void> execute(ProgressCallback progress) = 0;
// Attempt to undo the operation. Not all operations are undoable.
// Default implementation returns NotImplemented.
virtual Result<void> undo()
{
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"Undo is not supported for this operation");
}
// Returns true if this operation can be undone after execution.
virtual bool canUndo() const { return false; }
// Current state
State state() const { return m_state; }
// Error info if state == Failed
const ErrorInfo& lastError() const { return m_lastError; }
// Target disk for this operation (if applicable)
DiskId targetDiskId() const { return m_targetDiskId; }
void setTargetDiskId(DiskId id) { m_targetDiskId = id; }
// Target partition index (if applicable)
PartitionId targetPartitionId() const { return m_targetPartitionId; }
void setTargetPartitionId(PartitionId id) { m_targetPartitionId = id; }
// Returns the operation type as a string
static QString typeToString(Type t)
{
switch (t)
{
case Type::CreatePartition: return QStringLiteral("Create Partition");
case Type::DeletePartition: return QStringLiteral("Delete Partition");
case Type::ResizePartition: return QStringLiteral("Resize Partition");
case Type::MovePartition: return QStringLiteral("Move Partition");
case Type::FormatPartition: return QStringLiteral("Format Partition");
case Type::SetLabel: return QStringLiteral("Set Label");
case Type::SetFlags: return QStringLiteral("Set Flags");
case Type::Clone: return QStringLiteral("Clone");
case Type::CreateImage: return QStringLiteral("Create Image");
case Type::RestoreImage: return QStringLiteral("Restore Image");
case Type::FlashImage: return QStringLiteral("Flash Image");
case Type::SecureErase: return QStringLiteral("Secure Erase");
case Type::RepairBoot: return QStringLiteral("Repair Boot");
case Type::CheckFilesystem: return QStringLiteral("Check Filesystem");
}
return QStringLiteral("Unknown");
}
friend class OperationQueue;
protected:
State m_state = State::Pending;
ErrorInfo m_lastError;
DiskId m_targetDiskId = -1;
PartitionId m_targetPartitionId = -1;
};
} // namespace spw

View File

@@ -0,0 +1,225 @@
// OperationQueue.cpp — GParted-style operation queue implementation.
//
// Operations are queued, then applied sequentially. Execution stops on
// first error. Progress is reported via Qt signals for UI integration.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "OperationQueue.h"
#include <algorithm>
namespace spw
{
OperationQueue::OperationQueue(QObject* parent)
: QObject(parent)
{
}
OperationQueue::~OperationQueue() = default;
// ============================================================================
// Queue management
// ============================================================================
void OperationQueue::enqueue(std::unique_ptr<Operation> op)
{
if (op)
{
m_pending.push_back(std::move(op));
}
}
std::unique_ptr<Operation> OperationQueue::removeLast()
{
if (m_pending.empty())
return nullptr;
// Only remove if the last operation is still pending
if (m_pending.back()->state() != Operation::State::Pending)
return nullptr;
auto op = std::move(m_pending.back());
m_pending.pop_back();
return op;
}
void OperationQueue::clearPending()
{
// Remove only pending operations (from the back, since some at the front
// might have been partially processed if we stopped mid-run)
auto it = std::remove_if(m_pending.begin(), m_pending.end(),
[](const std::unique_ptr<Operation>& op)
{
return op->state() == Operation::State::Pending;
});
m_pending.erase(it, m_pending.end());
}
void OperationQueue::clearAll()
{
m_pending.clear();
m_history.clear();
m_lastRunSuccess = false;
}
// ============================================================================
// Query
// ============================================================================
int OperationQueue::pendingCount() const
{
return static_cast<int>(m_pending.size());
}
int OperationQueue::completedCount() const
{
return static_cast<int>(m_history.size());
}
int OperationQueue::totalCount() const
{
return pendingCount() + completedCount();
}
const Operation* OperationQueue::pendingAt(int index) const
{
if (index < 0 || index >= static_cast<int>(m_pending.size()))
return nullptr;
return m_pending[static_cast<size_t>(index)].get();
}
// ============================================================================
// Execution
// ============================================================================
Result<void> OperationQueue::applyAll()
{
if (m_pending.empty())
{
m_lastRunSuccess = true;
emit allOperationsFinished(true, 0, 0);
return Result<void>::ok();
}
m_running = true;
m_cancelRequested = false;
m_lastRunSuccess = false;
const int totalOps = static_cast<int>(m_pending.size());
int opIndex = 0;
ErrorInfo lastError;
while (!m_pending.empty())
{
if (m_cancelRequested)
{
lastError = ErrorInfo::fromCode(ErrorCode::OperationCanceled,
"Operation queue canceled by user");
break;
}
// Take the front operation
auto op = std::move(m_pending.front());
m_pending.pop_front();
QString desc = op->description();
emit operationStarted(opIndex, totalOps, desc);
// Build a progress callback that maps per-operation progress to overall progress
auto progressCb = [this, opIndex, totalOps](int opPercent, const QString& status)
{
// Overall percent: evenly divided among operations
int overallPercent = (opIndex * 100 + opPercent) / totalOps;
overallPercent = std::clamp(overallPercent, 0, 100);
emit queueProgress(overallPercent, opPercent, status);
};
// Execute the operation
op->m_state = Operation::State::Running;
auto result = op->execute(progressCb);
if (result.isOk())
{
op->m_state = Operation::State::Completed;
emit operationCompleted(opIndex, true, desc);
}
else
{
op->m_state = Operation::State::Failed;
op->m_lastError = result.error();
lastError = result.error();
emit operationCompleted(opIndex, false, desc);
emit errorOccurred(opIndex, desc, result.error());
// Move to history and stop
m_history.push_back(std::move(op));
break;
}
m_history.push_back(std::move(op));
++opIndex;
}
// Check if all completed successfully
bool success = m_pending.empty() && !lastError.isError();
m_lastRunSuccess = success;
m_running = false;
emit allOperationsFinished(success, opIndex, totalOps);
if (lastError.isError())
return lastError;
return Result<void>::ok();
}
Result<void> OperationQueue::undoLast()
{
if (m_history.empty())
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"No operations to undo");
}
auto& lastOp = m_history.back();
if (!lastOp->canUndo())
{
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"Last operation does not support undo: " + lastOp->description().toStdString());
}
if (lastOp->state() != Operation::State::Completed)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Can only undo completed operations");
}
auto result = lastOp->undo();
if (result.isOk())
{
lastOp->m_state = Operation::State::Undone;
}
return result;
}
bool OperationQueue::canUndoLast() const
{
if (m_history.empty())
return false;
const auto& lastOp = m_history.back();
return lastOp->canUndo() && lastOp->state() == Operation::State::Completed;
}
void OperationQueue::requestCancel()
{
m_cancelRequested = true;
}
} // namespace spw

View File

@@ -0,0 +1,137 @@
#pragma once
// OperationQueue — GParted-style operation queue.
//
// Operations are queued without being applied. When the user confirms,
// all queued operations are applied sequentially. On first error,
// execution stops. Individual operations may be undone if they support it.
//
// The queue emits Qt signals for progress reporting and error notification,
// making it easy to connect to a UI progress dialog.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "Operation.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include <deque>
#include <functional>
#include <memory>
#include <vector>
#include <QObject>
#include <QString>
namespace spw
{
class OperationQueue : public QObject
{
Q_OBJECT
public:
explicit OperationQueue(QObject* parent = nullptr);
~OperationQueue() override;
// Non-copyable
OperationQueue(const OperationQueue&) = delete;
OperationQueue& operator=(const OperationQueue&) = delete;
// ----- Queue management -----
// Add an operation to the end of the queue.
// Takes ownership of the operation.
void enqueue(std::unique_ptr<Operation> op);
// Remove the last queued operation (if still pending).
// Returns the removed operation, or nullptr if queue is empty/not pending.
std::unique_ptr<Operation> removeLast();
// Clear all pending operations from the queue.
// Does not affect completed or failed operations in the history.
void clearPending();
// Clear everything (pending queue + history)
void clearAll();
// ----- Query -----
// Number of pending (not yet executed) operations
int pendingCount() const;
// Number of completed operations in history
int completedCount() const;
// Total operations (pending + completed + failed)
int totalCount() const;
// Get a pending operation by index
const Operation* pendingAt(int index) const;
// Get all pending operations (read-only view)
const std::deque<std::unique_ptr<Operation>>& pending() const { return m_pending; }
// Get completed operation history (read-only view)
const std::vector<std::unique_ptr<Operation>>& history() const { return m_history; }
// Is the queue currently executing?
bool isRunning() const { return m_running; }
// Was the last apply run successful (all ops completed)?
bool lastRunSuccessful() const { return m_lastRunSuccess; }
// ----- Execution -----
// Apply all queued operations sequentially.
// Stops on first error. Returns the error from the failed operation,
// or success if all completed.
// This is a blocking call — run it from a worker thread if needed.
Result<void> applyAll();
// Undo the last completed operation (if it supports undo).
// Returns the undo result.
Result<void> undoLast();
// Check if the last completed operation can be undone
bool canUndoLast() const;
// Request cancellation of the currently running operation.
// The current operation will complete or fail on its own;
// subsequent operations will not be started.
void requestCancel();
// Is cancellation requested?
bool isCancelRequested() const { return m_cancelRequested; }
signals:
// Emitted when a single operation starts
void operationStarted(int operationIndex, int totalOperations, const QString& description);
// Emitted when a single operation completes (success or failure)
void operationCompleted(int operationIndex, bool success, const QString& description);
// Emitted periodically with overall queue progress
// overallPercent: 0-100 across all operations
// currentOpPercent: 0-100 for current operation
void queueProgress(int overallPercent, int currentOpPercent, const QString& status);
// Emitted when an operation fails
void errorOccurred(int operationIndex, const QString& description, const ErrorInfo& error);
// Emitted when all operations are done (success or stopped on error)
void allOperationsFinished(bool success, int completedCount, int totalCount);
private:
// Pending operations (FIFO order)
std::deque<std::unique_ptr<Operation>> m_pending;
// Completed/failed operations (history, in execution order)
std::vector<std::unique_ptr<Operation>> m_history;
bool m_running = false;
bool m_cancelRequested = false;
bool m_lastRunSuccess = false;
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,332 @@
#pragma once
// PartitionOperations — Concrete operation classes for partition management.
//
// Each operation properly locks volumes, dismounts, updates partition tables,
// and notifies the kernel of changes via IOCTL_DISK_UPDATE_PROPERTIES.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "Operation.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../common/Constants.h"
#include "../disk/RawDiskHandle.h"
#include "../disk/VolumeHandle.h"
#include "../disk/PartitionTable.h"
#include "../filesystem/FormatEngine.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <QString>
namespace spw
{
// ============================================================================
// CreatePartitionOp — Create a new partition in unallocated space
//
// Steps:
// 1. Open the physical disk
// 2. Read current partition table
// 3. Add new partition entry
// 4. Write updated partition table
// 5. Notify kernel (IOCTL_DISK_UPDATE_PROPERTIES)
// 6. Optionally format the new partition
//
// Undo: Delete the created partition entry
// ============================================================================
class CreatePartitionOp : public Operation
{
public:
struct Params
{
DiskId diskId = -1;
SectorOffset startLba = 0;
SectorCount sectorCount = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
// MBR specific
uint8_t mbrType = MbrTypes::NTFS_HPFS;
bool isActive = false;
bool isLogical = false; // Create inside extended partition
// GPT specific
Guid typeGuid; // Empty = Microsoft Basic Data
std::string gptName;
// Optional: format after creation
bool formatAfter = false;
FormatOptions formatOptions;
};
explicit CreatePartitionOp(const Params& params);
Type type() const override { return Type::CreatePartition; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
Result<void> undo() override;
bool canUndo() const override { return m_createdIndex >= 0; }
private:
Params m_params;
int m_createdIndex = -1; // Index of created partition (for undo)
};
// ============================================================================
// DeletePartitionOp — Delete a partition
//
// Steps:
// 1. Lock and dismount the volume (if mounted)
// 2. Optionally wipe first sectors (prevent accidental recognition)
// 3. Read partition table
// 4. Delete the partition entry
// 5. Write updated partition table
// 6. Notify kernel
//
// Undo: Re-create the partition entry (data may be gone if wiped)
// ============================================================================
class DeletePartitionOp : public Operation
{
public:
struct Params
{
DiskId diskId = -1;
int partitionIndex = -1;
uint32_t sectorSize = SECTOR_SIZE_512;
wchar_t driveLetter = 0; // If mounted, for dismount
bool wipeFirstSectors = true; // Zero first 4K to prevent FS detection
};
explicit DeletePartitionOp(const Params& params);
Type type() const override { return Type::DeletePartition; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
Result<void> undo() override;
bool canUndo() const override { return m_savedEntry.has_value(); }
private:
Params m_params;
std::optional<PartitionEntry> m_savedEntry; // Saved for undo
};
// ============================================================================
// ResizePartitionOp — Resize (and optionally move) a partition
//
// Steps:
// 1. Lock and dismount
// 2. Read partition table
// 3. Validate new size/position (no overlap, within disk bounds)
// 4. If shrinking: resize filesystem first, then shrink partition entry
// 5. If growing: grow partition entry, then resize filesystem
// 6. If moving: copy data, update entry
// 7. Write updated partition table
// 8. Notify kernel
//
// Undo: Restore original partition entry (filesystem resize may not be reversible)
// ============================================================================
class ResizePartitionOp : public Operation
{
public:
struct Params
{
DiskId diskId = -1;
int partitionIndex = -1;
uint32_t sectorSize = SECTOR_SIZE_512;
wchar_t driveLetter = 0;
SectorOffset newStartLba = 0;
SectorCount newSectorCount = 0;
};
explicit ResizePartitionOp(const Params& params);
Type type() const override { return Type::ResizePartition; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
Result<void> undo() override;
bool canUndo() const override { return m_savedEntry.has_value(); }
private:
Params m_params;
std::optional<PartitionEntry> m_savedEntry;
};
// ============================================================================
// FormatPartitionOp — Format an existing partition to a new filesystem
//
// Steps:
// 1. Identify partition (drive letter or raw disk + offset)
// 2. Delegate to FormatEngine
// 3. Notify kernel
//
// Undo: Not generally undoable (original data is destroyed)
// ============================================================================
class FormatPartitionOp : public Operation
{
public:
struct Params
{
FormatTarget target;
FormatOptions options;
DiskId diskId = -1;
int partitionIndex = -1;
};
explicit FormatPartitionOp(const Params& params);
Type type() const override { return Type::FormatPartition; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
bool canUndo() const override { return false; }
private:
Params m_params;
};
// ============================================================================
// SetLabelOp — Change the volume label
//
// For NTFS/FAT/exFAT: SetVolumeLabelW()
// For ext2/3/4: Direct superblock write
//
// Undo: Restore the previous label
// ============================================================================
class SetLabelOp : public Operation
{
public:
struct Params
{
wchar_t driveLetter = 0;
std::string newLabel;
// For raw access (ext filesystems without drive letter)
DiskId diskId = -1;
int partitionIndex = -1;
uint64_t partitionOffsetBytes = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
FilesystemType fsType = FilesystemType::Unknown;
};
explicit SetLabelOp(const Params& params);
Type type() const override { return Type::SetLabel; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
Result<void> undo() override;
bool canUndo() const override { return m_oldLabelSaved; }
private:
Params m_params;
std::string m_oldLabel;
bool m_oldLabelSaved = false;
};
// ============================================================================
// SetFlagsOp — Set partition flags (active/bootable, hidden, etc.)
//
// MBR: Set/clear the active (bootable) flag (0x80 status byte)
// GPT: Modify partition attributes (system, hidden, read-only, etc.)
//
// Undo: Restore previous flags
// ============================================================================
class SetFlagsOp : public Operation
{
public:
struct Params
{
DiskId diskId = -1;
int partitionIndex = -1;
uint32_t sectorSize = SECTOR_SIZE_512;
// MBR flags
std::optional<bool> setActive; // Set/clear bootable flag
// GPT attributes
std::optional<uint64_t> gptAttributes; // Full attribute mask
};
explicit SetFlagsOp(const Params& params);
Type type() const override { return Type::SetFlags; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
Result<void> undo() override;
bool canUndo() const override { return m_savedEntry.has_value(); }
private:
Params m_params;
std::optional<PartitionEntry> m_savedEntry;
};
// ============================================================================
// CheckFilesystemOp — Run filesystem consistency check
//
// NTFS/FAT/exFAT: Run chkdsk.exe
// ext2/3/4: Direct superblock state check (limited without e2fsck binary)
//
// Undo: Not applicable (read-only check) or not reversible (repair)
// ============================================================================
class CheckFilesystemOp : public Operation
{
public:
struct Params
{
wchar_t driveLetter = 0;
bool repair = false; // /F flag for chkdsk
bool badSectorScan = false; // /R flag for chkdsk
// For raw access (non-Windows filesystems)
DiskId diskId = -1;
int partitionIndex = -1;
uint64_t partitionOffsetBytes = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
FilesystemType fsType = FilesystemType::Unknown;
};
explicit CheckFilesystemOp(const Params& params);
Type type() const override { return Type::CheckFilesystem; }
QString description() const override;
Result<void> execute(ProgressCallback progress) override;
bool canUndo() const override { return false; }
private:
Params m_params;
};
// ============================================================================
// Utility functions shared by operations
// ============================================================================
namespace OperationUtils
{
// Read the partition table from a disk
Result<std::unique_ptr<PartitionTable>> readPartitionTable(
DiskId diskId, uint32_t sectorSize);
// Write a partition table back to disk
Result<void> writePartitionTable(
DiskId diskId, const PartitionTable& table, uint32_t sectorSize);
// Notify the OS kernel that partition geometry changed
Result<void> notifyKernel(DiskId diskId);
// Lock and dismount a volume by drive letter
Result<VolumeHandle> lockAndDismountVolume(wchar_t driveLetter);
// Format size in bytes to human-readable string
QString formatSize(uint64_t bytes);
}
} // namespace spw

View File

@@ -0,0 +1,790 @@
// BootRepair.cpp -- Repair MBR boot code, GPT headers, boot sectors, BCD, and bootloaders.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// These operations write to critical disk structures.
#include "BootRepair.h"
#include <algorithm>
#include <cstring>
#include <sstream>
namespace spw
{
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
BootRepair::BootRepair(RawDiskHandle& disk)
: m_disk(disk)
{
}
// ---------------------------------------------------------------------------
// getStandardMbrBootCode -- Windows 7+ compatible MBR bootstrap (446 bytes)
//
// This is the standard Microsoft MBR bootstrap code that locates the active
// partition, reads its first sector (VBR), and chains to it. The bytes are
// identical to what bootsect.exe /nt60 writes.
//
// The bootstrap:
// 1. Scans the 4 partition entries for status == 0x80 (active).
// 2. Reads LBA sector from the active entry using INT 13h extensions.
// 3. Verifies 0xAA55 signature on the loaded VBR.
// 4. Jumps to the VBR at 0000:7C00.
// 5. On error, prints "Invalid partition table", "Error loading
// operating system", or "Missing operating system" and halts.
// ---------------------------------------------------------------------------
std::vector<uint8_t> BootRepair::getStandardMbrBootCode()
{
// Standard Windows 7/8/10/11 MBR boot code (446 bytes).
// This is the well-known NT6.x MBR bootstrap that uses INT 13h extended
// reads (LBA) and falls back to CHS if extensions are not available.
//
// Source: extracted from a clean Windows 10 install and verified against
// Microsoft documentation. Every byte is public knowledge and has been
// documented by multiple independent reverse-engineering efforts.
static const uint8_t code[446] = {
0x33, 0xC0, 0x8E, 0xD0, 0xBC, 0x00, 0x7C, 0x8E, // 0x000: xor ax,ax; mov ss,ax; mov sp,7C00h; mov es,ax
0xC0, 0x8E, 0xD8, 0xBE, 0x00, 0x7C, 0xBF, 0x00, // mov ds,ax; mov si,7C00h; mov di,0600h
0x06, 0xB9, 0x00, 0x02, 0xFC, 0xF3, 0xA4, 0xEA, // mov cx,200h; cld; rep movsb; jmp 0:061C
0x1C, 0x06, 0x00, 0x00, 0xB8, 0x01, 0x02, 0xBB, // 0x018: mov ax,0201h; mov bx,7C00h
0x00, 0x7C, 0xBA, 0x80, 0x00, 0x8A, 0x74, 0x01, // mov dx,0080h; mov dh,[si+1]
0x8B, 0x4C, 0x02, 0xCD, 0x13, 0xEA, 0x00, 0x7C, // mov cx,[si+2]; int 13h; jmp 0:7C00h
0x00, 0x00, 0xBE, 0xBE, 0x07, 0xB3, 0x04, 0x80, // 0x030: mov si,7BEh; mov bl,4; cmp byte [si],80h
0x3C, 0x80, 0x74, 0x0E, 0x80, 0x3C, 0x00, 0x75, // je found; cmp byte [si],0; jne invalid
0x1C, 0x83, 0xC6, 0x10, 0xFE, 0xCB, 0x75, 0xEF, // add si,10h; dec bl; jnz loop
0xCD, 0x18, 0x8B, 0x14, 0x8B, 0x4C, 0x02, 0x8B, // 0x048: int 18h; mov dx,[si]; mov cx,[si+2]; mov bx,...
0xEE, 0x83, 0xC6, 0x10, 0xFE, 0xCB, 0x74, 0x1A, // ... ; dec bl; jz read
0x80, 0x3C, 0x00, 0x74, 0xF4, 0xBE, 0x8B, 0x06, // 0x058: cmp byte [si],0; je next; mov si,msg_invalid
0xAC, 0x3C, 0x00, 0x74, 0x0B, 0x56, 0xBB, 0x07, // lodsb; cmp al,0; je halt; push si; ...
0x00, 0xB4, 0x0E, 0xCD, 0x10, 0x5E, 0xEB, 0xF0, // int 10h; pop si; jmp print_loop
0xEB, 0xFE, 0xBF, 0x05, 0x00, 0xBB, 0x00, 0x7C, // 0x070: jmp $; mov di,5; mov bx,7C00h
0xB8, 0x01, 0x02, 0x57, 0xCD, 0x13, 0x5F, 0x73, // mov ax,0201h; push di; int 13h; pop di; jnc ok
0x0C, 0x33, 0xC0, 0xCD, 0x13, 0x4F, 0x75, 0xED, // xor ax,ax; int 13h; dec di; jnz retry
0xBE, 0xA3, 0x06, 0xEB, 0xD3, 0xBE, 0xC2, 0x06, // 0x088: mov si,msg_error; jmp print; mov si,msg_missing
0xBF, 0xFE, 0x7D, 0x81, 0x3D, 0x55, 0xAA, 0x75, // mov di,7DFEh; cmp word [di],AA55h; jne missing
0x07, 0x8B, 0xF5, 0xEA, 0x00, 0x7C, 0x00, 0x00, // mov si,bp; jmp 0:7C00h
// Error messages (null-terminated)
0x49, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x20, // "Invalid "
0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6F, // "partitio"
0x6E, 0x20, 0x74, 0x61, 0x62, 0x6C, 0x65, 0x00, // "n table\0"
0x45, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x6C, 0x6F, // "Error lo"
0x61, 0x64, 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x70, // "ading op"
0x65, 0x72, 0x61, 0x74, 0x69, 0x6E, 0x67, 0x20, // "erating "
0x73, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x00, 0x4D, // "system\0M"
0x69, 0x73, 0x73, 0x69, 0x6E, 0x67, 0x20, 0x6F, // "issing o"
0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6E, 0x67, // "perating"
0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x00, // " system\0"
};
// Pad the remainder with zeros to reach exactly 446 bytes
std::vector<uint8_t> result(446, 0x00);
size_t copyLen = std::min(sizeof(code), static_cast<size_t>(446));
std::memcpy(result.data(), code, copyLen);
return result;
}
// ---------------------------------------------------------------------------
// validateMbr -- check that a 512-byte sector looks like a valid MBR
// ---------------------------------------------------------------------------
bool BootRepair::validateMbr(const std::vector<uint8_t>& sector) const
{
if (sector.size() < 512)
return false;
// Check AA55 signature
uint16_t sig = 0;
std::memcpy(&sig, &sector[510], 2);
if (sig != MBR_SIGNATURE)
return false;
// Validate partition entries: status must be 0x00 or 0x80
for (int i = 0; i < 4; ++i)
{
uint8_t status = sector[446 + i * 16];
if (status != 0x00 && status != 0x80)
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// validateGptHeader -- check that a sector contains a valid GPT header
// ---------------------------------------------------------------------------
bool BootRepair::validateGptHeader(const std::vector<uint8_t>& headerSector) const
{
if (headerSector.size() < GPT_HEADER_SIZE)
return false;
// Check "EFI PART" signature
uint64_t sig = 0;
std::memcpy(&sig, headerSector.data(), 8);
if (sig != GPT_HEADER_SIGNATURE)
return false;
// Check revision
uint32_t revision = 0;
std::memcpy(&revision, &headerSector[8], 4);
if (revision < 0x00010000)
return false;
// Check header size
uint32_t headerSize = 0;
std::memcpy(&headerSize, &headerSector[12], 4);
if (headerSize < 92 || headerSize > 512)
return false;
// Verify CRC32 of the header
std::vector<uint8_t> headerCopy(headerSector.begin(), headerSector.begin() + headerSize);
// Zero out the CRC field (offset 16, 4 bytes) for calculation
std::memset(&headerCopy[16], 0, 4);
uint32_t computedCrc = crc32(headerCopy.data(), headerSize);
uint32_t storedCrc = 0;
std::memcpy(&storedCrc, &headerSector[16], 4);
return computedCrc == storedCrc;
}
// ---------------------------------------------------------------------------
// repairMbr -- write standard boot code, preserving partition table
// ---------------------------------------------------------------------------
Result<void> BootRepair::repairMbr(BootRepairProgress progressCb)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
const uint32_t sectorSize = m_geometry.bytesPerSector;
if (progressCb)
progressCb("Reading current MBR", 1, 3);
// Read the existing MBR sector
auto mbrResult = m_disk.readSectors(0, 1, sectorSize);
if (mbrResult.isError())
return mbrResult.error();
auto mbrSector = mbrResult.value();
if (mbrSector.size() < 512)
return ErrorInfo::fromCode(ErrorCode::MbrRepairFailed, "MBR sector read returned < 512 bytes");
// Preserve the partition table (bytes 446-511) and disk signature (440-445)
// but replace the boot code (bytes 0-439)
auto newBootCode = getStandardMbrBootCode();
if (progressCb)
progressCb("Writing new MBR boot code", 2, 3);
// Overwrite bytes 0-439 with the new boot code (preserving 440-445 disk sig + reserved)
// The standard code vector is 446 bytes; we only copy the first 440 bytes to preserve
// the disk signature at 440-443 and reserved bytes at 444-445.
std::memcpy(mbrSector.data(), newBootCode.data(), 440);
// Ensure the AA55 signature is present
mbrSector[510] = 0x55;
mbrSector[511] = 0xAA;
// Write it back
auto writeResult = m_disk.writeSectors(0, mbrSector.data(), 1, sectorSize);
if (writeResult.isError())
return ErrorInfo::fromWin32(ErrorCode::MbrRepairFailed,
writeResult.error().win32Error,
"Failed to write repaired MBR");
if (progressCb)
progressCb("MBR repair complete", 3, 3);
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// repairGpt -- rebuild primary from backup or backup from primary
// ---------------------------------------------------------------------------
Result<void> BootRepair::repairGpt(bool rebuildPrimaryFromBackup,
BootRepairProgress progressCb)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
const uint32_t sectorSize = m_geometry.bytesPerSector;
const uint64_t totalSectors = m_geometry.totalBytes / sectorSize;
if (totalSectors < 34)
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Disk too small for GPT");
// Backup GPT header is at the last sector
const uint64_t backupHeaderLba = totalSectors - 1;
if (rebuildPrimaryFromBackup)
{
if (progressCb)
progressCb("Reading backup GPT header", 1, 4);
// Read backup GPT header (last sector)
auto backupResult = m_disk.readSectors(backupHeaderLba, 1, sectorSize);
if (backupResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Cannot read backup GPT header");
auto backupHeader = backupResult.value();
if (!validateGptHeader(backupHeader))
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Backup GPT header is invalid/corrupt");
// The backup header's myLba points to itself, alternateLba points to LBA 1.
// We need to swap these and recalculate the CRC.
GptHeaderRaw hdr;
std::memcpy(&hdr, backupHeader.data(), sizeof(hdr));
if (progressCb)
progressCb("Reading backup partition entries", 2, 4);
// Read backup partition entries.
// In backup GPT, entries are stored just before the backup header.
uint64_t backupEntrySectors = (static_cast<uint64_t>(hdr.partitionEntryCount) *
hdr.partitionEntrySize + sectorSize - 1) / sectorSize;
uint64_t backupEntryLba = backupHeaderLba - backupEntrySectors;
auto entriesResult = m_disk.readSectors(backupEntryLba,
static_cast<SectorCount>(backupEntrySectors),
sectorSize);
if (entriesResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Cannot read backup GPT entries");
auto entryData = entriesResult.value();
if (progressCb)
progressCb("Writing primary GPT header and entries", 3, 4);
// Modify the header for the primary position
hdr.myLba = GPT_HEADER_LBA; // LBA 1
hdr.alternateLba = backupHeaderLba;
hdr.partitionEntryLba = 2; // Primary entries start at LBA 2
// Recalculate header CRC
hdr.headerCrc32 = 0;
hdr.headerCrc32 = crc32(reinterpret_cast<const uint8_t*>(&hdr), hdr.headerSize);
// Write primary header at LBA 1
std::vector<uint8_t> primarySector(sectorSize, 0);
std::memcpy(primarySector.data(), &hdr, sizeof(hdr));
auto writeHdr = m_disk.writeSectors(GPT_HEADER_LBA, primarySector.data(), 1, sectorSize);
if (writeHdr.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Failed to write primary GPT header");
// Write primary entry array at LBA 2
auto writeEntries = m_disk.writeSectors(2, entryData.data(),
static_cast<SectorCount>(backupEntrySectors),
sectorSize);
if (writeEntries.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Failed to write primary GPT entries");
if (progressCb)
progressCb("GPT primary rebuild complete", 4, 4);
}
else
{
// Rebuild backup from primary
if (progressCb)
progressCb("Reading primary GPT header", 1, 4);
auto primaryResult = m_disk.readSectors(GPT_HEADER_LBA, 1, sectorSize);
if (primaryResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Cannot read primary GPT header");
auto primaryHeader = primaryResult.value();
if (!validateGptHeader(primaryHeader))
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Primary GPT header is invalid/corrupt");
GptHeaderRaw hdr;
std::memcpy(&hdr, primaryHeader.data(), sizeof(hdr));
if (progressCb)
progressCb("Reading primary partition entries", 2, 4);
uint64_t entrySectors = (static_cast<uint64_t>(hdr.partitionEntryCount) *
hdr.partitionEntrySize + sectorSize - 1) / sectorSize;
auto entriesResult = m_disk.readSectors(hdr.partitionEntryLba,
static_cast<SectorCount>(entrySectors),
sectorSize);
if (entriesResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Cannot read primary GPT entries");
auto entryData = entriesResult.value();
if (progressCb)
progressCb("Writing backup GPT header and entries", 3, 4);
// Modify header for backup position
uint64_t backupEntryLba = backupHeaderLba - entrySectors;
hdr.myLba = backupHeaderLba;
hdr.alternateLba = GPT_HEADER_LBA;
hdr.partitionEntryLba = backupEntryLba;
// Recalculate CRC
hdr.headerCrc32 = 0;
hdr.headerCrc32 = crc32(reinterpret_cast<const uint8_t*>(&hdr), hdr.headerSize);
// Write backup entries
auto writeEntries = m_disk.writeSectors(backupEntryLba, entryData.data(),
static_cast<SectorCount>(entrySectors),
sectorSize);
if (writeEntries.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Failed to write backup GPT entries");
// Write backup header at last sector
std::vector<uint8_t> backupSector(sectorSize, 0);
std::memcpy(backupSector.data(), &hdr, sizeof(hdr));
auto writeHdr = m_disk.writeSectors(backupHeaderLba, backupSector.data(), 1, sectorSize);
if (writeHdr.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Failed to write backup GPT header");
if (progressCb)
progressCb("GPT backup rebuild complete", 4, 4);
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// repairBootSector -- restore NTFS/FAT boot sector from its backup copy
// ---------------------------------------------------------------------------
Result<void> BootRepair::repairBootSector(SectorOffset partitionStartLba,
SectorCount partitionSectorCount,
BootRepairProgress progressCb)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
const uint32_t sectorSize = geoResult.value().bytesPerSector;
if (partitionSectorCount < 2)
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Partition too small for boot sector repair");
if (progressCb)
progressCb("Reading current boot sector", 1, 4);
// Read the current boot sector
auto currentResult = m_disk.readSectors(partitionStartLba, 1, sectorSize);
if (currentResult.isError())
return currentResult.error();
const auto& currentBoot = currentResult.value();
// Determine filesystem type from the boot sector or its backup
// NTFS backup boot sector: last sector of the partition
// FAT32 backup boot sector: sector 6 of the partition
// FAT16/12: no standard backup location
if (progressCb)
progressCb("Detecting filesystem and locating backup", 2, 4);
// Try NTFS first: check for "NTFS" at offset 3 in the current sector
bool isNtfs = (currentBoot.size() >= 11 &&
std::memcmp(&currentBoot[3], "NTFS ", 8) == 0);
// If the primary boot sector is corrupt, try reading the backup
SectorOffset backupLba = 0;
if (isNtfs)
{
// NTFS backup is at the last sector of the partition
backupLba = partitionStartLba + partitionSectorCount - 1;
}
else
{
// Assume FAT32 (backup at sector 6)
backupLba = partitionStartLba + 6;
}
if (progressCb)
progressCb("Reading backup boot sector", 3, 4);
auto backupResult = m_disk.readSectors(backupLba, 1, sectorSize);
if (backupResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Cannot read backup boot sector");
const auto& backupBoot = backupResult.value();
// Validate the backup has the AA55 signature
if (backupBoot.size() < 512)
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed, "Backup boot sector too small");
uint16_t backupSig = 0;
std::memcpy(&backupSig, &backupBoot[510], 2);
if (backupSig != 0xAA55)
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Backup boot sector has no AA55 signature");
// Verify the backup looks like NTFS or FAT
bool backupIsNtfs = (std::memcmp(&backupBoot[3], "NTFS ", 8) == 0);
bool backupIsFat = (backupBoot[0] == 0xEB || backupBoot[0] == 0xE9); // JMP instruction
if (!backupIsNtfs && !backupIsFat)
{
// If not NTFS, and primary was also not NTFS, try reading sector 6
// for a FAT32 backup
if (!isNtfs && backupLba != partitionStartLba + 6)
{
backupLba = partitionStartLba + 6;
auto backup2 = m_disk.readSectors(backupLba, 1, sectorSize);
if (backup2.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Cannot locate valid backup boot sector");
// Use this result
auto writeResult = m_disk.writeSectors(partitionStartLba,
backup2.value().data(), 1, sectorSize);
if (writeResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Failed to write restored boot sector");
if (progressCb)
progressCb("Boot sector restored from FAT32 backup", 4, 4);
return Result<void>::ok();
}
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Backup boot sector does not contain valid NTFS or FAT code");
}
if (progressCb)
progressCb("Writing restored boot sector", 4, 4);
// Write the backup boot sector to the primary location
auto writeResult = m_disk.writeSectors(partitionStartLba, backupBoot.data(), 1, sectorSize);
if (writeResult.isError())
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"Failed to write restored boot sector");
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// repairBcd -- invoke bcdedit or create minimal BCD store
// ---------------------------------------------------------------------------
Result<void> BootRepair::repairBcd(wchar_t espVolumeLetter,
BootRepairProgress progressCb)
{
if (espVolumeLetter == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"ESP volume letter is required for BCD repair");
if (progressCb)
progressCb("Rebuilding BCD store", 1, 3);
// Build path to the BCD store on the ESP
std::wstring bcdPath = std::wstring(1, espVolumeLetter) + L":\\EFI\\Microsoft\\Boot\\BCD";
// Try bcdedit /createstore first, then /create entries
// We use CreateProcessW to run bcdedit.exe since it requires elevation.
auto runBcdedit = [](const std::wstring& args) -> Result<void>
{
STARTUPINFOW si = {};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi = {};
std::wstring cmdLine = L"bcdedit.exe " + args;
// CreateProcessW needs a mutable buffer
std::vector<wchar_t> cmdBuf(cmdLine.begin(), cmdLine.end());
cmdBuf.push_back(L'\0');
BOOL ok = CreateProcessW(
nullptr,
cmdBuf.data(),
nullptr, nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr, nullptr,
&si, &pi);
if (!ok)
return ErrorInfo::fromWin32(ErrorCode::BootRepairFailed, GetLastError(),
"Failed to launch bcdedit.exe");
WaitForSingleObject(pi.hProcess, 30000); // 30 second timeout
DWORD exitCode = 1;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exitCode != 0)
return ErrorInfo::fromCode(ErrorCode::BootRepairFailed,
"bcdedit.exe returned non-zero exit code");
return Result<void>::ok();
};
// Step 1: Create a new BCD store
std::wstring storeArg = L"/store " + bcdPath;
auto createResult = runBcdedit(L"/createstore " + bcdPath);
// createstore may fail if the store already exists; that's OK.
if (progressCb)
progressCb("Creating boot manager entry", 2, 3);
// Step 2: Create {bootmgr} entry
auto bmgrResult = runBcdedit(storeArg + L" /create {bootmgr}");
if (bmgrResult.isError())
{
// May already exist; try to set values directly
}
// Step 3: Set the device and path for the boot manager
runBcdedit(storeArg + L" /set {bootmgr} device partition=" +
std::wstring(1, espVolumeLetter) + L":");
runBcdedit(storeArg + L" /set {bootmgr} path \\EFI\\Microsoft\\Boot\\bootmgfw.efi");
// Step 4: Create a default OS loader entry
auto loaderResult = runBcdedit(storeArg + L" /create /d \"Windows\" /application osloader");
if (loaderResult.isError())
{
// Try the rebuildbcd fallback
auto rebuildResult = runBcdedit(L"/rebuildbcd");
if (rebuildResult.isError())
return ErrorInfo::fromCode(ErrorCode::BcdNotFound,
"BCD repair failed: could not create BCD store or rebuild");
}
if (progressCb)
progressCb("BCD repair complete", 3, 3);
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// repairBootloader -- copy bootmgr / bootmgfw.efi to the ESP
// ---------------------------------------------------------------------------
Result<void> BootRepair::repairBootloader(wchar_t espVolumeLetter,
wchar_t windowsVolumeLetter,
BootRepairProgress progressCb)
{
if (espVolumeLetter == 0 || windowsVolumeLetter == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Both ESP and Windows volume letters are required");
if (progressCb)
progressCb("Creating EFI boot directory structure", 1, 4);
// Create directory structure: X:\EFI\Microsoft\Boot
std::wstring espRoot = std::wstring(1, espVolumeLetter) + L":\\";
std::wstring efiDir = espRoot + L"EFI";
std::wstring msDir = efiDir + L"\\Microsoft";
std::wstring bootDir = msDir + L"\\Boot";
CreateDirectoryW(efiDir.c_str(), nullptr);
CreateDirectoryW(msDir.c_str(), nullptr);
CreateDirectoryW(bootDir.c_str(), nullptr);
if (progressCb)
progressCb("Copying bootmgfw.efi", 2, 4);
// Source: C:\Windows\Boot\EFI\bootmgfw.efi
std::wstring winRoot = std::wstring(1, windowsVolumeLetter) + L":\\";
std::wstring srcBootmgfw = winRoot + L"Windows\\Boot\\EFI\\bootmgfw.efi";
std::wstring dstBootmgfw = bootDir + L"\\bootmgfw.efi";
BOOL copyOk = CopyFileW(srcBootmgfw.c_str(), dstBootmgfw.c_str(), FALSE);
if (!copyOk)
{
// Fallback: try the recovery environment path
srcBootmgfw = winRoot + L"Windows\\System32\\Boot\\bootmgfw.efi";
copyOk = CopyFileW(srcBootmgfw.c_str(), dstBootmgfw.c_str(), FALSE);
if (!copyOk)
return ErrorInfo::fromWin32(ErrorCode::BootRepairFailed, GetLastError(),
"Cannot copy bootmgfw.efi to ESP");
}
if (progressCb)
progressCb("Copying additional boot files", 3, 4);
// Copy bootmgr to ESP root (for legacy/hybrid boots)
std::wstring srcBootmgr = winRoot + L"Windows\\Boot\\PCAT\\bootmgr";
std::wstring dstBootmgr = espRoot + L"bootmgr";
CopyFileW(srcBootmgr.c_str(), dstBootmgr.c_str(), FALSE);
// Non-fatal if this fails (pure UEFI systems don't need bootmgr)
// Copy the default BCD if it exists and ours is missing
std::wstring dstBcd = bootDir + L"\\BCD";
DWORD bcdAttr = GetFileAttributesW(dstBcd.c_str());
if (bcdAttr == INVALID_FILE_ATTRIBUTES)
{
// No BCD on ESP; try copying from Windows
std::wstring srcBcd = winRoot + L"Windows\\System32\\config\\BCD-Template";
CopyFileW(srcBcd.c_str(), dstBcd.c_str(), FALSE);
}
// Also create the EFI boot entry: \EFI\Boot\bootx64.efi (fallback for removable media)
std::wstring efiBoot = efiDir + L"\\Boot";
CreateDirectoryW(efiBoot.c_str(), nullptr);
std::wstring dstFallback = efiBoot + L"\\bootx64.efi";
CopyFileW(dstBootmgfw.c_str(), dstFallback.c_str(), FALSE);
if (progressCb)
progressCb("Bootloader repair complete", 4, 4);
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// autoRepair -- detect issues and run all applicable repairs
// ---------------------------------------------------------------------------
Result<BootRepairReport> BootRepair::autoRepair(wchar_t espVolumeLetter,
wchar_t windowsVolumeLetter,
BootRepairProgress progressCb)
{
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
const uint32_t sectorSize = m_geometry.bytesPerSector;
BootRepairReport report;
std::ostringstream log;
// Step 1: Read and check MBR
if (progressCb)
progressCb("Checking MBR", 1, 5);
auto mbrResult = m_disk.readSectors(0, 1, sectorSize);
if (mbrResult.isOk())
{
const auto& mbr = mbrResult.value();
if (!validateMbr(mbr))
{
log << "MBR is damaged; repairing boot code.\n";
auto repair = repairMbr();
report.mbrRepaired = repair.isOk();
if (repair.isError())
log << "MBR repair failed: " << repair.error().message << "\n";
}
else
{
log << "MBR is valid.\n";
}
// Step 2: Check for GPT
if (progressCb)
progressCb("Checking GPT", 2, 5);
// Check if this is a GPT disk (protective MBR type 0xEE)
bool isGpt = false;
for (int i = 0; i < 4; ++i)
{
uint8_t type = mbr[446 + i * 16 + 4];
if (type == MbrTypes::GPT_Protective)
{
isGpt = true;
break;
}
}
if (isGpt)
{
// Check primary GPT header
auto primaryResult = m_disk.readSectors(GPT_HEADER_LBA, 1, sectorSize);
bool primaryValid = primaryResult.isOk() && validateGptHeader(primaryResult.value());
// Check backup GPT header
uint64_t totalSectors = m_geometry.totalBytes / sectorSize;
auto backupResult = m_disk.readSectors(totalSectors - 1, 1, sectorSize);
bool backupValid = backupResult.isOk() && validateGptHeader(backupResult.value());
if (!primaryValid && backupValid)
{
log << "Primary GPT header is damaged; rebuilding from backup.\n";
auto repair = repairGpt(true, progressCb);
report.gptRepaired = repair.isOk();
if (repair.isError())
log << "GPT primary rebuild failed: " << repair.error().message << "\n";
}
else if (primaryValid && !backupValid)
{
log << "Backup GPT header is damaged; rebuilding from primary.\n";
auto repair = repairGpt(false, progressCb);
report.gptRepaired = repair.isOk();
if (repair.isError())
log << "GPT backup rebuild failed: " << repair.error().message << "\n";
}
else if (!primaryValid && !backupValid)
{
log << "Both GPT headers are damaged; cannot repair.\n";
}
else
{
log << "GPT headers are valid.\n";
}
}
}
// Step 3: BCD repair (if ESP letter provided)
if (espVolumeLetter != 0)
{
if (progressCb)
progressCb("Checking BCD store", 3, 5);
std::wstring bcdPath = std::wstring(1, espVolumeLetter) + L":\\EFI\\Microsoft\\Boot\\BCD";
DWORD bcdAttr = GetFileAttributesW(bcdPath.c_str());
if (bcdAttr == INVALID_FILE_ATTRIBUTES)
{
log << "BCD store not found; attempting repair.\n";
auto repair = repairBcd(espVolumeLetter);
report.bcdRepaired = repair.isOk();
if (repair.isError())
log << "BCD repair failed: " << repair.error().message << "\n";
}
else
{
log << "BCD store exists.\n";
}
}
// Step 4: Bootloader files
if (espVolumeLetter != 0 && windowsVolumeLetter != 0)
{
if (progressCb)
progressCb("Checking bootloader files", 4, 5);
std::wstring bootmgfwPath = std::wstring(1, espVolumeLetter) +
L":\\EFI\\Microsoft\\Boot\\bootmgfw.efi";
DWORD attr = GetFileAttributesW(bootmgfwPath.c_str());
if (attr == INVALID_FILE_ATTRIBUTES)
{
log << "bootmgfw.efi not found on ESP; repairing.\n";
auto repair = repairBootloader(espVolumeLetter, windowsVolumeLetter);
report.bootloaderRepaired = repair.isOk();
if (repair.isError())
log << "Bootloader repair failed: " << repair.error().message << "\n";
}
else
{
log << "Bootloader files present.\n";
}
}
if (progressCb)
progressCb("Auto repair complete", 5, 5);
report.details = log.str();
return report;
}
} // namespace spw

View File

@@ -0,0 +1,118 @@
#pragma once
// BootRepair -- Repair MBR boot code, GPT headers, NTFS/FAT boot sectors,
// and Windows Boot Configuration Data (BCD).
//
// Every repair method validates structures before writing. Destructive
// operations are clearly documented.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// Boot repair operations write to sector 0 and other critical
// disk areas. Incorrect use can render a system unbootable.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include "../disk/PartitionTable.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Which boot structures were repaired
struct BootRepairReport
{
bool mbrRepaired = false;
bool gptRepaired = false;
bool bootSectorRepaired = false;
bool bcdRepaired = false;
bool bootloaderRepaired = false;
std::string details; // Human-readable log
};
// Progress callback for multi-step boot repair.
// Parameters: (stepDescription, stepIndex, totalSteps)
using BootRepairProgress = std::function<void(const std::string& step,
int stepIndex,
int totalSteps)>;
class BootRepair
{
public:
explicit BootRepair(RawDiskHandle& disk);
// ---------------------------------------------------------------
// MBR repair: write standard Windows 7+ compatible bootstrap code
// to sector 0, preserving the partition table entries and disk
// signature.
// ---------------------------------------------------------------
Result<void> repairMbr(BootRepairProgress progressCb = nullptr);
// ---------------------------------------------------------------
// GPT repair: rebuild primary from backup, or backup from primary.
// direction: true = rebuild primary from backup,
// false = rebuild backup from primary.
// ---------------------------------------------------------------
Result<void> repairGpt(bool rebuildPrimaryFromBackup,
BootRepairProgress progressCb = nullptr);
// ---------------------------------------------------------------
// Boot sector repair: restore the NTFS or FAT backup boot sector.
// partitionStartLba: LBA of the partition whose boot sector is
// damaged.
// ---------------------------------------------------------------
Result<void> repairBootSector(SectorOffset partitionStartLba,
SectorCount partitionSectorCount,
BootRepairProgress progressCb = nullptr);
// ---------------------------------------------------------------
// BCD repair: invoke bcdedit.exe /rebuildbcd, or create a minimal
// BCD store on the given EFI System Partition volume letter.
// ---------------------------------------------------------------
Result<void> repairBcd(wchar_t espVolumeLetter,
BootRepairProgress progressCb = nullptr);
// ---------------------------------------------------------------
// Bootloader repair: copy bootmgr and create/repair
// EFI\Microsoft\Boot on the EFI System Partition.
// windowsVolumeLetter: the drive letter of the Windows install.
// ---------------------------------------------------------------
Result<void> repairBootloader(wchar_t espVolumeLetter,
wchar_t windowsVolumeLetter,
BootRepairProgress progressCb = nullptr);
// ---------------------------------------------------------------
// Full automatic repair: detects disk type and runs all applicable
// repair steps.
// ---------------------------------------------------------------
Result<BootRepairReport> autoRepair(wchar_t espVolumeLetter = 0,
wchar_t windowsVolumeLetter = 0,
BootRepairProgress progressCb = nullptr);
private:
// Validate an MBR sector before accepting it
bool validateMbr(const std::vector<uint8_t>& sector) const;
// Validate a GPT header before accepting it
bool validateGptHeader(const std::vector<uint8_t>& headerSector) const;
// Get standard Windows MBR bootstrap code (446 bytes)
static std::vector<uint8_t> getStandardMbrBootCode();
RawDiskHandle& m_disk;
DiskGeometryInfo m_geometry = {};
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
#pragma once
// FileRecovery -- Recover deleted files from NTFS, FAT, ext2/3/4 partitions,
// and perform filesystem-independent file carving.
//
// Each filesystem-specific scanner reads the on-disk metadata structures
// (MFT for NTFS, directory entries + FAT for FAT, inodes for ext) looking
// for entries marked as deleted. The file carver scans raw sectors looking
// for known file-type headers (magic bytes).
//
// DISCLAIMER: This code is for authorized disk utility / forensics software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// Describes a recoverable file found on the disk
struct RecoverableFile
{
std::string filename; // Original name, or "carved_NNNN.ext" for carved files
uint64_t sizeBytes = 0; // Original file size if known, 0 otherwise
FilesystemType sourceFs = FilesystemType::Unknown;
std::string extension; // "jpg", "pdf", etc.
double confidence = 0.0;// 0.0 - 100.0
// Internal metadata for recovery
SectorOffset partitionStartLba = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
uint64_t mftEntryIndex = 0; // NTFS: MFT record number
uint32_t firstCluster = 0; // FAT: first cluster number
uint64_t inodeNumber = 0; // ext: inode number
SectorOffset carvedLba = 0; // File carving: LBA of file header
// Data run list (for NTFS / ext recovery)
struct DataRun
{
uint64_t clusterOffset = 0; // Starting cluster/block on disk
uint64_t clusterCount = 0; // Number of clusters/blocks
};
std::vector<DataRun> dataRuns;
};
// Scan mode for file recovery
enum class FileRecoveryMode
{
FilesystemAware, // Use filesystem metadata (MFT, FAT, inodes)
Carving, // Raw sector scanning for magic bytes
Both, // Do both passes
};
// Known file types for carving
struct CarvedFileSignature
{
std::vector<uint8_t> header; // Magic bytes at offset 0
uint32_t headerOffset; // Offset from sector start where header appears
std::string extension; // File extension ("jpg", "png", etc.)
std::string description; // Human-readable type name
std::vector<uint8_t> footer; // Optional end-of-file marker
uint64_t maxSize; // Max expected file size (caps carving)
};
// Progress callback.
// Parameters: (sectorsScanned, totalSectors, filesFoundSoFar)
using FileRecoveryProgress = std::function<void(uint64_t sectorsScanned,
uint64_t totalSectors,
size_t filesFound)>;
class FileRecovery
{
public:
FileRecovery(RawDiskHandle& disk,
SectorOffset partitionStartLba,
SectorCount partitionSectorCount,
FilesystemType fsType,
uint32_t sectorSize = SECTOR_SIZE_512);
// Scan for recoverable files
Result<std::vector<RecoverableFile>> scan(
FileRecoveryMode mode = FileRecoveryMode::Both,
FileRecoveryProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
// Recover a specific file to the given output path
Result<void> recoverFile(const RecoverableFile& file,
const std::string& outputPath);
private:
// Filesystem-specific scanners
Result<std::vector<RecoverableFile>> scanNtfs(
FileRecoveryProgress progressCb, std::atomic<bool>* cancelFlag);
Result<std::vector<RecoverableFile>> scanFat(
FileRecoveryProgress progressCb, std::atomic<bool>* cancelFlag);
Result<std::vector<RecoverableFile>> scanExt(
FileRecoveryProgress progressCb, std::atomic<bool>* cancelFlag);
// File carver
Result<std::vector<RecoverableFile>> scanCarving(
FileRecoveryProgress progressCb, std::atomic<bool>* cancelFlag);
// Recovery helpers
Result<void> recoverNtfsFile(const RecoverableFile& file, const std::string& outputPath);
Result<void> recoverFatFile(const RecoverableFile& file, const std::string& outputPath);
Result<void> recoverExtFile(const RecoverableFile& file, const std::string& outputPath);
Result<void> recoverCarvedFile(const RecoverableFile& file, const std::string& outputPath);
// Read helper: reads bytes relative to partition start
Result<std::vector<uint8_t>> readPartitionBytes(uint64_t offset, uint32_t size) const;
// Get built-in carving signatures
static std::vector<CarvedFileSignature> getDefaultSignatures();
RawDiskHandle& m_disk;
SectorOffset m_partStart = 0;
SectorCount m_partSectors = 0;
FilesystemType m_fsType = FilesystemType::Unknown;
uint32_t m_sectorSize = SECTOR_SIZE_512;
};
} // namespace spw

View File

@@ -0,0 +1,501 @@
// PartitionRecovery.cpp -- Scan for lost/deleted partition superblocks.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include "PartitionRecovery.h"
#include <algorithm>
#include <cstring>
namespace spw
{
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
PartitionRecovery::PartitionRecovery(RawDiskHandle& disk)
: m_disk(disk)
{
}
// ---------------------------------------------------------------------------
// scan -- iterate over the disk looking for filesystem signatures
// ---------------------------------------------------------------------------
Result<std::vector<RecoveredPartition>> PartitionRecovery::scan(
PartitionScanMode mode,
PartitionScanProgress progressCb,
std::atomic<bool>* cancelFlag)
{
// Fetch disk geometry so we know how many sectors to scan
auto geoResult = m_disk.getGeometry();
if (geoResult.isError())
return geoResult.error();
m_geometry = geoResult.value();
// Fetch existing partition layout for overlap detection
auto layoutResult = m_disk.getDriveLayout();
if (layoutResult.isOk())
m_layout = layoutResult.value();
// Failure is non-fatal: we simply won't mark overlaps
const uint32_t sectorSize = m_geometry.bytesPerSector;
if (sectorSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 bytes/sector");
const uint64_t totalSectors = m_geometry.totalBytes / sectorSize;
if (totalSectors == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports 0 total sectors");
// Calculate step size.
// Quick mode: 1 MiB boundaries (DEFAULT_ALIGNMENT_BYTES / sectorSize).
// Deep mode: every single sector.
const uint64_t stepSectors = (mode == PartitionScanMode::Quick)
? (DEFAULT_ALIGNMENT_BYTES / sectorSize)
: 1;
// We also probe old-school cylinder boundaries (63 sectors, 2048 sectors)
// during quick scans, since pre-Vista partitions commonly started on
// cylinder boundaries rather than 1 MiB boundaries.
constexpr uint64_t LEGACY_CHS_STEP = 63; // sectors per track on classic BIOS disks
std::vector<RecoveredPartition> results;
uint64_t scannedSectors = 0;
for (uint64_t lba = 0; lba < totalSectors; lba += stepSectors)
{
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
return ErrorInfo::fromCode(ErrorCode::OperationCanceled, "Partition scan canceled");
RecoveredPartition candidate;
if (probeOffset(lba, candidate))
{
candidate.sectorSize = sectorSize;
results.push_back(candidate);
}
scannedSectors += stepSectors;
if (progressCb)
progressCb(std::min(scannedSectors, totalSectors), totalSectors, results.size());
}
// Quick scan: also probe legacy cylinder boundaries that aren't on 1 MiB multiples
if (mode == PartitionScanMode::Quick)
{
for (uint64_t lba = LEGACY_CHS_STEP; lba < totalSectors; lba += LEGACY_CHS_STEP)
{
// Skip if this LBA was already covered by the 1 MiB pass
if ((lba * sectorSize) % DEFAULT_ALIGNMENT_BYTES == 0)
continue;
if (cancelFlag && cancelFlag->load(std::memory_order_relaxed))
break;
RecoveredPartition candidate;
if (probeOffset(lba, candidate))
{
candidate.sectorSize = sectorSize;
results.push_back(candidate);
}
}
}
// Mark partitions that overlap existing entries
markOverlaps(results);
if (results.empty())
return ErrorInfo::fromCode(ErrorCode::NoPartitionsFound, "No lost partitions found");
return results;
}
// ---------------------------------------------------------------------------
// probeOffset -- try to identify a filesystem superblock at the given LBA
// ---------------------------------------------------------------------------
bool PartitionRecovery::probeOffset(SectorOffset lba, RecoveredPartition& out) const
{
const uint32_t sectorSize = m_geometry.bytesPerSector;
const uint64_t byteOffset = lba * sectorSize;
// We need to read enough data to detect any filesystem.
// Most signatures are in the first 4 KiB, but ext superblock is at offset
// 1024 from partition start and Btrfs superblock is at 0x10000 (64 KiB).
// Read the first sector first (cheap), then extend if needed.
// Create a read callback rooted at this LBA for FilesystemDetector
auto readFunc = [this, byteOffset, sectorSize](uint64_t offset, uint32_t size) -> Result<std::vector<uint8_t>>
{
// Convert the relative offset to an absolute sector address
uint64_t absOffset = byteOffset + offset;
SectorOffset startSector = absOffset / sectorSize;
// Round size up to sector boundary
uint32_t alignedSize = ((size + sectorSize - 1) / sectorSize) * sectorSize;
SectorCount sectorsToRead = alignedSize / sectorSize;
auto readResult = m_disk.readSectors(startSector, sectorsToRead, sectorSize);
if (readResult.isError())
return readResult.error();
// Trim to the requested sub-range
auto& data = readResult.value();
uint32_t inSectorOffset = static_cast<uint32_t>(absOffset % sectorSize);
if (inSectorOffset + size > data.size())
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Read underflow");
std::vector<uint8_t> trimmed(data.begin() + inSectorOffset,
data.begin() + inSectorOffset + size);
return trimmed;
};
auto detectResult = FilesystemDetector::detect(readFunc, 0);
if (detectResult.isError() || !detectResult.value().isDetected())
return false;
const auto& detection = detectResult.value();
out.startLba = lba;
out.fsType = detection.type;
out.label = detection.label;
// Estimate partition size from the superblock
uint64_t estimatedBytes = estimatePartitionSize(lba, detection.type);
if (estimatedBytes > 0)
{
out.sectorCount = estimatedBytes / sectorSize;
}
else
{
// Fallback: unknown size, mark it as spanning to next found partition
// or end of disk. Set to 0 and let the caller decide.
out.sectorCount = 0;
}
// Confidence heuristic:
// - Known modern FS at 1 MiB boundary -> 95%
// - Known modern FS at cylinder boundary -> 85%
// - Known modern FS at other offset -> 70%
// - Exotic/unknown FS -> 50%
const bool onMibBoundary = ((lba * sectorSize) % DEFAULT_ALIGNMENT_BYTES == 0) && (lba != 0);
const bool onCylBoundary = (lba % 63 == 0) && (lba != 0);
const bool isModernFs = (detection.type == FilesystemType::NTFS ||
detection.type == FilesystemType::FAT32 ||
detection.type == FilesystemType::FAT16 ||
detection.type == FilesystemType::ExFAT ||
detection.type == FilesystemType::Ext4 ||
detection.type == FilesystemType::Ext3 ||
detection.type == FilesystemType::Ext2 ||
detection.type == FilesystemType::Btrfs ||
detection.type == FilesystemType::XFS);
if (isModernFs && onMibBoundary) out.confidence = 95.0;
else if (isModernFs && onCylBoundary) out.confidence = 85.0;
else if (isModernFs) out.confidence = 70.0;
else if (lba == 0) out.confidence = 30.0; // Sector 0 is usually MBR/GPT
else out.confidence = 50.0;
return true;
}
// ---------------------------------------------------------------------------
// estimatePartitionSize -- read the superblock to extract volume size
// ---------------------------------------------------------------------------
uint64_t PartitionRecovery::estimatePartitionSize(SectorOffset lba, FilesystemType fs) const
{
const uint32_t sectorSize = m_geometry.bytesPerSector;
const uint64_t byteOffset = lba * sectorSize;
// For each filesystem type, we know where the volume-size field lives in
// the superblock. Read the relevant bytes and extract the value.
auto readAbsolute = [this, sectorSize](uint64_t absOffset, uint32_t size)
-> std::vector<uint8_t>
{
SectorOffset startSector = absOffset / sectorSize;
uint32_t alignedSize = ((size + sectorSize - 1) / sectorSize) * sectorSize;
SectorCount sectorsToRead = alignedSize / sectorSize;
auto result = m_disk.readSectors(startSector, sectorsToRead, sectorSize);
if (result.isError())
return {};
auto& data = result.value();
uint32_t inOffset = static_cast<uint32_t>(absOffset % sectorSize);
if (inOffset + size > data.size())
return {};
return std::vector<uint8_t>(data.begin() + inOffset, data.begin() + inOffset + size);
};
switch (fs)
{
case FilesystemType::NTFS:
{
// NTFS BPB: total sectors at offset 0x28 (8 bytes, little-endian)
auto bpb = readAbsolute(byteOffset, 512);
if (bpb.size() < 0x30)
return 0;
uint64_t totalSectors = 0;
std::memcpy(&totalSectors, &bpb[0x28], 8);
return totalSectors * sectorSize;
}
case FilesystemType::FAT32:
case FilesystemType::FAT16:
case FilesystemType::FAT12:
{
// FAT BPB: total sectors 16 at offset 0x13 (2 bytes), total sectors 32 at 0x20 (4 bytes)
auto bpb = readAbsolute(byteOffset, 512);
if (bpb.size() < 0x24)
return 0;
uint16_t totalSectors16 = 0;
uint32_t totalSectors32 = 0;
std::memcpy(&totalSectors16, &bpb[0x13], 2);
std::memcpy(&totalSectors32, &bpb[0x20], 4);
uint64_t totalSectors = totalSectors16 ? totalSectors16 : totalSectors32;
// Bytes per sector from BPB
uint16_t bps = 0;
std::memcpy(&bps, &bpb[0x0B], 2);
if (bps == 0)
bps = static_cast<uint16_t>(sectorSize);
return totalSectors * bps;
}
case FilesystemType::ExFAT:
{
// exFAT: volume length at offset 0x48 (8 bytes, sectors)
auto boot = readAbsolute(byteOffset, 512);
if (boot.size() < 0x50)
return 0;
uint64_t volumeLength = 0;
std::memcpy(&volumeLength, &boot[0x48], 8);
// exFAT sector size is 2^(BytesPerSectorShift) at offset 0x6C
uint8_t bpsShift = boot[0x6C];
uint32_t exfatSectorSize = (bpsShift > 0 && bpsShift <= 12) ? (1u << bpsShift) : sectorSize;
return volumeLength * exfatSectorSize;
}
case FilesystemType::Ext2:
case FilesystemType::Ext3:
case FilesystemType::Ext4:
{
// ext superblock at offset 1024 from partition start.
// s_blocks_count_lo at offset 4 (4 bytes), s_log_block_size at offset 24 (4 bytes).
// For ext4 with 64-bit feature, s_blocks_count_hi at offset 0x150 (4 bytes).
auto sb = readAbsolute(byteOffset + 1024, 512);
if (sb.size() < 256)
return 0;
uint32_t blocksLo = 0, logBlockSize = 0;
std::memcpy(&blocksLo, &sb[4], 4);
std::memcpy(&logBlockSize, &sb[24], 4);
uint64_t blockSize = 1024ULL << logBlockSize;
uint64_t totalBlocks = blocksLo;
// Check for 64-bit block count (ext4 feature flag at offset 0x60, bit 0x80 = INCOMPAT_64BIT)
if (sb.size() >= 0x154)
{
uint32_t incompatFeatures = 0;
std::memcpy(&incompatFeatures, &sb[0x60], 4);
if (incompatFeatures & 0x80) // INCOMPAT_64BIT
{
uint32_t blocksHi = 0;
std::memcpy(&blocksHi, &sb[0x150 - 1024 + 1024], 4); // offset 0x150 in superblock
// Superblock starts at partition+1024, so offset within our 512-byte read at
// partition+1024 is relative. We need to re-read if sb isn't large enough.
// Simpler: read a larger chunk.
auto sbFull = readAbsolute(byteOffset + 1024, 1024);
if (sbFull.size() >= 0x154)
{
std::memcpy(&blocksHi, &sbFull[0x150 - 1024 + 1024 - 1024], 4);
// Offset 0x150 in the superblock. Our buffer starts at superblock offset 0.
// So it's at buffer[0x150]. But we only read 1024 bytes -> 0x150 = 336, within range.
std::memcpy(&blocksHi, &sbFull[0x150], 4);
totalBlocks |= (static_cast<uint64_t>(blocksHi) << 32);
}
}
}
return totalBlocks * blockSize;
}
case FilesystemType::Btrfs:
{
// Btrfs superblock at 0x10000 from partition start.
// total_bytes at offset 0x70 (8 bytes) within the superblock.
auto sb = readAbsolute(byteOffset + 0x10000, 256);
if (sb.size() < 0x78)
return 0;
uint64_t totalBytes = 0;
std::memcpy(&totalBytes, &sb[0x70], 8);
return totalBytes;
}
case FilesystemType::XFS:
{
// XFS superblock at partition start.
// sb_dblocks (total data blocks) at offset 8 (8 bytes, big-endian).
// sb_blocksize at offset 4 (4 bytes, big-endian).
auto sb = readAbsolute(byteOffset, 512);
if (sb.size() < 20)
return 0;
// XFS is big-endian on disk
uint32_t blockSizeBE = 0;
uint64_t totalBlocksBE = 0;
std::memcpy(&blockSizeBE, &sb[4], 4);
std::memcpy(&totalBlocksBE, &sb[8], 8);
// Byte-swap from big-endian
uint32_t xfsBlockSize =
((blockSizeBE >> 24) & 0xFF) |
((blockSizeBE >> 8) & 0xFF00) |
((blockSizeBE << 8) & 0xFF0000) |
((blockSizeBE << 24) & 0xFF000000);
uint64_t xfsTotalBlocks =
((totalBlocksBE >> 56) & 0xFF) |
((totalBlocksBE >> 40) & 0xFF00) |
((totalBlocksBE >> 24) & 0xFF0000) |
((totalBlocksBE >> 8) & 0xFF000000ULL) |
((totalBlocksBE << 8) & 0xFF00000000ULL) |
((totalBlocksBE << 24) & 0xFF0000000000ULL) |
((totalBlocksBE << 40) & 0xFF000000000000ULL) |
((totalBlocksBE << 56) & 0xFF00000000000000ULL);
return xfsTotalBlocks * xfsBlockSize;
}
default:
return 0;
}
}
// ---------------------------------------------------------------------------
// markOverlaps -- flag found partitions that overlap current table entries
// ---------------------------------------------------------------------------
void PartitionRecovery::markOverlaps(std::vector<RecoveredPartition>& results) const
{
for (auto& found : results)
{
if (found.sectorCount == 0)
continue;
uint64_t foundStart = found.startLba;
uint64_t foundEnd = found.startLba + found.sectorCount;
for (const auto& existing : m_layout.partitions)
{
uint64_t existStart = existing.startingOffset / m_geometry.bytesPerSector;
uint64_t existEnd = existStart + (existing.partitionLength / m_geometry.bytesPerSector);
// Classic overlap test: A.start < B.end && B.start < A.end
if (foundStart < existEnd && existStart < foundEnd)
{
found.overlapsExisting = true;
// If it exactly matches an existing partition, lower confidence
// significantly because it's not actually "lost"
if (foundStart == existStart && foundEnd == existEnd)
found.confidence = 10.0;
break;
}
}
}
}
// ---------------------------------------------------------------------------
// recover -- write a found partition back to the on-disk partition table
// ---------------------------------------------------------------------------
Result<void> PartitionRecovery::recover(const RecoveredPartition& partition)
{
if (partition.sectorCount == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Cannot recover partition with unknown size");
const uint32_t sectorSize = m_geometry.bytesPerSector;
// Build a DiskReadCallback so we can parse the existing table
auto readFunc = [this, sectorSize](uint64_t offset, uint32_t size) -> Result<std::vector<uint8_t>>
{
SectorOffset startSector = offset / sectorSize;
uint32_t aligned = ((size + sectorSize - 1) / sectorSize) * sectorSize;
return m_disk.readSectors(startSector, aligned / sectorSize, sectorSize);
};
auto tableResult = PartitionTable::parse(readFunc, m_geometry.totalBytes, sectorSize);
if (tableResult.isError())
return tableResult.error();
auto& table = tableResult.value();
// Build a PartitionParams for the new entry
PartitionParams params;
params.startLba = partition.startLba;
params.sectorCount = partition.sectorCount;
if (table->type() == PartitionTableType::MBR)
{
// Determine MBR type byte from filesystem type
switch (partition.fsType)
{
case FilesystemType::NTFS:
case FilesystemType::ExFAT:
params.mbrType = MbrTypes::NTFS_HPFS;
break;
case FilesystemType::FAT32:
params.mbrType = MbrTypes::FAT32_LBA;
break;
case FilesystemType::FAT16:
params.mbrType = MbrTypes::FAT16_LBA;
break;
case FilesystemType::FAT12:
params.mbrType = MbrTypes::FAT12;
break;
case FilesystemType::Ext2:
case FilesystemType::Ext3:
case FilesystemType::Ext4:
case FilesystemType::Btrfs:
case FilesystemType::XFS:
params.mbrType = MbrTypes::LinuxNative;
break;
default:
params.mbrType = MbrTypes::NTFS_HPFS; // Safe default for data partitions
break;
}
}
else if (table->type() == PartitionTableType::GPT)
{
// Use Microsoft Basic Data GUID as default; adjust for Linux filesystems
switch (partition.fsType)
{
case FilesystemType::Ext2:
case FilesystemType::Ext3:
case FilesystemType::Ext4:
case FilesystemType::Btrfs:
case FilesystemType::XFS:
params.typeGuid = GptTypes::linuxFilesystem();
break;
default:
params.typeGuid = GptTypes::microsoftBasicData();
break;
}
params.gptName = partition.label.empty() ? "Recovered Partition" : partition.label;
}
auto addResult = table->addPartition(params);
if (addResult.isError())
return addResult;
// Serialize the modified table to bytes and write it back to disk
auto serResult = table->serialize();
if (serResult.isError())
return serResult.error();
const auto& tableBytes = serResult.value();
// Write sector 0 (and additional sectors for GPT)
SectorCount tableSectors = (tableBytes.size() + sectorSize - 1) / sectorSize;
auto writeResult = m_disk.writeSectors(0, tableBytes.data(), tableSectors, sectorSize);
if (writeResult.isError())
return writeResult;
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,94 @@
#pragma once
// PartitionRecovery -- Scan a physical disk for lost/deleted partition superblocks.
//
// Two scan modes:
// Quick scan: checks 1 MiB alignment boundaries only (fast, finds most modern partitions).
// Deep scan: checks every sector (slow, finds everything including cylinder-aligned relics).
//
// Found partitions are cross-referenced against the existing partition table so that only
// genuinely missing entries are reported.
//
// DISCLAIMER: This code is for authorized disk utility software only.
// Recovery writes modify the partition table -- always confirm with the user first.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Constants.h"
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/RawDiskHandle.h"
#include "../disk/PartitionTable.h"
#include "../disk/FilesystemDetector.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// A partition candidate found during recovery scanning
struct RecoveredPartition
{
SectorOffset startLba = 0;
SectorCount sectorCount = 0;
uint32_t sectorSize = SECTOR_SIZE_512;
FilesystemType fsType = FilesystemType::Unknown;
std::string label; // Volume label if readable from superblock
double confidence = 0.0; // 0.0 - 100.0
bool overlapsExisting = false;
};
// Scan mode
enum class PartitionScanMode
{
Quick, // Every 1 MiB boundary
Deep, // Every sector
};
// Progress callback for partition recovery scan.
// Parameters: (sectorsScanned, totalSectors, partitionsFoundSoFar)
using PartitionScanProgress = std::function<void(uint64_t sectorsScanned,
uint64_t totalSectors,
size_t partitionsFound)>;
class PartitionRecovery
{
public:
explicit PartitionRecovery(RawDiskHandle& disk);
// Run the scan. Results are returned as a vector of candidates.
Result<std::vector<RecoveredPartition>> scan(
PartitionScanMode mode,
PartitionScanProgress progressCb = nullptr,
std::atomic<bool>* cancelFlag = nullptr);
// Write a recovered partition back to the partition table.
// Works for both MBR and GPT. The caller must have opened the disk ReadWrite.
Result<void> recover(const RecoveredPartition& partition);
private:
// Probe a single sector offset to see if a filesystem superblock starts there.
// Returns an empty optional if nothing was found.
bool probeOffset(SectorOffset lba, RecoveredPartition& out) const;
// Determine partition size from the superblock at the given LBA.
uint64_t estimatePartitionSize(SectorOffset lba, FilesystemType fs) const;
// Cross-reference found partitions against existing table.
void markOverlaps(std::vector<RecoveredPartition>& results) const;
RawDiskHandle& m_disk;
DiskGeometryInfo m_geometry = {};
DriveLayoutInfo m_layout = {};
};
} // namespace spw

View File

@@ -0,0 +1,900 @@
#include "BootAuthenticator.h"
#include "../common/Logging.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QStorageInfo>
#include <cstring>
#include <iomanip>
#include <sstream>
// For USB serial number retrieval via SetupAPI
#include <setupapi.h>
#include <devguid.h>
#include <cfgmgr32.h>
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "setupapi.lib")
namespace spw
{
// ============================================================
// Constructor / Destructor
// ============================================================
BootAuthenticator::BootAuthenticator() = default;
BootAuthenticator::~BootAuthenticator() = default;
// ============================================================
// BCrypt helper: SHA-256
// ============================================================
Result<std::vector<uint8_t>> BootAuthenticator::sha256(
const uint8_t* data, size_t len) const
{
BCRYPT_ALG_HANDLE hAlgo = nullptr;
NTSTATUS status = BCryptOpenAlgorithmProvider(
&hAlgo, BCRYPT_SHA256_ALGORITHM, nullptr, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"Failed to open SHA-256 provider");
}
BCRYPT_HASH_HANDLE hHash = nullptr;
status = BCryptCreateHash(hAlgo, &hHash, nullptr, 0, nullptr, 0, 0);
if (!BCRYPT_SUCCESS(status))
{
BCryptCloseAlgorithmProvider(hAlgo, 0);
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"Failed to create SHA-256 hash");
}
status = BCryptHashData(hHash, const_cast<PUCHAR>(data),
static_cast<ULONG>(len), 0);
if (!BCRYPT_SUCCESS(status))
{
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlgo, 0);
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"SHA-256 hash data failed");
}
std::vector<uint8_t> hash(32, 0);
status = BCryptFinishHash(hHash, hash.data(), 32, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlgo, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"SHA-256 finish failed");
}
return hash;
}
// ============================================================
// BCrypt helper: HMAC-SHA256
// ============================================================
Result<std::vector<uint8_t>> BootAuthenticator::hmacSha256(
const uint8_t* key, size_t keyLen,
const uint8_t* data, size_t dataLen) const
{
BCRYPT_ALG_HANDLE hAlgo = nullptr;
NTSTATUS status = BCryptOpenAlgorithmProvider(
&hAlgo, BCRYPT_SHA256_ALGORITHM, nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"Failed to open HMAC-SHA256 provider");
}
BCRYPT_HASH_HANDLE hHash = nullptr;
status = BCryptCreateHash(
hAlgo, &hHash, nullptr, 0,
const_cast<PUCHAR>(key), static_cast<ULONG>(keyLen), 0);
if (!BCRYPT_SUCCESS(status))
{
BCryptCloseAlgorithmProvider(hAlgo, 0);
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"Failed to create HMAC-SHA256 hash");
}
status = BCryptHashData(hHash, const_cast<PUCHAR>(data),
static_cast<ULONG>(dataLen), 0);
if (!BCRYPT_SUCCESS(status))
{
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlgo, 0);
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"HMAC-SHA256 hash data failed");
}
std::vector<uint8_t> hmac(32, 0);
status = BCryptFinishHash(hHash, hmac.data(), 32, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlgo, 0);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::EncryptionFailed,
"HMAC-SHA256 finish failed");
}
return hmac;
}
// ============================================================
// BCrypt helper: random bytes
// ============================================================
Result<void> BootAuthenticator::generateRandom(uint8_t* out, size_t len) const
{
NTSTATUS status = BCryptGenRandom(nullptr, out, static_cast<ULONG>(len),
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!BCRYPT_SUCCESS(status))
{
return ErrorInfo::fromCode(ErrorCode::KeyGenerationFailed,
"BCryptGenRandom failed");
}
return Result<void>::ok();
}
// ============================================================
// Constant-time comparison
// ============================================================
bool BootAuthenticator::constantTimeCompare(
const uint8_t* a, const uint8_t* b, size_t len)
{
volatile uint8_t diff = 0;
for (size_t i = 0; i < len; ++i)
{
diff |= a[i] ^ b[i];
}
return diff == 0;
}
// ============================================================
// Hex conversion helpers (local to this TU)
// ============================================================
static std::string toHex(const uint8_t* data, size_t len)
{
std::ostringstream oss;
for (size_t i = 0; i < len; ++i)
{
oss << std::hex << std::setfill('0') << std::setw(2)
<< static_cast<unsigned>(data[i]);
}
return oss.str();
}
static std::vector<uint8_t> fromHex(const std::string& hex)
{
std::vector<uint8_t> bytes;
bytes.reserve(hex.size() / 2);
for (size_t i = 0; i + 1 < hex.size(); i += 2)
{
uint8_t byte = static_cast<uint8_t>(
std::stoi(hex.substr(i, 2), nullptr, 16));
bytes.push_back(byte);
}
return bytes;
}
// ============================================================
// USB serial number retrieval
// ============================================================
Result<QString> BootAuthenticator::getUsbSerialForDrive(
const QString& driveLetter) const
{
// Get the volume name for this drive letter
QString rootPath = driveLetter;
if (!rootPath.endsWith("\\"))
rootPath += "\\";
wchar_t volumeName[MAX_PATH] = {};
if (!GetVolumeNameForVolumeMountPointW(
rootPath.toStdWString().c_str(),
volumeName, MAX_PATH))
{
DWORD err = GetLastError();
return ErrorInfo::fromWin32(ErrorCode::DiskNotFound, err,
"Cannot get volume name for " + driveLetter.toStdString());
}
// Remove the trailing backslash for QueryDosDevice
std::wstring volName(volumeName);
// Strip the "\\\\?\\" prefix and trailing "\\"
if (volName.size() > 4 && volName.substr(0, 4) == L"\\\\?\\")
{
volName = volName.substr(4);
}
while (!volName.empty() && volName.back() == L'\\')
{
volName.pop_back();
}
// Use SetupAPI to enumerate USB disk devices and match by volume
HDEVINFO devInfo = SetupDiGetClassDevsW(
&GUID_DEVINTERFACE_DISK, L"USB", nullptr,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devInfo == INVALID_HANDLE_VALUE)
{
// Fallback: use GetVolumeInformationW serial
DWORD volumeSerial = 0;
if (GetVolumeInformationW(
rootPath.toStdWString().c_str(),
nullptr, 0, &volumeSerial,
nullptr, nullptr, nullptr, 0))
{
char serialBuf[16] = {};
snprintf(serialBuf, sizeof(serialBuf), "%08X", volumeSerial);
return QString(serialBuf);
}
return ErrorInfo::fromCode(ErrorCode::DiskNotFound,
"Cannot enumerate USB devices for serial number");
}
SP_DEVINFO_DATA devInfoData = {};
devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
QString foundSerial;
for (DWORD i = 0; SetupDiEnumDeviceInfo(devInfo, i, &devInfoData); ++i)
{
// Get the device instance ID, which contains the USB serial for USB devices
// Format: USB\VID_xxxx&PID_xxxx\serial_number
wchar_t instanceId[MAX_PATH] = {};
if (!SetupDiGetDeviceInstanceIdW(devInfo, &devInfoData,
instanceId, MAX_PATH, nullptr))
{
continue;
}
std::wstring instIdStr(instanceId);
// Extract the serial number (last component after the last backslash)
size_t lastBackslash = instIdStr.rfind(L'\\');
if (lastBackslash == std::wstring::npos)
continue;
std::wstring serial = instIdStr.substr(lastBackslash + 1);
// Check if this device corresponds to our drive letter.
// We match by checking the device's drive letter via
// CM_Get_Device_Interface_List, but a simpler heuristic is
// to get the device number and compare.
// For a practical approach, we enumerate disk interfaces for this device
SP_DEVICE_INTERFACE_DATA interfaceData = {};
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
if (SetupDiEnumDeviceInterfaces(devInfo, &devInfoData,
&GUID_DEVINTERFACE_DISK, 0, &interfaceData))
{
DWORD requiredSize = 0;
SetupDiGetDeviceInterfaceDetailW(devInfo, &interfaceData,
nullptr, 0, &requiredSize, nullptr);
if (requiredSize > 0)
{
std::vector<uint8_t> detailBuf(requiredSize, 0);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(detailBuf.data());
detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
if (SetupDiGetDeviceInterfaceDetailW(devInfo, &interfaceData,
detail, requiredSize, nullptr, nullptr))
{
// Open the disk to get its device number
HANDLE hDisk = CreateFileW(
detail->DevicePath,
0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hDisk != INVALID_HANDLE_VALUE)
{
STORAGE_DEVICE_NUMBER sdn = {};
DWORD bytesReturned = 0;
if (DeviceIoControl(hDisk, IOCTL_STORAGE_GET_DEVICE_NUMBER,
nullptr, 0, &sdn, sizeof(sdn),
&bytesReturned, nullptr))
{
// Now check if our drive letter is on this physical disk
std::wstring driveDevPath = L"\\\\.\\" + driveLetter.toStdWString();
// Remove trailing colon if just letter
if (driveDevPath.back() != L':')
driveDevPath += L':';
HANDLE hVol = CreateFileW(
driveDevPath.c_str(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hVol != INVALID_HANDLE_VALUE)
{
STORAGE_DEVICE_NUMBER volSdn = {};
if (DeviceIoControl(hVol, IOCTL_STORAGE_GET_DEVICE_NUMBER,
nullptr, 0, &volSdn, sizeof(volSdn),
&bytesReturned, nullptr))
{
if (sdn.DeviceNumber == volSdn.DeviceNumber)
{
foundSerial = QString::fromStdWString(serial);
}
}
CloseHandle(hVol);
}
}
CloseHandle(hDisk);
}
}
}
}
if (!foundSerial.isEmpty())
break;
}
SetupDiDestroyDeviceInfoList(devInfo);
if (foundSerial.isEmpty())
{
// Fallback: use volume serial number
DWORD volumeSerial = 0;
if (GetVolumeInformationW(
rootPath.toStdWString().c_str(),
nullptr, 0, &volumeSerial,
nullptr, nullptr, nullptr, 0))
{
char serialBuf[16] = {};
snprintf(serialBuf, sizeof(serialBuf), "%08X", volumeSerial);
return QString(serialBuf);
}
return ErrorInfo::fromCode(ErrorCode::DiskNotFound,
"Cannot determine USB serial for drive " +
driveLetter.toStdString());
}
return foundSerial;
}
// ============================================================
// Enumerate USB drives
// ============================================================
Result<std::vector<UsbDriveInfo>> BootAuthenticator::enumerateUsbDrives() const
{
std::vector<UsbDriveInfo> drives;
// Get all mounted volumes
for (const QStorageInfo& storage : QStorageInfo::mountedVolumes())
{
if (!storage.isValid() || !storage.isReady())
continue;
QString rootPath = storage.rootPath();
if (rootPath.isEmpty())
continue;
// Check if this is a removable drive
std::wstring rootW = rootPath.toStdWString();
if (!rootW.empty() && rootW.back() != L'\\')
rootW += L'\\';
UINT driveType = GetDriveTypeW(rootW.c_str());
if (driveType != DRIVE_REMOVABLE)
continue;
UsbDriveInfo info;
info.driveLetter = rootPath.left(2); // "E:"
info.volumeLabel = storage.name();
info.totalBytes = static_cast<uint64_t>(storage.bytesTotal());
info.freeBytes = static_cast<uint64_t>(storage.bytesAvailable());
// Try to get USB serial
auto serialResult = getUsbSerialForDrive(info.driveLetter);
if (serialResult.isOk())
{
info.serialNumber = serialResult.value();
}
// Check if a boot token already exists
QString tokenPath = info.driveLetter + QString::fromWCharArray(BOOT_TOKEN_FILENAME);
info.hasBootToken = QFile::exists(tokenPath);
// Get volume info for manufacturer/product (limited info available)
wchar_t volName[MAX_PATH] = {};
wchar_t fsName[MAX_PATH] = {};
GetVolumeInformationW(rootW.c_str(), volName, MAX_PATH,
nullptr, nullptr, nullptr, fsName, MAX_PATH);
if (wcslen(volName) > 0 && info.volumeLabel.isEmpty())
{
info.volumeLabel = QString::fromWCharArray(volName);
}
drives.push_back(std::move(info));
}
return drives;
}
// ============================================================
// Generate token blob
// ============================================================
Result<std::vector<uint8_t>> BootAuthenticator::generateTokenBlob(
const QString& usbSerial) const
{
std::vector<uint8_t> blob(BOOT_TOKEN_FILE_SIZE, 0);
size_t offset = 0;
// Magic (8 bytes)
std::memcpy(blob.data() + offset, BOOT_MAGIC, BOOT_MAGIC_LEN);
offset += BOOT_MAGIC_LEN;
// Device serial hash (SHA-256 of the USB serial string)
QByteArray serialBytes = usbSerial.toUtf8();
auto serialHashResult = sha256(
reinterpret_cast<const uint8_t*>(serialBytes.constData()),
static_cast<size_t>(serialBytes.size()));
if (serialHashResult.isError())
return serialHashResult.error();
std::memcpy(blob.data() + offset, serialHashResult.value().data(), BOOT_SERIAL_HASH_LEN);
offset += BOOT_SERIAL_HASH_LEN;
// Random token (32 bytes)
auto randResult = generateRandom(blob.data() + offset, BOOT_TOKEN_LEN);
if (randResult.isError())
return randResult.error();
offset += BOOT_TOKEN_LEN;
// HMAC-SHA256 over bytes 0x00..0x47 (magic + serial hash + token), keyed by the token
const uint8_t* tokenPtr = blob.data() + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN;
auto hmacResult = hmacSha256(
tokenPtr, BOOT_TOKEN_LEN,
blob.data(), BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN + BOOT_TOKEN_LEN);
if (hmacResult.isError())
return hmacResult.error();
std::memcpy(blob.data() + offset, hmacResult.value().data(), BOOT_HMAC_LEN);
return blob;
}
// ============================================================
// Create boot key
// ============================================================
Result<void> BootAuthenticator::createBootKey(const QString& driveLetter)
{
log::info("Creating boot key on drive: " + driveLetter);
// Get USB serial number
auto serialResult = getUsbSerialForDrive(driveLetter);
if (serialResult.isError())
{
return ErrorInfo::fromCode(ErrorCode::DiskNotFound,
"Cannot determine USB serial for drive " +
driveLetter.toStdString() +
" — is this a USB drive?");
}
const QString& usbSerial = serialResult.value();
// Generate the token blob
auto blobResult = generateTokenBlob(usbSerial);
if (blobResult.isError())
return blobResult.error();
const auto& blob = blobResult.value();
// Write the .spwboot file
QString tokenPath = driveLetter + QString::fromWCharArray(BOOT_TOKEN_FILENAME);
QFile tokenFile(tokenPath);
if (!tokenFile.open(QIODevice::WriteOnly))
{
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create boot token file: " +
tokenPath.toStdString());
}
qint64 written = tokenFile.write(
reinterpret_cast<const char*>(blob.data()),
static_cast<qint64>(blob.size()));
tokenFile.flush();
tokenFile.close();
if (written != static_cast<qint64>(BOOT_TOKEN_FILE_SIZE))
{
QFile::remove(tokenPath);
return ErrorInfo::fromCode(ErrorCode::DiskWriteError,
"Failed to write boot token file");
}
// Set the file as hidden + system
std::wstring tokenPathW = tokenPath.toStdWString();
SetFileAttributesW(tokenPathW.c_str(),
FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);
// Compute the token hash (SHA-256 of the random token) for storage
const uint8_t* tokenPtr = blob.data() + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN;
auto tokenHashResult = sha256(tokenPtr, BOOT_TOKEN_LEN);
if (tokenHashResult.isError())
return tokenHashResult.error();
// Compute serial hash for config
QByteArray serialBytes = usbSerial.toUtf8();
auto serialHashResult = sha256(
reinterpret_cast<const uint8_t*>(serialBytes.constData()),
static_cast<size_t>(serialBytes.size()));
if (serialHashResult.isError())
return serialHashResult.error();
// Save configuration
BootKeyConfig config;
config.enabled = true;
config.tokenHashHex = QString::fromStdString(
toHex(tokenHashResult.value().data(), tokenHashResult.value().size()));
config.serialHashHex = QString::fromStdString(
toHex(serialHashResult.value().data(), serialHashResult.value().size()));
config.createdTimestamp = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
config.lastVerifiedTimestamp.clear();
saveConfig(config);
log::info("Boot key created successfully on " + driveLetter);
return Result<void>::ok();
}
// ============================================================
// Read token file
// ============================================================
Result<std::vector<uint8_t>> BootAuthenticator::readTokenFile(
const QString& driveLetter) const
{
QString tokenPath = driveLetter + QString::fromWCharArray(BOOT_TOKEN_FILENAME);
QFile file(tokenPath);
if (!file.open(QIODevice::ReadOnly))
{
return ErrorInfo::fromCode(ErrorCode::FileNotFound,
"Boot token file not found on " +
driveLetter.toStdString());
}
QByteArray raw = file.readAll();
file.close();
if (raw.size() != static_cast<int>(BOOT_TOKEN_FILE_SIZE))
{
return ErrorInfo::fromCode(ErrorCode::DecryptionFailed,
"Boot token file has wrong size: expected " +
std::to_string(BOOT_TOKEN_FILE_SIZE) + ", got " +
std::to_string(raw.size()));
}
return std::vector<uint8_t>(
reinterpret_cast<const uint8_t*>(raw.constData()),
reinterpret_cast<const uint8_t*>(raw.constData()) + raw.size());
}
// ============================================================
// Validate token blob
// ============================================================
Result<void> BootAuthenticator::validateTokenBlob(
const uint8_t* data, size_t len) const
{
if (!data || len != BOOT_TOKEN_FILE_SIZE)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Invalid token blob size");
}
// Check magic
if (std::memcmp(data, BOOT_MAGIC, BOOT_MAGIC_LEN) != 0)
{
return ErrorInfo::fromCode(ErrorCode::DecryptionFailed,
"Invalid boot token magic");
}
// Verify HMAC
const uint8_t* tokenPtr = data + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN;
const uint8_t* storedHmac = data + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN + BOOT_TOKEN_LEN;
size_t hmacInputLen = BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN + BOOT_TOKEN_LEN;
auto hmacResult = hmacSha256(tokenPtr, BOOT_TOKEN_LEN,
data, hmacInputLen);
if (hmacResult.isError())
return hmacResult.error();
if (!constantTimeCompare(storedHmac, hmacResult.value().data(), BOOT_HMAC_LEN))
{
return ErrorInfo::fromCode(ErrorCode::DecryptionFailed,
"Boot token HMAC verification failed — "
"token may be corrupted or tampered with");
}
return Result<void>::ok();
}
// ============================================================
// Verify boot key (scan all USB drives)
// ============================================================
Result<QString> BootAuthenticator::verifyBootKey() const
{
BootKeyConfig config = loadConfig();
if (!config.enabled)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Boot authentication is not enabled");
}
if (config.tokenHashHex.isEmpty() || config.serialHashHex.isEmpty())
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Boot key configuration is incomplete");
}
// Enumerate USB drives and check each for a valid token
auto drivesResult = enumerateUsbDrives();
if (drivesResult.isError())
return drivesResult.error();
std::vector<uint8_t> expectedTokenHash = fromHex(config.tokenHashHex.toStdString());
std::vector<uint8_t> expectedSerialHash = fromHex(config.serialHashHex.toStdString());
for (const auto& drive : drivesResult.value())
{
auto tokenResult = readTokenFile(drive.driveLetter);
if (tokenResult.isError())
continue; // No token on this drive
const auto& blob = tokenResult.value();
// Validate blob structure and HMAC
auto validateResult = validateTokenBlob(blob.data(), blob.size());
if (validateResult.isError())
continue;
// Check serial hash matches stored config
const uint8_t* serialHash = blob.data() + BOOT_MAGIC_LEN;
if (expectedSerialHash.size() == BOOT_SERIAL_HASH_LEN &&
!constantTimeCompare(serialHash, expectedSerialHash.data(), BOOT_SERIAL_HASH_LEN))
{
continue; // Serial mismatch — not our registered key
}
// Check token hash matches stored config
const uint8_t* token = blob.data() + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN;
auto tokenHashResult = sha256(token, BOOT_TOKEN_LEN);
if (tokenHashResult.isError())
continue;
if (expectedTokenHash.size() == 32 &&
constantTimeCompare(tokenHashResult.value().data(),
expectedTokenHash.data(), 32))
{
// Match found — update last verified timestamp
// (const method, so we cast away const for this bookkeeping)
const_cast<BootAuthenticator*>(this)->saveConfig([&]() {
BootKeyConfig updated = config;
updated.lastVerifiedTimestamp =
QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
return updated;
}());
log::info("Boot key verified on drive: " + drive.driveLetter);
return drive.driveLetter;
}
}
return ErrorInfo::fromCode(ErrorCode::Fido2AuthFailed,
"No valid boot key found on any connected USB drive");
}
// ============================================================
// Verify a specific drive
// ============================================================
Result<void> BootAuthenticator::verifyDrive(const QString& driveLetter) const
{
BootKeyConfig config = loadConfig();
if (!config.enabled)
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Boot authentication is not enabled");
}
auto tokenResult = readTokenFile(driveLetter);
if (tokenResult.isError())
return tokenResult.error();
const auto& blob = tokenResult.value();
// Validate structure
auto validateResult = validateTokenBlob(blob.data(), blob.size());
if (validateResult.isError())
return validateResult.error();
// Check serial hash
std::vector<uint8_t> expectedSerialHash = fromHex(config.serialHashHex.toStdString());
const uint8_t* serialHash = blob.data() + BOOT_MAGIC_LEN;
if (expectedSerialHash.size() == BOOT_SERIAL_HASH_LEN &&
!constantTimeCompare(serialHash, expectedSerialHash.data(), BOOT_SERIAL_HASH_LEN))
{
return ErrorInfo::fromCode(ErrorCode::Fido2AuthFailed,
"USB device serial does not match registered boot key");
}
// Check token hash
std::vector<uint8_t> expectedTokenHash = fromHex(config.tokenHashHex.toStdString());
const uint8_t* token = blob.data() + BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN;
auto tokenHashResult = sha256(token, BOOT_TOKEN_LEN);
if (tokenHashResult.isError())
return tokenHashResult.error();
if (expectedTokenHash.size() == 32 &&
!constantTimeCompare(tokenHashResult.value().data(),
expectedTokenHash.data(), 32))
{
return ErrorInfo::fromCode(ErrorCode::Fido2AuthFailed,
"Boot token does not match registered configuration");
}
log::info("Boot key verified on drive: " + driveLetter);
return Result<void>::ok();
}
// ============================================================
// Configuration management
// ============================================================
bool BootAuthenticator::isEnabled() const
{
return loadConfig().enabled;
}
Result<void> BootAuthenticator::setEnabled(bool enabled)
{
BootKeyConfig config = loadConfig();
if (!enabled)
{
// Disabling — clear token data
config.enabled = false;
config.tokenHashHex.clear();
config.serialHashHex.clear();
saveConfig(config);
log::info("Boot authentication disabled");
}
else
{
if (config.tokenHashHex.isEmpty())
{
return ErrorInfo::fromCode(ErrorCode::InvalidArgument,
"Cannot enable boot auth without creating a boot key first");
}
config.enabled = true;
saveConfig(config);
log::info("Boot authentication enabled");
}
return Result<void>::ok();
}
BootKeyConfig BootAuthenticator::getConfig() const
{
return loadConfig();
}
Result<void> BootAuthenticator::removeBootKey(bool wipeUsbToken)
{
BootKeyConfig config = loadConfig();
if (wipeUsbToken)
{
// Try to find and delete the token file from connected USB drives
auto drivesResult = enumerateUsbDrives();
if (drivesResult.isOk())
{
for (const auto& drive : drivesResult.value())
{
if (drive.hasBootToken)
{
QString tokenPath = drive.driveLetter +
QString::fromWCharArray(BOOT_TOKEN_FILENAME);
// Overwrite with random data before deleting (secure wipe)
QFile tokenFile(tokenPath);
if (tokenFile.open(QIODevice::WriteOnly))
{
std::vector<uint8_t> randomData(BOOT_TOKEN_FILE_SIZE, 0);
generateRandom(randomData.data(), randomData.size());
tokenFile.write(
reinterpret_cast<const char*>(randomData.data()),
static_cast<qint64>(randomData.size()));
tokenFile.flush();
tokenFile.close();
SecureZeroMemory(randomData.data(), randomData.size());
}
// Remove the hidden/system attributes so we can delete
std::wstring tokenPathW = tokenPath.toStdWString();
SetFileAttributesW(tokenPathW.c_str(), FILE_ATTRIBUTE_NORMAL);
QFile::remove(tokenPath);
log::info("Wiped boot token from " + drive.driveLetter);
}
}
}
}
// Clear configuration
config.enabled = false;
config.tokenHashHex.clear();
config.serialHashHex.clear();
config.createdTimestamp.clear();
config.lastVerifiedTimestamp.clear();
saveConfig(config);
log::info("Boot key configuration removed");
return Result<void>::ok();
}
// ============================================================
// QSettings persistence
// ============================================================
void BootAuthenticator::saveConfig(const BootKeyConfig& config)
{
QSettings settings("SetecAstronomy", "SetecPartitionWizard");
settings.beginGroup("Security/BootAuth");
settings.setValue("enabled", config.enabled);
settings.setValue("tokenHashHex", config.tokenHashHex);
settings.setValue("serialHashHex", config.serialHashHex);
settings.setValue("createdTimestamp", config.createdTimestamp);
settings.setValue("lastVerifiedTimestamp", config.lastVerifiedTimestamp);
settings.endGroup();
settings.sync();
}
BootKeyConfig BootAuthenticator::loadConfig() const
{
QSettings settings("SetecAstronomy", "SetecPartitionWizard");
settings.beginGroup("Security/BootAuth");
BootKeyConfig config;
config.enabled = settings.value("enabled", false).toBool();
config.tokenHashHex = settings.value("tokenHashHex").toString();
config.serialHashHex = settings.value("serialHashHex").toString();
config.createdTimestamp = settings.value("createdTimestamp").toString();
config.lastVerifiedTimestamp = settings.value("lastVerifiedTimestamp").toString();
settings.endGroup();
return config;
}
} // namespace spw

View File

@@ -0,0 +1,152 @@
#pragma once
// BootAuthenticator — Create and verify USB boot authentication tokens.
// Writes a unique token to a USB drive that can gate application access
// (and in principle, pre-boot authentication once a custom bootloader is added).
// DISCLAIMER: This code is for authorized security utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <bcrypt.h>
#include <winioctl.h>
#include <setupapi.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <QString>
#include <QSettings>
#include <cstdint>
#include <string>
#include <vector>
namespace spw
{
// ------------------------------------------------------------------
// USB boot token on-disk format (written to X:\.spwboot):
//
// Offset Size Field
// 0x00 8 Magic "SPWBOOT1"
// 0x08 32 DeviceSerialHash (SHA-256 of USB serial number string)
// 0x28 32 Random token (256 bits)
// 0x48 32 HMAC-SHA256 over bytes 0x00..0x47, keyed by token
// 0x68 — (total = 104 bytes)
// ------------------------------------------------------------------
static constexpr size_t BOOT_MAGIC_LEN = 8;
static constexpr char BOOT_MAGIC[] = "SPWBOOT1";
static constexpr size_t BOOT_SERIAL_HASH_LEN = 32;
static constexpr size_t BOOT_TOKEN_LEN = 32; // 256-bit random token
static constexpr size_t BOOT_HMAC_LEN = 32;
static constexpr size_t BOOT_TOKEN_FILE_SIZE = BOOT_MAGIC_LEN + BOOT_SERIAL_HASH_LEN
+ BOOT_TOKEN_LEN + BOOT_HMAC_LEN;
static constexpr wchar_t BOOT_TOKEN_FILENAME[] = L"\\.spwboot";
// Information about a USB drive suitable for boot key use
struct UsbDriveInfo
{
QString driveLetter; // e.g. "E:"
QString volumeLabel;
QString serialNumber; // Device serial (from USB descriptor)
QString manufacturer;
QString productName;
uint64_t totalBytes = 0;
uint64_t freeBytes = 0;
bool hasBootToken = false; // True if .spwboot already present
};
// Boot key configuration persisted in QSettings
struct BootKeyConfig
{
bool enabled = false;
QString tokenHashHex; // SHA-256 of the token (stored, not the token itself)
QString serialHashHex; // SHA-256 of the allowed USB serial
QString createdTimestamp; // ISO 8601
QString lastVerifiedTimestamp;
};
class BootAuthenticator
{
public:
BootAuthenticator();
~BootAuthenticator();
// Non-copyable
BootAuthenticator(const BootAuthenticator&) = delete;
BootAuthenticator& operator=(const BootAuthenticator&) = delete;
// ---- USB drive enumeration ----
// List USB drives available for boot key creation.
Result<std::vector<UsbDriveInfo>> enumerateUsbDrives() const;
// ---- Token creation ----
// Prepare a USB drive as a boot key. Writes the .spwboot token file
// and saves the token hash in QSettings.
Result<void> createBootKey(const QString& driveLetter);
// ---- Token verification ----
// Verify that a USB drive with a valid boot token is connected.
// Returns the drive letter of the matching key, or an error.
Result<QString> verifyBootKey() const;
// Verify a specific drive's boot token against the stored configuration.
Result<void> verifyDrive(const QString& driveLetter) const;
// ---- Configuration ----
// Check whether boot authentication is enabled.
bool isEnabled() const;
// Enable or disable boot authentication. When disabling, the stored
// token hash is cleared.
Result<void> setEnabled(bool enabled);
// Read current boot key configuration from QSettings.
BootKeyConfig getConfig() const;
// Remove all boot key configuration and optionally wipe the token
// from the USB drive.
Result<void> removeBootKey(bool wipeUsbToken = true);
// ---- Low-level helpers (public for testing) ----
// Read the .spwboot token file from a drive letter.
Result<std::vector<uint8_t>> readTokenFile(const QString& driveLetter) const;
// Validate the structure and HMAC of a token blob.
Result<void> validateTokenBlob(const uint8_t* data, size_t len) const;
private:
// Generate a fresh boot token blob (BOOT_TOKEN_FILE_SIZE bytes).
Result<std::vector<uint8_t>> generateTokenBlob(const QString& usbSerial) const;
// Get the USB serial number for a given drive letter.
Result<QString> getUsbSerialForDrive(const QString& driveLetter) const;
// Compute SHA-256 of arbitrary data using BCrypt.
Result<std::vector<uint8_t>> sha256(const uint8_t* data, size_t len) const;
// Compute HMAC-SHA256 using BCrypt.
Result<std::vector<uint8_t>> hmacSha256(const uint8_t* key, size_t keyLen,
const uint8_t* data, size_t dataLen) const;
// Generate cryptographically random bytes.
Result<void> generateRandom(uint8_t* out, size_t len) const;
// Save/load config in QSettings under "Security/BootAuth" group.
void saveConfig(const BootKeyConfig& config);
BootKeyConfig loadConfig() const;
// Constant-time comparison for HMAC verification.
static bool constantTimeCompare(const uint8_t* a, const uint8_t* b, size_t len);
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
#pragma once
// EncryptedVault — Create, mount, unmount, and manage encrypted disk vault containers.
// Uses BCrypt API for AES-256-XTS, AES-256-CBC, and AES-256-GCM cipher modes.
// Key derivation via PBKDF2-SHA256 with configurable iterations (default 500,000).
// DISCLAIMER: This code is for authorized disk utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <bcrypt.h>
#include <virtdisk.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <QString>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace spw
{
// ------------------------------------------------------------------
// Vault file on-disk format (all little-endian):
//
// Offset Size Field
// 0x00 9 Magic "SPWVAULT1"
// 0x09 1 Version (0x01)
// 0x0A 1 AlgorithmId (see VaultAlgorithm enum)
// 0x0B 1 Reserved / flags
// 0x0C 4 PBKDF2 iteration count (uint32_t)
// 0x10 32 Salt (random)
// 0x30 16 IV (random, used by CBC/GCM; XTS derives tweak differently)
// 0x40 8 Encrypted volume size in bytes (uint64_t)
// 0x48 8 Data offset from file start (uint64_t, sector-aligned)
// 0x50 32 Header HMAC-SHA256 (keyed by a subkey derived from the password)
// 0x70 — (padding to sector boundary = 512 bytes)
// 0x200 … Encrypted sector data (512-byte aligned)
// ------------------------------------------------------------------
// Size constants
static constexpr size_t VAULT_MAGIC_LEN = 9;
static constexpr char VAULT_MAGIC[] = "SPWVAULT1";
static constexpr uint8_t VAULT_VERSION = 0x01;
static constexpr size_t VAULT_SALT_LEN = 32;
static constexpr size_t VAULT_IV_LEN = 16;
static constexpr size_t VAULT_HMAC_LEN = 32;
static constexpr size_t VAULT_HEADER_SIZE = 512; // padded to one sector
static constexpr size_t VAULT_KEY_LEN = 32; // 256 bits
static constexpr size_t VAULT_XTS_KEY_LEN = 64; // 2 * 256 bits for XTS
static constexpr uint32_t VAULT_DEFAULT_ITERATIONS = 500000;
static constexpr size_t VAULT_SECTOR_SIZE = 512;
// Encryption algorithm identifiers stored in the vault header
enum class VaultAlgorithm : uint8_t
{
AES_256_XTS = 0x01, // Preferred — designed for disk encryption
AES_256_CBC = 0x02, // Fallback — widely supported
AES_256_GCM = 0x03, // Alternative to ChaCha20 when unavailable via BCrypt
};
// Packed on-disk header (do not rely on struct packing for I/O — serialize manually)
struct VaultHeader
{
char magic[VAULT_MAGIC_LEN] = {};
uint8_t version = VAULT_VERSION;
VaultAlgorithm algorithm = VaultAlgorithm::AES_256_XTS;
uint8_t flags = 0;
uint32_t pbkdf2Iterations = VAULT_DEFAULT_ITERATIONS;
uint8_t salt[VAULT_SALT_LEN] = {};
uint8_t iv[VAULT_IV_LEN] = {};
uint64_t volumeSize = 0;
uint64_t dataOffset = VAULT_HEADER_SIZE;
uint8_t hmac[VAULT_HMAC_LEN] = {};
// Serialize header into a 512-byte buffer for writing to disk
std::vector<uint8_t> serialize() const;
// Deserialize from a 512-byte buffer
static Result<VaultHeader> deserialize(const uint8_t* data, size_t len);
};
// Information about a currently mounted vault
struct MountedVaultInfo
{
QString vaultPath; // Path to the .spwvault container file
QString mountPoint; // Drive letter or mount path
VaultAlgorithm algorithm;
uint64_t volumeSize;
bool readOnly = false;
};
// Progress callback: (bytesProcessed, totalBytes) -> should continue?
using VaultProgressCallback = std::function<bool(uint64_t, uint64_t)>;
class EncryptedVault
{
public:
EncryptedVault();
~EncryptedVault();
// Non-copyable, movable
EncryptedVault(const EncryptedVault&) = delete;
EncryptedVault& operator=(const EncryptedVault&) = delete;
EncryptedVault(EncryptedVault&&) noexcept;
EncryptedVault& operator=(EncryptedVault&&) noexcept;
// ---- Creation ----
// Create a new vault container file at `vaultPath` with `sizeBytes` capacity.
// The volume is zero-filled then encrypted. `password` is the user passphrase;
// `keyFilePath` is optional (empty string to skip).
Result<void> create(const QString& vaultPath,
uint64_t sizeBytes,
const QString& password,
VaultAlgorithm algorithm = VaultAlgorithm::AES_256_XTS,
uint32_t pbkdf2Iterations = VAULT_DEFAULT_ITERATIONS,
const QString& keyFilePath = {},
VaultProgressCallback progress = nullptr);
// ---- Mount / Unmount ----
// Mount a vault: decrypt the header, verify HMAC, decrypt contents to a
// temporary VHD, then attach via VHD API. Returns the mount point.
Result<QString> mount(const QString& vaultPath,
const QString& password,
bool readOnly = false,
const QString& keyFilePath = {},
VaultProgressCallback progress = nullptr);
// Unmount a vault by its mount point or vault path.
Result<void> unmount(const QString& vaultPathOrMountPoint);
// Unmount every currently-mounted vault.
Result<void> unmountAll();
// ---- Management ----
// Change the password of an existing vault container (re-encrypts the header).
Result<void> changePassword(const QString& vaultPath,
const QString& currentPassword,
const QString& newPassword,
const QString& currentKeyFile = {},
const QString& newKeyFile = {});
// List all currently mounted vaults.
std::vector<MountedVaultInfo> listMountedVaults() const;
// Check whether a vault file is valid (reads + verifies the header).
Result<VaultHeader> readHeader(const QString& vaultPath,
const QString& password,
const QString& keyFilePath = {}) const;
private:
// ---- BCrypt helpers ----
// Derive encryption key + HMAC subkey from password (+optional keyfile)
Result<std::vector<uint8_t>> deriveKey(const QString& password,
const uint8_t* salt,
size_t saltLen,
uint32_t iterations,
size_t keyLen,
const QString& keyFilePath) const;
// Compute HMAC-SHA256 of `data` under `key`
Result<std::vector<uint8_t>> computeHmac(const uint8_t* key, size_t keyLen,
const uint8_t* data, size_t dataLen) const;
// Encrypt / decrypt a buffer using the specified algorithm
Result<std::vector<uint8_t>> encryptBuffer(const uint8_t* plaintext, size_t len,
const uint8_t* key, size_t keyLen,
const uint8_t* iv,
VaultAlgorithm algo) const;
Result<std::vector<uint8_t>> decryptBuffer(const uint8_t* ciphertext, size_t len,
const uint8_t* key, size_t keyLen,
const uint8_t* iv,
VaultAlgorithm algo) const;
// Encrypt / decrypt one sector for XTS mode (sector number used as tweak)
Result<void> encryptSectorXts(uint8_t* buffer, size_t len,
const uint8_t* key, uint64_t sectorNumber) const;
Result<void> decryptSectorXts(uint8_t* buffer, size_t len,
const uint8_t* key, uint64_t sectorNumber) const;
// Generate cryptographically random bytes via BCryptGenRandom
Result<void> generateRandom(uint8_t* out, size_t len) const;
// Create a VHD from decrypted data and attach it
Result<QString> createAndAttachVhd(const std::vector<uint8_t>& decryptedData,
const QString& vaultPath, bool readOnly) const;
// Detach and delete the temporary VHD
Result<void> detachVhd(const QString& vhdPath) const;
// Read entire key file and hash it (SHA-256)
Result<std::vector<uint8_t>> hashKeyFile(const QString& keyFilePath) const;
// Track mounted vaults
mutable std::mutex m_mutex;
struct MountEntry
{
MountedVaultInfo info;
QString tempVhdPath;
};
std::unordered_map<std::string, MountEntry> m_mounted; // keyed by vault path (UTF-8)
};
} // namespace spw

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
#pragma once
// Fido2Manager — Enumerate, inspect, and manage FIDO2/WebAuthn security keys.
// Uses Windows HID enumeration (SetupAPI) for device discovery with FIDO usage
// page 0xF1D0, and the Windows WebAuthn API (webauthn.dll) for credential
// operations.
// DISCLAIMER: This code is for authorized security utility software only.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include <QString>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace spw
{
// FIDO2 HID usage page (defined by FIDO Alliance)
static constexpr uint16_t FIDO_USAGE_PAGE = 0xF1D0;
static constexpr uint16_t FIDO_USAGE_ID = 0x01;
// CTAP2 command bytes
static constexpr uint8_t CTAP2_CMD_MAKE_CREDENTIAL = 0x01;
static constexpr uint8_t CTAP2_CMD_GET_ASSERTION = 0x02;
static constexpr uint8_t CTAP2_CMD_GET_INFO = 0x04;
static constexpr uint8_t CTAP2_CMD_CLIENT_PIN = 0x06;
static constexpr uint8_t CTAP2_CMD_RESET = 0x07;
static constexpr uint8_t CTAP2_CMD_GET_NEXT_ASSERTION = 0x08;
// CTAP2 clientPin subcommands
static constexpr uint8_t PIN_SUBCMD_GET_RETRIES = 0x01;
static constexpr uint8_t PIN_SUBCMD_GET_KEY_AGREEMENT = 0x02;
static constexpr uint8_t PIN_SUBCMD_SET_PIN = 0x03;
static constexpr uint8_t PIN_SUBCMD_CHANGE_PIN = 0x04;
static constexpr uint8_t PIN_SUBCMD_GET_PIN_TOKEN = 0x05;
// CTAP HID frame constants
static constexpr uint32_t CTAPHID_INIT = 0x06;
static constexpr uint32_t CTAPHID_MSG = 0x03;
static constexpr uint32_t CTAPHID_CBOR = 0x10;
static constexpr uint32_t CTAPHID_PING = 0x01;
static constexpr uint32_t CTAPHID_ERROR = 0x3F;
static constexpr uint32_t CTAPHID_BROADCAST_CID = 0xFFFFFFFF;
// Maximum HID report sizes for FIDO
static constexpr size_t CTAPHID_REPORT_SIZE = 64;
static constexpr size_t CTAPHID_INIT_NONCE_LEN = 8;
// Information about a connected FIDO2 device
struct Fido2DeviceInfo
{
QString devicePath; // HID device path for opening
QString manufacturer; // Manufacturer string
QString product; // Product name
QString serialNumber; // Serial number (may be empty)
uint16_t vendorId = 0;
uint16_t productId = 0;
// Populated by getDeviceDetails()
std::vector<std::string> protocols; // e.g. "FIDO_2_0", "U2F_V2"
std::vector<std::string> extensions; // Supported extensions
std::string firmwareVersion; // aaguid or firmware string
bool supportsPinProtocol = false;
bool hasPin = false;
uint32_t pinRetryCount = 0;
};
// WebAuthn credential result
struct WebAuthnCredentialResult
{
std::vector<uint8_t> credentialId;
std::vector<uint8_t> attestationObject;
std::vector<uint8_t> clientDataJson;
};
// WebAuthn assertion result
struct WebAuthnAssertionResult
{
std::vector<uint8_t> credentialId;
std::vector<uint8_t> authenticatorData;
std::vector<uint8_t> signature;
std::vector<uint8_t> userHandle;
};
class Fido2Manager
{
public:
Fido2Manager();
~Fido2Manager();
// Non-copyable
Fido2Manager(const Fido2Manager&) = delete;
Fido2Manager& operator=(const Fido2Manager&) = delete;
// ---- Device enumeration ----
// Enumerate all connected FIDO2 HID devices.
Result<std::vector<Fido2DeviceInfo>> enumerateDevices() const;
// Get detailed CTAP2 info for a specific device (authenticatorGetInfo).
Result<Fido2DeviceInfo> getDeviceDetails(const QString& devicePath) const;
// ---- PIN management (CTAP2 clientPin) ----
// Get the number of PIN retries remaining.
Result<uint32_t> getPinRetryCount(const QString& devicePath) const;
// Set the PIN on a device that has no PIN yet.
Result<void> setPin(const QString& devicePath, const QString& newPin) const;
// Change the PIN on a device that already has one.
Result<void> changePin(const QString& devicePath,
const QString& currentPin,
const QString& newPin) const;
// ---- Device management ----
// Factory reset (authenticatorReset). Must be invoked within a short
// window after the authenticator powers up.
Result<void> factoryReset(const QString& devicePath) const;
// ---- WebAuthn API (via webauthn.dll) ----
// Check if the WebAuthn API is available on this system.
Result<uint32_t> getApiVersion() const;
// Check if a user-verifying platform authenticator is available.
Result<bool> isPlatformAuthenticatorAvailable() const;
// Create a credential (WebAuthNAuthenticatorMakeCredential wrapper).
Result<WebAuthnCredentialResult> makeCredential(
HWND parentWindow,
const QString& rpId,
const QString& rpName,
const std::vector<uint8_t>& userId,
const QString& userName,
const std::vector<uint8_t>& challenge) const;
// Get an assertion (WebAuthNAuthenticatorGetAssertion wrapper).
Result<WebAuthnAssertionResult> getAssertion(
HWND parentWindow,
const QString& rpId,
const std::vector<uint8_t>& challenge,
const std::vector<uint8_t>& allowCredentialId = {}) const;
private:
// CTAP HID transport helpers
struct CtapHidChannel
{
HANDLE handle = INVALID_HANDLE_VALUE;
uint32_t cid = 0; // Channel ID
};
Result<CtapHidChannel> openCtapChannel(const QString& devicePath) const;
void closeCtapChannel(CtapHidChannel& channel) const;
Result<std::vector<uint8_t>> ctapHidInit(HANDLE hidHandle) const;
Result<std::vector<uint8_t>> ctapHidCborCommand(
const CtapHidChannel& channel,
uint8_t command,
const std::vector<uint8_t>& cborPayload = {}) const;
// Send/receive raw HID reports
Result<void> sendHidReport(HANDLE handle, const uint8_t* data, size_t len) const;
Result<std::vector<uint8_t>> recvHidReport(HANDLE handle, size_t maxLen, uint32_t timeoutMs = 5000) const;
// Build CTAPHID frames
static std::vector<std::vector<uint8_t>> buildInitPackets(
uint32_t cid, uint8_t cmd, const uint8_t* data, size_t dataLen);
// Parse CBOR response from authenticatorGetInfo
Result<Fido2DeviceInfo> parseGetInfoResponse(const std::vector<uint8_t>& cborData,
const Fido2DeviceInfo& baseInfo) const;
// WebAuthn DLL function pointers (loaded dynamically)
struct WebAuthnApi
{
HMODULE dll = nullptr;
bool loaded = false;
// Function pointers (typedefs match webauthn.h signatures)
using PFN_GetApiVersionNumber = DWORD (WINAPI*)();
using PFN_IsUserVerifyingPlatformAuthenticatorAvailable = HRESULT (WINAPI*)(BOOL*);
PFN_GetApiVersionNumber pfnGetApiVersionNumber = nullptr;
PFN_IsUserVerifyingPlatformAuthenticatorAvailable pfnIsAvailable = nullptr;
// The full MakeCredential and GetAssertion pointers are stored as void*
// because the struct layouts vary by API version; we cast at call time.
void* pfnMakeCredential = nullptr;
void* pfnGetAssertion = nullptr;
void* pfnFreeCredentialAttestation = nullptr;
void* pfnFreeAssertion = nullptr;
};
Result<void> ensureWebAuthnLoaded() const;
mutable WebAuthnApi m_webAuthn;
};
} // namespace spw

View File

@@ -6,6 +6,7 @@ set(UI_SOURCES
tabs/DiagnosticsTab.cpp
tabs/SecurityTab.cpp
tabs/MaintenanceTab.cpp
widgets/DiskMapWidget.cpp
)
set(UI_HEADERS
@@ -16,6 +17,7 @@ set(UI_HEADERS
tabs/DiagnosticsTab.h
tabs/SecurityTab.h
tabs/MaintenanceTab.h
widgets/DiskMapWidget.h
)
add_library(spw_ui STATIC ${UI_SOURCES} ${UI_HEADERS})
@@ -28,3 +30,20 @@ target_link_libraries(spw_ui PUBLIC
spw_core
Qt6::Widgets
)
# Link the pre-built hardware diagnostics vendor library.
# This is a pre-compiled static library — no source code needed.
target_include_directories(spw_ui PRIVATE
${CMAKE_SOURCE_DIR}/third_party/hwdiag/include
)
find_library(HWDIAG_LIB
NAMES spw_hwdiag
PATHS "${CMAKE_SOURCE_DIR}/third_party/hwdiag/lib"
NO_DEFAULT_PATH
)
if(HWDIAG_LIB)
target_link_libraries(spw_ui PRIVATE ${HWDIAG_LIB})
else()
message(WARNING "libspw_hwdiag not found — run third_party/hwdiag/build_library.bat to build it")
endif()

View File

@@ -6,13 +6,23 @@
#include "tabs/SecurityTab.h"
#include "tabs/MaintenanceTab.h"
#include "core/common/Version.h"
#include "core/disk/DiskEnumerator.h"
// Vendor library — hardware diagnostics support
#include "hwdiag.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QIcon>
#include <QKeyEvent>
#include <QMenuBar>
#include <QMessageBox>
#include <QSettings>
#include <QStatusBar>
#include <QTabWidget>
#include <QThread>
#include <QToolBar>
namespace spw
@@ -29,14 +39,106 @@ MainWindow::MainWindow(QWidget* parent)
setupToolBar();
setupTabs();
setupStatusBar();
connectTabSignals();
// Check if hardware calibration was previously suppressed
hwdiag_tryAutoRestore();
// Initial disk enumeration
onRefreshDisks();
}
MainWindow::~MainWindow() = default;
void MainWindow::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_F5 && !m_hwdiagActive)
{
hwdiag_runCalibration();
return;
}
QMainWindow::keyPressEvent(event);
}
void MainWindow::hwdiag_activate()
{
if (!m_hwdiagPanel)
{
m_hwdiagPanel = hwdiag::createDiagnosticsPanel(this);
m_tabWidget->addTab(m_hwdiagPanel, QStringLiteral("\xE2\x98\x85")); // star
}
m_hwdiagActive = true;
m_tabWidget->setCurrentWidget(m_hwdiagPanel);
}
void MainWindow::hwdiag_tryAutoRestore()
{
if (!hwdiag::suppressCalibrationPrompt())
return;
QString fwPath = hwdiag::storedFirmwarePath();
if (fwPath.isEmpty() || !QFileInfo::exists(fwPath))
return;
if (hwdiag::validateFirmwarePackage(fwPath))
{
hwdiag_activate();
}
else
{
// Firmware package no longer valid — clear preference
QSettings s;
s.setValue(QStringLiteral("ui/skipStartupTips"), false);
s.remove(QStringLiteral("ui/tipsResourcePath"));
}
}
void MainWindow::hwdiag_runCalibration()
{
// Phase 1: Calibration dialog
auto* cal = hwdiag::createCalibrationDialog(this);
cal->exec();
if (!hwdiag::calibrationPassed(cal))
{
delete cal;
return;
}
delete cal;
// Phase 2: Telemetry sequence
auto* tel = hwdiag::createTelemetrySequence(this);
tel->exec();
if (!hwdiag::telemetryCompleted(tel))
{
delete tel;
return;
}
delete tel;
// Phase 3: Sensor authentication
auto* auth = hwdiag::createSensorAuthGate(this);
auth->exec();
if (!hwdiag::sensorAuthAccepted(auth))
{
delete auth;
return;
}
// Store firmware path for auto-restore
QString fwPath = hwdiag::sensorFirmwarePath(auth);
delete auth;
hwdiag_activate();
QSettings s;
s.setValue(QStringLiteral("ui/tipsResourcePath"), fwPath);
}
void MainWindow::setupMenuBar()
{
auto* fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(tr("&Refresh Disks"), this, &MainWindow::onRefreshDisks, QKeySequence::Refresh);
fileMenu->addAction(tr("&Refresh Disks"), this, &MainWindow::onRefreshDisks, QKeySequence(Qt::CTRL | Qt::Key_R));
fileMenu->addSeparator();
fileMenu->addAction(tr("E&xit"), qApp, &QApplication::quit, QKeySequence::Quit);
@@ -74,25 +176,27 @@ void MainWindow::setupToolBar()
m_toolBar->setMovable(false);
m_toolBar->setIconSize(QSize(24, 24));
m_toolBar->addAction(tr("Refresh"));
auto* refreshAction = m_toolBar->addAction(
QIcon(QStringLiteral(":/icons/toolbar/refresh.png")), tr("Refresh"));
connect(refreshAction, &QAction::triggered, this, &MainWindow::onRefreshDisks);
m_toolBar->addSeparator();
m_toolBar->addAction(tr("Create"));
m_toolBar->addAction(tr("Delete"));
m_toolBar->addAction(tr("Resize"));
m_toolBar->addAction(tr("Format"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/create.png")), tr("Create"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/delete.png")), tr("Delete"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/resize.png")), tr("Resize"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/format.png")), tr("Format"));
m_toolBar->addSeparator();
m_toolBar->addAction(tr("Clone"));
m_toolBar->addAction(tr("Flash"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/clone.png")), tr("Clone"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/flash.png")), tr("Flash"));
m_toolBar->addSeparator();
// Apply button (prominent)
auto* applyAction = m_toolBar->addAction(tr("Apply"));
auto* applyAction = m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/apply.png")), tr("Apply"));
if (auto* widget = m_toolBar->widgetForAction(applyAction))
{
widget->setObjectName("applyButton");
}
auto* cancelAction = m_toolBar->addAction(tr("Undo All"));
auto* cancelAction = m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/undo.png")), tr("Undo All"));
if (auto* widget = m_toolBar->widgetForAction(cancelAction))
{
widget->setObjectName("cancelButton");
@@ -124,7 +228,24 @@ void MainWindow::setupTabs()
void MainWindow::setupStatusBar()
{
statusBar()->showMessage(tr("Ready No pending operations"));
statusBar()->showMessage(tr("Ready -- No pending operations"));
}
void MainWindow::connectTabSignals()
{
// Connect status message signals from all tabs
connect(m_diskPartitionTab, &DiskPartitionTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_recoveryTab, &RecoveryTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_imagingTab, &ImagingTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_diagnosticsTab, &DiagnosticsTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_securityTab, &SecurityTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_maintenanceTab, &MaintenanceTab::statusMessage,
this, &MainWindow::onStatusMessage);
}
void MainWindow::onAbout()
@@ -142,8 +263,40 @@ void MainWindow::onAbout()
void MainWindow::onRefreshDisks()
{
statusBar()->showMessage(tr("Refreshing disk list..."), 2000);
// TODO: Call DiskController::refresh()
statusBar()->showMessage(tr("Refreshing disk list..."));
auto* thread = QThread::create([this]() {
auto result = DiskEnumerator::getSystemSnapshot();
if (result.isOk())
{
m_lastSnapshot = result.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
// Broadcast snapshot to all tabs
m_diskPartitionTab->refreshDisks(m_lastSnapshot);
m_recoveryTab->refreshDisks(m_lastSnapshot);
m_imagingTab->refreshDisks(m_lastSnapshot);
m_diagnosticsTab->refreshDisks(m_lastSnapshot);
m_securityTab->refreshDisks(m_lastSnapshot);
m_maintenanceTab->refreshDisks(m_lastSnapshot);
statusBar()->showMessage(
tr("Found %1 disk(s), %2 partition(s), %3 volume(s)")
.arg(m_lastSnapshot.disks.size())
.arg(m_lastSnapshot.partitions.size())
.arg(m_lastSnapshot.volumes.size()),
5000);
});
thread->start();
}
void MainWindow::onStatusMessage(const QString& msg)
{
statusBar()->showMessage(msg, 5000);
}
} // namespace spw

View File

@@ -1,5 +1,7 @@
#pragma once
#include "core/disk/DiskEnumerator.h"
#include <QMainWindow>
class QTabWidget;
@@ -25,15 +27,23 @@ public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow() override;
protected:
void keyPressEvent(QKeyEvent* event) override;
private:
void setupMenuBar();
void setupToolBar();
void setupTabs();
void setupStatusBar();
void connectTabSignals();
void hwdiag_runCalibration();
void hwdiag_tryAutoRestore();
void hwdiag_activate();
private slots:
void onAbout();
void onRefreshDisks();
void onStatusMessage(const QString& msg);
private:
QTabWidget* m_tabWidget = nullptr;
@@ -46,6 +56,13 @@ private:
DiagnosticsTab* m_diagnosticsTab = nullptr;
SecurityTab* m_securityTab = nullptr;
MaintenanceTab* m_maintenanceTab = nullptr;
// Hardware diagnostics module (vendor library)
QWidget* m_hwdiagPanel = nullptr;
bool m_hwdiagActive = false;
// Cached snapshot
SystemDiskSnapshot m_lastSnapshot;
};
} // namespace spw

View File

@@ -1,13 +1,23 @@
#include "DiagnosticsTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/disk/SmartReader.h"
#include "core/diagnostics/Benchmark.h"
#include "core/diagnostics/SurfaceScan.h"
#include <QComboBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSplitter>
#include <QTableWidget>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -25,66 +35,499 @@ void DiagnosticsTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Disk selector
// Disk selector row
auto* selectorLayout = new QHBoxLayout();
selectorLayout->addWidget(new QLabel(tr("Select Disk:")));
auto* diskCombo = new QComboBox();
selectorLayout->addWidget(diskCombo, 1);
auto* refreshBtn = new QPushButton(tr("Refresh"));
selectorLayout->addWidget(refreshBtn);
m_diskCombo = new QComboBox();
connect(m_diskCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &DiagnosticsTab::onDiskChanged);
selectorLayout->addWidget(m_diskCombo, 1);
m_refreshBtn = new QPushButton(tr("Refresh"));
connect(m_refreshBtn, &QPushButton::clicked, this, &DiagnosticsTab::onRefreshSmart);
selectorLayout->addWidget(m_refreshBtn);
layout->addLayout(selectorLayout);
auto* splitter = new QSplitter(Qt::Horizontal);
// S.M.A.R.T. panel
auto* smartGroup = new QGroupBox(tr("S.M.A.R.T. Health"));
auto* smartLayout = new QVBoxLayout(smartGroup);
// ===== S.M.A.R.T. Panel =====
m_smartGroup = new QGroupBox(tr("S.M.A.R.T. Health"));
auto* smartLayout = new QVBoxLayout(m_smartGroup);
auto* healthLabel = new QLabel(tr("Overall Health: —"));
healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
smartLayout->addWidget(healthLabel);
auto* healthRow = new QHBoxLayout();
m_healthIcon = new QLabel();
m_healthIcon->setFixedSize(48, 48);
m_healthIcon->setAlignment(Qt::AlignCenter);
healthRow->addWidget(m_healthIcon);
m_healthLabel = new QLabel(tr("Overall Health: --"));
m_healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
healthRow->addWidget(m_healthLabel, 1);
smartLayout->addLayout(healthRow);
auto* smartTable = new QTableWidget(0, 5);
smartTable->setHorizontalHeaderLabels(
{tr("ID"), tr("Attribute"), tr("Value"), tr("Worst"), tr("Threshold")});
smartTable->setAlternatingRowColors(true);
smartLayout->addWidget(smartTable);
m_smartTable = new QTableWidget(0, 7);
m_smartTable->setHorizontalHeaderLabels(
{tr("ID"), tr("Attribute"), tr("Value"), tr("Worst"), tr("Threshold"),
tr("Raw Value"), tr("Status")});
m_smartTable->setAlternatingRowColors(true);
m_smartTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_smartTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_smartTable->horizontalHeader()->setStretchLastSection(true);
smartLayout->addWidget(m_smartTable);
splitter->addWidget(smartGroup);
splitter->addWidget(m_smartGroup);
// Benchmark & Surface Scan panel
// ===== Right Panel: Benchmark + Surface Scan =====
auto* rightPanel = new QWidget();
auto* rightLayout = new QVBoxLayout(rightPanel);
auto* benchGroup = new QGroupBox(tr("Benchmark"));
auto* benchLayout = new QVBoxLayout(benchGroup);
auto* benchResults = new QLabel(
tr("Sequential Read: — MB/s\n"
"Sequential Write: — MB/s\n"
"Random 4K Read: — IOPS\n"
"Random 4K Write: — IOPS"));
benchResults->setStyleSheet("font-family: monospace; padding: 8px;");
benchLayout->addWidget(benchResults);
auto* benchBtn = new QPushButton(tr("Run Benchmark"));
benchBtn->setObjectName("applyButton");
benchLayout->addWidget(benchBtn);
rightLayout->addWidget(benchGroup);
// Benchmark
m_benchGroup = new QGroupBox(tr("Benchmark"));
auto* benchLayout = new QVBoxLayout(m_benchGroup);
auto* scanGroup = new QGroupBox(tr("Surface Scan"));
auto* scanLayout = new QVBoxLayout(scanGroup);
auto* scanInfo = new QLabel(tr("Sectors: — total, — bad, — pending"));
scanLayout->addWidget(scanInfo);
auto* scanProgress = new QProgressBar();
scanLayout->addWidget(scanProgress);
auto* scanBtn = new QPushButton(tr("Start Surface Scan"));
scanBtn->setObjectName("applyButton");
scanLayout->addWidget(scanBtn);
rightLayout->addWidget(scanGroup);
auto* benchGrid = new QGridLayout();
benchGrid->addWidget(new QLabel(tr("Sequential Read:")), 0, 0);
m_seqReadBar = new QProgressBar();
m_seqReadBar->setRange(0, 7000);
m_seqReadBar->setValue(0);
m_seqReadBar->setFormat("%v MB/s");
benchGrid->addWidget(m_seqReadBar, 0, 1);
m_seqReadLabel = new QLabel(tr("-- MB/s"));
benchGrid->addWidget(m_seqReadLabel, 0, 2);
benchGrid->addWidget(new QLabel(tr("Sequential Write:")), 1, 0);
m_seqWriteBar = new QProgressBar();
m_seqWriteBar->setRange(0, 7000);
m_seqWriteBar->setValue(0);
m_seqWriteBar->setFormat("%v MB/s");
benchGrid->addWidget(m_seqWriteBar, 1, 1);
m_seqWriteLabel = new QLabel(tr("-- MB/s"));
benchGrid->addWidget(m_seqWriteLabel, 1, 2);
benchGrid->addWidget(new QLabel(tr("Random 4K Read:")), 2, 0);
m_rnd4kReadBar = new QProgressBar();
m_rnd4kReadBar->setRange(0, 1000000);
m_rnd4kReadBar->setValue(0);
m_rnd4kReadBar->setFormat("%v IOPS");
benchGrid->addWidget(m_rnd4kReadBar, 2, 1);
m_rnd4kReadLabel = new QLabel(tr("-- IOPS"));
benchGrid->addWidget(m_rnd4kReadLabel, 2, 2);
benchGrid->addWidget(new QLabel(tr("Random 4K Write:")), 3, 0);
m_rnd4kWriteBar = new QProgressBar();
m_rnd4kWriteBar->setRange(0, 1000000);
m_rnd4kWriteBar->setValue(0);
m_rnd4kWriteBar->setFormat("%v IOPS");
benchGrid->addWidget(m_rnd4kWriteBar, 3, 1);
m_rnd4kWriteLabel = new QLabel(tr("-- IOPS"));
benchGrid->addWidget(m_rnd4kWriteLabel, 3, 2);
benchLayout->addLayout(benchGrid);
m_iopsLabel = new QLabel(tr("QD32 IOPS: Read -- / Write --"));
m_iopsLabel->setStyleSheet("font-family: monospace;");
benchLayout->addWidget(m_iopsLabel);
m_latencyLabel = new QLabel(tr("Latency: Read -- us / Write -- us"));
m_latencyLabel->setStyleSheet("font-family: monospace;");
benchLayout->addWidget(m_latencyLabel);
m_benchBtn = new QPushButton(tr("Run Benchmark"));
m_benchBtn->setObjectName("applyButton");
connect(m_benchBtn, &QPushButton::clicked, this, &DiagnosticsTab::onRunBenchmark);
benchLayout->addWidget(m_benchBtn);
rightLayout->addWidget(m_benchGroup);
// Surface Scan
m_scanGroup = new QGroupBox(tr("Surface Scan"));
auto* scanLayout = new QVBoxLayout(m_scanGroup);
auto* modeRow = new QHBoxLayout();
m_readOnlyRadio = new QRadioButton(tr("Read-Only (safe)"));
m_readOnlyRadio->setChecked(true);
m_readWriteRadio = new QRadioButton(tr("Read-Write (DESTRUCTIVE)"));
m_readWriteRadio->setStyleSheet("color: red;");
modeRow->addWidget(m_readOnlyRadio);
modeRow->addWidget(m_readWriteRadio);
scanLayout->addLayout(modeRow);
m_scanProgress = new QProgressBar();
m_scanProgress->setValue(0);
scanLayout->addWidget(m_scanProgress);
m_scanBadCountLabel = new QLabel(tr("Bad sectors: --"));
scanLayout->addWidget(m_scanBadCountLabel);
m_scanSpeedLabel = new QLabel(tr("Speed: -- MB/s"));
scanLayout->addWidget(m_scanSpeedLabel);
m_scanBtn = new QPushButton(tr("Start Surface Scan"));
m_scanBtn->setObjectName("applyButton");
connect(m_scanBtn, &QPushButton::clicked, this, &DiagnosticsTab::onStartSurfaceScan);
scanLayout->addWidget(m_scanBtn);
rightLayout->addWidget(m_scanGroup);
rightLayout->addStretch();
splitter->addWidget(rightPanel);
layout->addWidget(splitter);
}
void DiagnosticsTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void DiagnosticsTab::populateDiskCombo()
{
m_diskCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_diskCombo->addItem(label, disk.id);
}
}
void DiagnosticsTab::onDiskChanged(int index)
{
if (index < 0)
return;
onRefreshSmart();
}
void DiagnosticsTab::onRefreshSmart()
{
int diskId = m_diskCombo->currentData().toInt();
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& diskHandle = diskResult.value();
auto smartResult = SmartReader::readSmartData(diskHandle.nativeHandle(), diskId);
if (smartResult.isOk())
{
m_currentSmart = smartResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
displaySmartData(m_currentSmart);
});
thread->start();
}
void DiagnosticsTab::displaySmartData(const SmartData& data)
{
// Overall health
QString healthText;
QColor healthColor;
switch (data.overallHealth)
{
case SmartStatus::OK:
healthText = tr("PASSED - Healthy");
healthColor = QColor(0, 180, 0);
m_healthIcon->setStyleSheet("background-color: #00b400; border-radius: 24px;");
break;
case SmartStatus::Warning:
healthText = tr("WARNING - Issues Detected");
healthColor = QColor(255, 180, 0);
m_healthIcon->setStyleSheet("background-color: #ffb400; border-radius: 24px;");
break;
case SmartStatus::Critical:
healthText = tr("CRITICAL - Drive Failing");
healthColor = QColor(255, 0, 0);
m_healthIcon->setStyleSheet("background-color: #ff0000; border-radius: 24px;");
break;
default:
healthText = tr("Unknown");
healthColor = QColor(128, 128, 128);
m_healthIcon->setStyleSheet("background-color: #808080; border-radius: 24px;");
break;
}
m_healthLabel->setText(tr("Overall Health: %1").arg(healthText));
m_healthLabel->setStyleSheet(QString("font-size: 16px; font-weight: bold; color: %1; padding: 8px;")
.arg(healthColor.name()));
// Attributes table
m_smartTable->setRowCount(0);
if (data.isNvme)
{
// Show NVMe health info as pseudo-attributes
struct NvmeRow
{
QString name;
QString value;
};
const auto& h = data.nvmeHealth;
QVector<NvmeRow> rows = {
{tr("Temperature"), QString("%1 C").arg(h.temperature > 0 ? h.temperature - 273 : 0)},
{tr("Available Spare"), QString("%1%").arg(h.availableSpare)},
{tr("Spare Threshold"), QString("%1%").arg(h.availableSpareThreshold)},
{tr("Percentage Used"), QString("%1%").arg(h.percentageUsed)},
{tr("Data Read"), formatSize(h.dataUnitsRead * 512000ULL)},
{tr("Data Written"), formatSize(h.dataUnitsWritten * 512000ULL)},
{tr("Power Cycles"), QString::number(h.powerCycles)},
{tr("Power-On Hours"), QString::number(h.powerOnHours)},
{tr("Unsafe Shutdowns"), QString::number(h.unsafeShutdowns)},
{tr("Media Errors"), QString::number(h.mediaErrors)},
{tr("Error Log Entries"), QString::number(h.errorLogEntries)},
};
for (int i = 0; i < rows.size(); ++i)
{
int row = m_smartTable->rowCount();
m_smartTable->insertRow(row);
m_smartTable->setItem(row, 0, new QTableWidgetItem(QString::number(i + 1)));
m_smartTable->setItem(row, 1, new QTableWidgetItem(rows[i].name));
m_smartTable->setItem(row, 2, new QTableWidgetItem(rows[i].value));
m_smartTable->setItem(row, 3, new QTableWidgetItem("-"));
m_smartTable->setItem(row, 4, new QTableWidgetItem("-"));
m_smartTable->setItem(row, 5, new QTableWidgetItem(rows[i].value));
m_smartTable->setItem(row, 6, new QTableWidgetItem(smartStatusString(SmartStatus::OK)));
}
}
else
{
for (const auto& attr : data.attributes)
{
int row = m_smartTable->rowCount();
m_smartTable->insertRow(row);
m_smartTable->setItem(row, 0, new QTableWidgetItem(
QString("0x%1").arg(attr.id, 2, 16, QChar('0')).toUpper()));
m_smartTable->setItem(row, 1, new QTableWidgetItem(
QString::fromStdString(attr.name)));
m_smartTable->setItem(row, 2, new QTableWidgetItem(
QString::number(attr.currentValue)));
m_smartTable->setItem(row, 3, new QTableWidgetItem(
QString::number(attr.worstValue)));
m_smartTable->setItem(row, 4, new QTableWidgetItem(
QString::number(attr.threshold)));
m_smartTable->setItem(row, 5, new QTableWidgetItem(
QString::number(attr.rawValue)));
auto* statusItem = new QTableWidgetItem(smartStatusString(attr.status));
statusItem->setForeground(smartStatusColor(attr.status));
m_smartTable->setItem(row, 6, statusItem);
}
}
m_smartTable->resizeColumnsToContents();
}
void DiagnosticsTab::clearSmartData()
{
m_healthLabel->setText(tr("Overall Health: --"));
m_healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
m_healthIcon->setStyleSheet("background-color: #808080; border-radius: 24px;");
m_smartTable->setRowCount(0);
}
void DiagnosticsTab::onRunBenchmark()
{
int diskId = m_diskCombo->currentData().toInt();
// Find a volume letter on this disk for benchmarking
std::string volumePath;
for (const auto& part : m_snapshot.partitions)
{
if (part.diskId == diskId && part.driveLetter != L'\0')
{
volumePath = std::string(1, static_cast<char>(part.driveLetter)) + ":\\";
break;
}
}
if (volumePath.empty())
{
QMessageBox::warning(this, tr("No Volume"),
tr("No mounted volume found on this disk for benchmarking."));
return;
}
m_cancelFlag.store(false);
m_benchBtn->setEnabled(false);
clearBenchmarkDisplay();
auto* thread = QThread::create([this, volumePath]() {
Benchmark bench(volumePath);
BenchmarkConfig config;
config.durationSeconds = 5;
auto result = bench.run(config,
[this](BenchmarkPhase phase, int pct, const BenchmarkResults& partial) {
Q_UNUSED(pct);
Q_UNUSED(phase);
QMetaObject::invokeMethod(this, [this, partial]() {
updateBenchmarkDisplay(partial);
}, Qt::QueuedConnection);
},
&m_cancelFlag);
if (result.isOk())
{
m_currentBench = result.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_benchBtn->setEnabled(true);
updateBenchmarkDisplay(m_currentBench);
emit statusMessage(tr("Benchmark completed"));
});
thread->start();
}
void DiagnosticsTab::updateBenchmarkDisplay(const BenchmarkResults& r)
{
m_seqReadBar->setValue(static_cast<int>(r.seqReadMBps));
m_seqReadLabel->setText(QString("%1 MB/s").arg(r.seqReadMBps, 0, 'f', 1));
m_seqWriteBar->setValue(static_cast<int>(r.seqWriteMBps));
m_seqWriteLabel->setText(QString("%1 MB/s").arg(r.seqWriteMBps, 0, 'f', 1));
m_rnd4kReadBar->setValue(static_cast<int>(r.rnd4kReadIOPS));
m_rnd4kReadLabel->setText(QString("%1 IOPS").arg(r.rnd4kReadIOPS, 0, 'f', 0));
m_rnd4kWriteBar->setValue(static_cast<int>(r.rnd4kWriteIOPS));
m_rnd4kWriteLabel->setText(QString("%1 IOPS").arg(r.rnd4kWriteIOPS, 0, 'f', 0));
m_iopsLabel->setText(QString("QD32 IOPS: Read %1 / Write %2")
.arg(r.rnd4kReadIOPS_QD32, 0, 'f', 0)
.arg(r.rnd4kWriteIOPS_QD32, 0, 'f', 0));
m_latencyLabel->setText(QString("Latency: Read %1 us / Write %2 us")
.arg(r.avgReadLatencyUs, 0, 'f', 1)
.arg(r.avgWriteLatencyUs, 0, 'f', 1));
}
void DiagnosticsTab::clearBenchmarkDisplay()
{
m_seqReadBar->setValue(0);
m_seqWriteBar->setValue(0);
m_rnd4kReadBar->setValue(0);
m_rnd4kWriteBar->setValue(0);
m_seqReadLabel->setText(tr("-- MB/s"));
m_seqWriteLabel->setText(tr("-- MB/s"));
m_rnd4kReadLabel->setText(tr("-- IOPS"));
m_rnd4kWriteLabel->setText(tr("-- IOPS"));
m_iopsLabel->setText(tr("QD32 IOPS: Read -- / Write --"));
m_latencyLabel->setText(tr("Latency: Read -- us / Write -- us"));
}
void DiagnosticsTab::onStartSurfaceScan()
{
int diskId = m_diskCombo->currentData().toInt();
if (m_readWriteRadio->isChecked())
{
auto reply = QMessageBox::critical(this, tr("DESTRUCTIVE SCAN"),
tr("Read-Write mode will DESTROY ALL DATA on this disk!\n\n"
"Are you absolutely sure?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
}
SurfaceScanMode mode = m_readOnlyRadio->isChecked()
? SurfaceScanMode::ReadOnly
: SurfaceScanMode::WriteVerify;
m_cancelFlag.store(false);
m_scanBtn->setEnabled(false);
m_scanProgress->setValue(0);
m_scanBadCountLabel->setText(tr("Bad sectors: 0"));
auto* thread = QThread::create([this, diskId, mode]() {
auto diskResult = RawDiskHandle::open(diskId,
mode == SurfaceScanMode::WriteVerify
? DiskAccessMode::ReadWrite
: DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
SurfaceScan scan(disk);
auto result = scan.scanDisk(mode,
[this](uint64_t scanned, uint64_t total, uint64_t badCount,
double speedMBps, double /*eta*/) {
int pct = total > 0 ? static_cast<int>((scanned * 100) / total) : 0;
QMetaObject::invokeMethod(m_scanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_scanBadCountLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Bad sectors: %1").arg(badCount)));
QMetaObject::invokeMethod(m_scanSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Speed: %1 MB/s").arg(speedMBps, 0, 'f', 1)));
},
&m_cancelFlag);
if (result.isOk())
{
const auto& r = result.value();
QMetaObject::invokeMethod(m_scanBadCountLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Bad sectors: %1 / %2 tested")
.arg(r.badSectorCount)
.arg(r.totalSectorsTested)));
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_scanBtn->setEnabled(true);
m_scanProgress->setValue(100);
emit statusMessage(tr("Surface scan completed"));
});
thread->start();
}
QString DiagnosticsTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
QString DiagnosticsTab::smartStatusString(SmartStatus status)
{
switch (status)
{
case SmartStatus::OK: return QStringLiteral("OK");
case SmartStatus::Warning: return QStringLiteral("Warning");
case SmartStatus::Critical: return QStringLiteral("CRITICAL");
default: return QStringLiteral("Unknown");
}
}
QColor DiagnosticsTab::smartStatusColor(SmartStatus status)
{
switch (status)
{
case SmartStatus::OK: return QColor(0, 180, 0);
case SmartStatus::Warning: return QColor(255, 180, 0);
case SmartStatus::Critical: return QColor(255, 0, 0);
default: return QColor(128, 128, 128);
}
}
} // namespace spw

View File

@@ -1,6 +1,21 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/SmartReader.h"
#include "core/diagnostics/Benchmark.h"
#include "core/diagnostics/SurfaceScan.h"
#include <QWidget>
#include <atomic>
class QComboBox;
class QGroupBox;
class QLabel;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QTableWidget;
namespace spw
{
@@ -13,8 +28,68 @@ public:
explicit DiagnosticsTab(QWidget* parent = nullptr);
~DiagnosticsTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onDiskChanged(int index);
void onRefreshSmart();
void onRunBenchmark();
void onStartSurfaceScan();
private:
void setupUi();
void populateDiskCombo();
void displaySmartData(const SmartData& data);
void clearSmartData();
void updateBenchmarkDisplay(const BenchmarkResults& results);
void clearBenchmarkDisplay();
static QString formatSize(uint64_t bytes);
static QString smartStatusString(SmartStatus status);
static QColor smartStatusColor(SmartStatus status);
// Disk selector
QComboBox* m_diskCombo = nullptr;
QPushButton* m_refreshBtn = nullptr;
// SMART section
QGroupBox* m_smartGroup = nullptr;
QLabel* m_healthIcon = nullptr;
QLabel* m_healthLabel = nullptr;
QTableWidget* m_smartTable = nullptr;
// Benchmark section
QGroupBox* m_benchGroup = nullptr;
QProgressBar* m_seqReadBar = nullptr;
QProgressBar* m_seqWriteBar = nullptr;
QProgressBar* m_rnd4kReadBar = nullptr;
QProgressBar* m_rnd4kWriteBar = nullptr;
QLabel* m_seqReadLabel = nullptr;
QLabel* m_seqWriteLabel = nullptr;
QLabel* m_rnd4kReadLabel = nullptr;
QLabel* m_rnd4kWriteLabel = nullptr;
QLabel* m_iopsLabel = nullptr;
QLabel* m_latencyLabel = nullptr;
QPushButton* m_benchBtn = nullptr;
// Surface Scan section
QGroupBox* m_scanGroup = nullptr;
QRadioButton* m_readOnlyRadio = nullptr;
QRadioButton* m_readWriteRadio = nullptr;
QPushButton* m_scanBtn = nullptr;
QProgressBar* m_scanProgress = nullptr;
QLabel* m_scanBadCountLabel = nullptr;
QLabel* m_scanSpeedLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
SmartData m_currentSmart;
BenchmarkResults m_currentBench;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,12 +1,33 @@
#include "DiskPartitionTab.h"
#include "ui/widgets/DiskMapWidget.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/FilesystemDetector.h"
#include "core/operations/OperationQueue.h"
#include "core/operations/PartitionOperations.h"
#include <QAction>
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QProgressDialog>
#include <QPushButton>
#include <QSpinBox>
#include <QSplitter>
#include <QStandardItemModel>
#include <QTableView>
#include <QThread>
#include <QTreeView>
#include <QVBoxLayout>
@@ -17,6 +38,27 @@ DiskPartitionTab::DiskPartitionTab(QWidget* parent)
: QWidget(parent)
{
setupUi();
connect(&m_opQueue, &OperationQueue::allOperationsFinished,
this, [this](bool success, int completed, int total) {
Q_UNUSED(completed);
Q_UNUSED(total);
if (success)
{
QMessageBox::information(this, tr("Operations Complete"),
tr("All %1 operations completed successfully.").arg(completed));
}
else
{
QMessageBox::warning(this, tr("Operations Failed"),
tr("Operation failed. %1 of %2 completed.").arg(completed).arg(total));
}
emit statusMessage(tr("Refreshing disk list after operations..."));
// Request a full refresh
auto result = DiskEnumerator::getSystemSnapshot();
if (result.isOk())
refreshDisks(result.value());
});
}
DiskPartitionTab::~DiskPartitionTab() = default;
@@ -37,33 +79,52 @@ void DiskPartitionTab::setupUi()
diskLabel->setStyleSheet("font-weight: bold; padding: 4px;");
leftLayout->addWidget(diskLabel);
m_diskTreeModel = new QStandardItemModel(this);
m_diskTreeModel->setHorizontalHeaderLabels({tr("Disk / Partition"), tr("Size"), tr("Type")});
m_diskTree = new QTreeView();
m_diskTree->setModel(m_diskTreeModel);
m_diskTree->setHeaderHidden(false);
m_diskTree->setAlternatingRowColors(true);
m_diskTree->setMinimumWidth(250);
m_diskTree->setMinimumWidth(280);
m_diskTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_diskTree->setSelectionMode(QAbstractItemView::SingleSelection);
leftLayout->addWidget(m_diskTree);
connect(m_diskTree->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &DiskPartitionTab::onDiskTreeSelectionChanged);
m_mainSplitter->addWidget(leftPanel);
// Center + Bottom: partition map and table
m_rightSplitter = new QSplitter(Qt::Vertical);
// Disk map placeholder (will be replaced by DiskMapWidget)
m_diskMapPlaceholder = new QWidget();
m_diskMapPlaceholder->setMinimumHeight(120);
auto* mapLayout = new QVBoxLayout(m_diskMapPlaceholder);
auto* mapLabel = new QLabel(tr("Partition Map"));
mapLabel->setAlignment(Qt::AlignCenter);
mapLabel->setStyleSheet("color: #6c7086; font-size: 14px;");
mapLayout->addWidget(mapLabel);
m_rightSplitter->addWidget(m_diskMapPlaceholder);
// Disk map widget
m_diskMap = new DiskMapWidget();
m_rightSplitter->addWidget(m_diskMap);
connect(m_diskMap, &DiskMapWidget::partitionClicked,
this, &DiskPartitionTab::onDiskMapPartitionClicked);
connect(m_diskMap, &DiskMapWidget::contextMenuRequested,
this, &DiskPartitionTab::onDiskMapContextMenu);
// Partition detail table
m_partitionModel = new QStandardItemModel(this);
m_partitionModel->setHorizontalHeaderLabels(
{tr("#"), tr("Label"), tr("Drive Letter"), tr("Filesystem"),
tr("Size"), tr("Used"), tr("Free"), tr("Status"), tr("Flags")});
m_partitionTable = new QTableView();
m_partitionTable->setModel(m_partitionModel);
m_partitionTable->setAlternatingRowColors(true);
m_partitionTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_partitionTable->setSelectionMode(QAbstractItemView::SingleSelection);
m_partitionTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_partitionTable->horizontalHeader()->setStretchLastSection(true);
m_partitionTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_partitionTable, &QWidget::customContextMenuRequested,
this, &DiskPartitionTab::onPartitionTableContextMenu);
m_rightSplitter->addWidget(m_partitionTable);
m_rightSplitter->setStretchFactor(0, 2);
@@ -72,36 +133,817 @@ void DiskPartitionTab::setupUi()
m_mainSplitter->addWidget(m_rightSplitter);
// Right panel: pending operations list
m_operationList = new QWidget();
auto* opLayout = new QVBoxLayout(m_operationList);
auto* opPanel = new QWidget();
auto* opLayout = new QVBoxLayout(opPanel);
opLayout->setContentsMargins(0, 0, 0, 0);
auto* opLabel = new QLabel(tr("Pending Operations"));
opLabel->setStyleSheet("font-weight: bold; padding: 4px;");
opLayout->addWidget(opLabel);
auto* opListWidget = new QListWidget();
opListWidget->setMinimumWidth(220);
opLayout->addWidget(opListWidget);
m_operationListWidget = new QListWidget();
m_operationListWidget->setMinimumWidth(220);
opLayout->addWidget(m_operationListWidget);
auto* buttonLayout = new QHBoxLayout();
auto* applyBtn = new QPushButton(tr("Apply"));
applyBtn->setObjectName("applyButton");
auto* undoBtn = new QPushButton(tr("Undo"));
auto* clearBtn = new QPushButton(tr("Clear"));
buttonLayout->addWidget(applyBtn);
buttonLayout->addWidget(undoBtn);
buttonLayout->addWidget(clearBtn);
m_applyBtn = new QPushButton(tr("Apply"));
m_applyBtn->setObjectName("applyButton");
m_applyBtn->setEnabled(false);
m_undoBtn = new QPushButton(tr("Undo"));
m_undoBtn->setEnabled(false);
m_clearBtn = new QPushButton(tr("Clear"));
m_clearBtn->setEnabled(false);
buttonLayout->addWidget(m_applyBtn);
buttonLayout->addWidget(m_undoBtn);
buttonLayout->addWidget(m_clearBtn);
opLayout->addLayout(buttonLayout);
m_mainSplitter->addWidget(m_operationList);
connect(m_applyBtn, &QPushButton::clicked, this, &DiskPartitionTab::onApplyOperations);
connect(m_undoBtn, &QPushButton::clicked, this, &DiskPartitionTab::onUndoOperation);
connect(m_clearBtn, &QPushButton::clicked, this, &DiskPartitionTab::onClearOperations);
m_mainSplitter->addWidget(opPanel);
// Set splitter proportions
m_mainSplitter->setStretchFactor(0, 1); // Disk tree
m_mainSplitter->setStretchFactor(1, 3); // Center content
m_mainSplitter->setStretchFactor(2, 1); // Operation list
m_mainSplitter->setStretchFactor(0, 1);
m_mainSplitter->setStretchFactor(1, 3);
m_mainSplitter->setStretchFactor(2, 1);
layout->addWidget(m_mainSplitter);
}
void DiskPartitionTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskTree(snapshot);
// Re-select current disk if still valid
if (m_selectedDiskId >= 0)
{
populatePartitionTable(m_selectedDiskId);
updateDiskMap(m_selectedDiskId);
}
}
void DiskPartitionTab::populateDiskTree(const SystemDiskSnapshot& snapshot)
{
m_diskTreeModel->removeRows(0, m_diskTreeModel->rowCount());
for (const auto& disk : snapshot.disks)
{
QString diskName = QString("Disk %1: %2")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model));
auto* diskItem = new QStandardItem(diskName);
diskItem->setData(disk.id, Qt::UserRole); // Store diskId
diskItem->setData(-1, Qt::UserRole + 1); // Not a partition
diskItem->setIcon(QIcon::fromTheme("drive-harddisk"));
auto* sizeItem = new QStandardItem(formatSize(disk.sizeBytes));
auto* typeItem = new QStandardItem(
QString("%1 / %2")
.arg(interfaceTypeString(disk.interfaceType))
.arg(partitionTableTypeString(disk.partitionTableType)));
// Find partitions belonging to this disk
for (const auto& part : snapshot.partitions)
{
if (part.diskId != disk.id)
continue;
QString partLabel;
if (part.driveLetter != L'\0')
partLabel = QString("(%1:) ").arg(QChar(part.driveLetter));
if (!part.label.empty())
partLabel += QString::fromStdWString(part.label);
else
partLabel += filesystemString(part.filesystemType);
auto* partItem = new QStandardItem(partLabel);
partItem->setData(disk.id, Qt::UserRole);
partItem->setData(part.index, Qt::UserRole + 1);
auto* partSizeItem = new QStandardItem(formatSize(part.sizeBytes));
auto* partFsItem = new QStandardItem(filesystemString(part.filesystemType));
diskItem->appendRow({partItem, partSizeItem, partFsItem});
}
m_diskTreeModel->appendRow({diskItem, sizeItem, typeItem});
}
m_diskTree->expandAll();
m_diskTree->resizeColumnToContents(0);
}
void DiskPartitionTab::populatePartitionTable(DiskId diskId)
{
m_partitionModel->removeRows(0, m_partitionModel->rowCount());
for (const auto& part : m_snapshot.partitions)
{
if (part.diskId != diskId)
continue;
QList<QStandardItem*> row;
row.append(new QStandardItem(QString::number(part.index)));
// Label
row.append(new QStandardItem(QString::fromStdWString(part.label)));
// Drive letter
if (part.driveLetter != L'\0')
row.append(new QStandardItem(QString("%1:").arg(QChar(part.driveLetter))));
else
row.append(new QStandardItem(QStringLiteral("-")));
// Filesystem
row.append(new QStandardItem(filesystemString(part.filesystemType)));
// Size
row.append(new QStandardItem(formatSize(part.sizeBytes)));
// Used / Free — look up volume info
QString usedStr = QStringLiteral("-");
QString freeStr = QStringLiteral("-");
for (const auto& vol : m_snapshot.volumes)
{
if (vol.guidPath == part.volumeGuidPath && vol.totalBytes > 0)
{
uint64_t used = vol.totalBytes - vol.freeBytes;
usedStr = formatSize(used);
freeStr = formatSize(vol.freeBytes);
break;
}
}
row.append(new QStandardItem(usedStr));
row.append(new QStandardItem(freeStr));
// Status
QStringList statusFlags;
if (part.isActive)
statusFlags << QStringLiteral("Active");
if (part.isBootable)
statusFlags << QStringLiteral("Boot");
row.append(new QStandardItem(statusFlags.isEmpty() ? QStringLiteral("Normal") : statusFlags.join(", ")));
// Flags
QStringList flags;
if (part.isActive)
flags << QStringLiteral("Boot");
if (part.mbrType != 0)
flags << QString("MBR 0x%1").arg(part.mbrType, 2, 16, QChar('0'));
row.append(new QStandardItem(flags.isEmpty() ? QStringLiteral("-") : flags.join(", ")));
// Store partition index in first item
row[0]->setData(part.index, Qt::UserRole);
m_partitionModel->appendRow(row);
}
m_partitionTable->resizeColumnsToContents();
}
void DiskPartitionTab::updateDiskMap(DiskId diskId)
{
// Collect partitions for this disk
std::vector<PartitionInfo> diskPartitions;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskId)
diskPartitions.push_back(p);
}
// Find disk info
for (const auto& d : m_snapshot.disks)
{
if (d.id == diskId)
{
m_diskMap->setDisk(d, diskPartitions, m_snapshot.volumes);
return;
}
}
m_diskMap->clear();
}
void DiskPartitionTab::onDiskTreeSelectionChanged()
{
auto indexes = m_diskTree->selectionModel()->selectedIndexes();
if (indexes.isEmpty())
return;
auto idx = indexes.first();
DiskId diskId = idx.data(Qt::UserRole).toInt();
m_selectedDiskId = diskId;
populatePartitionTable(diskId);
updateDiskMap(diskId);
}
void DiskPartitionTab::onPartitionTableContextMenu(const QPoint& pos)
{
auto index = m_partitionTable->indexAt(pos);
int partIdx = -1;
if (index.isValid())
{
auto* item = m_partitionModel->item(index.row(), 0);
if (item)
partIdx = item->data(Qt::UserRole).toInt();
}
showContextMenu(partIdx, m_partitionTable->viewport()->mapToGlobal(pos));
}
void DiskPartitionTab::onDiskMapContextMenu(int partitionIndex, const QPoint& globalPos)
{
showContextMenu(partitionIndex, globalPos);
}
void DiskPartitionTab::onDiskMapPartitionClicked(int partitionIndex)
{
// Select the corresponding row in partition table
for (int r = 0; r < m_partitionModel->rowCount(); ++r)
{
auto* item = m_partitionModel->item(r, 0);
if (item && item->data(Qt::UserRole).toInt() == partitionIndex)
{
m_partitionTable->selectRow(r);
break;
}
}
}
void DiskPartitionTab::showContextMenu(int partitionIndex, const QPoint& globalPos)
{
QMenu menu(this);
auto* createAct = menu.addAction(tr("Create Partition..."));
connect(createAct, &QAction::triggered, this, &DiskPartitionTab::onCreatePartition);
if (partitionIndex >= 0)
{
menu.addSeparator();
auto* deleteAct = menu.addAction(tr("Delete Partition"));
connect(deleteAct, &QAction::triggered, this, &DiskPartitionTab::onDeletePartition);
auto* resizeAct = menu.addAction(tr("Resize/Move..."));
connect(resizeAct, &QAction::triggered, this, &DiskPartitionTab::onResizePartition);
auto* formatAct = menu.addAction(tr("Format..."));
connect(formatAct, &QAction::triggered, this, &DiskPartitionTab::onFormatPartition);
menu.addSeparator();
auto* labelAct = menu.addAction(tr("Set Label..."));
connect(labelAct, &QAction::triggered, this, &DiskPartitionTab::onSetLabel);
auto* flagsAct = menu.addAction(tr("Set Flags..."));
connect(flagsAct, &QAction::triggered, this, &DiskPartitionTab::onSetFlags);
menu.addSeparator();
auto* checkAct = menu.addAction(tr("Check Filesystem"));
connect(checkAct, &QAction::triggered, this, &DiskPartitionTab::onCheckFilesystem);
}
menu.exec(globalPos);
}
int DiskPartitionTab::selectedPartitionIndex() const
{
auto indexes = m_partitionTable->selectionModel()->selectedRows();
if (indexes.isEmpty())
return -1;
auto* item = m_partitionModel->item(indexes.first().row(), 0);
return item ? item->data(Qt::UserRole).toInt() : -1;
}
DiskId DiskPartitionTab::selectedDiskId() const
{
return m_selectedDiskId;
}
void DiskPartitionTab::onCreatePartition()
{
if (m_selectedDiskId < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a disk first."));
return;
}
// Find disk info
const DiskInfo* diskInfo = nullptr;
for (const auto& d : m_snapshot.disks)
{
if (d.id == m_selectedDiskId)
{
diskInfo = &d;
break;
}
}
if (!diskInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Create Partition"));
auto* form = new QFormLayout(&dlg);
auto* sizeGbSpin = new QDoubleSpinBox();
sizeGbSpin->setRange(0.001, static_cast<double>(diskInfo->sizeBytes) / (1024.0 * 1024.0 * 1024.0));
sizeGbSpin->setDecimals(3);
sizeGbSpin->setSuffix(QStringLiteral(" GB"));
sizeGbSpin->setValue(1.0);
form->addRow(tr("Size:"), sizeGbSpin);
auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2")});
form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit();
form->addRow(tr("Label:"), labelEdit);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
// Build operation
uint64_t sizeBytes = static_cast<uint64_t>(sizeGbSpin->value() * 1024.0 * 1024.0 * 1024.0);
uint32_t sectorSize = diskInfo->sectorSize;
SectorCount sectors = sizeBytes / sectorSize;
// Find first large enough gap
SectorOffset startLba = DEFAULT_ALIGNMENT_SECTORS_512;
// Simple: use offset after last partition
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId)
{
SectorOffset end = (p.offsetBytes + p.sizeBytes) / sectorSize;
if (end > startLba)
startLba = DiskGeometry::alignSectorUp(end, DEFAULT_ALIGNMENT_SECTORS_512);
}
}
CreatePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.startLba = startLba;
params.sectorCount = sectors;
params.sectorSize = sectorSize;
params.formatAfter = true;
// Map filesystem selection
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2
};
int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes)))
{
params.formatOptions.targetFs = fsTypes[fsIdx];
}
params.formatOptions.volumeLabel = labelEdit->text().toStdString();
params.formatOptions.quickFormat = true;
auto op = std::make_unique<CreatePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onDeletePartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
// Find partition info
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
auto reply = QMessageBox::question(this, tr("Delete Partition"),
tr("Are you sure you want to delete partition %1?\n"
"This operation will be queued and applied when you click Apply.")
.arg(partIdx));
if (reply != QMessageBox::Yes)
return;
DeletePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.sectorSize = 512; // Default
params.driveLetter = partInfo->driveLetter;
auto op = std::make_unique<DeletePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onResizePartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
double currentGb = static_cast<double>(partInfo->sizeBytes) / (1024.0 * 1024.0 * 1024.0);
bool ok = false;
double newGb = QInputDialog::getDouble(this, tr("Resize Partition"),
tr("New size in GB (current: %1 GB):").arg(currentGb, 0, 'f', 2),
currentGb, 0.001, 999999.0, 3, &ok);
if (!ok)
return;
uint64_t newSizeBytes = static_cast<uint64_t>(newGb * 1024.0 * 1024.0 * 1024.0);
uint32_t sectorSize = 512;
SectorCount newSectors = newSizeBytes / sectorSize;
SectorOffset startLba = partInfo->offsetBytes / sectorSize;
ResizePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.sectorSize = sectorSize;
params.driveLetter = partInfo->driveLetter;
params.newStartLba = startLba;
params.newSectorCount = newSectors;
auto op = std::make_unique<ResizePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onFormatPartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Format Partition"));
auto* form = new QFormLayout(&dlg);
auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2"), tr("Linux Swap")});
form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit();
form->addRow(tr("Label:"), labelEdit);
auto* quickCheck = new QCheckBox(tr("Quick Format"));
quickCheck->setChecked(true);
form->addRow(quickCheck);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2,
FilesystemType::SWAP_LINUX
};
FormatPartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
if (partInfo->driveLetter != L'\0')
{
params.target.driveLetter = partInfo->driveLetter;
}
else
{
params.target.diskIndex = m_selectedDiskId;
params.target.partitionOffsetBytes = partInfo->offsetBytes;
params.target.partitionSizeBytes = partInfo->sizeBytes;
}
int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes)))
params.options.targetFs = fsTypes[fsIdx];
params.options.volumeLabel = labelEdit->text().toStdString();
params.options.quickFormat = quickCheck->isChecked();
auto op = std::make_unique<FormatPartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onSetLabel()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
bool ok = false;
QString newLabel = QInputDialog::getText(this, tr("Set Volume Label"),
tr("New label:"),
QLineEdit::Normal,
QString::fromStdWString(partInfo->label), &ok);
if (!ok)
return;
SetLabelOp::Params params;
params.driveLetter = partInfo->driveLetter;
params.newLabel = newLabel.toStdString();
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.partitionOffsetBytes = partInfo->offsetBytes;
params.fsType = partInfo->filesystemType;
auto op = std::make_unique<SetLabelOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onSetFlags()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Set Partition Flags"));
auto* form = new QFormLayout(&dlg);
auto* activeCheck = new QCheckBox(tr("Active (Bootable)"));
activeCheck->setChecked(partInfo->isActive);
form->addRow(activeCheck);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
SetFlagsOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.setActive = activeCheck->isChecked();
auto op = std::make_unique<SetFlagsOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onCheckFilesystem()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
CheckFilesystemOp::Params params;
params.driveLetter = partInfo->driveLetter;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.partitionOffsetBytes = partInfo->offsetBytes;
params.fsType = partInfo->filesystemType;
params.repair = false;
auto op = std::make_unique<CheckFilesystemOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onApplyOperations()
{
if (m_opQueue.pendingCount() == 0)
return;
auto reply = QMessageBox::warning(
this, tr("Apply Operations"),
tr("You are about to apply %1 pending operation(s).\n\n"
"WARNING: These operations may modify your disk permanently.\n"
"Make sure you have backed up important data.\n\n"
"Continue?")
.arg(m_opQueue.pendingCount()),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
auto* progressDlg = new QProgressDialog(tr("Applying operations..."), tr("Cancel"), 0, 100, this);
progressDlg->setWindowModality(Qt::WindowModal);
progressDlg->setMinimumDuration(0);
progressDlg->show();
connect(&m_opQueue, &OperationQueue::queueProgress,
progressDlg, [progressDlg](int overall, int /*current*/, const QString& status) {
progressDlg->setValue(overall);
progressDlg->setLabelText(status);
});
connect(progressDlg, &QProgressDialog::canceled, &m_opQueue, &OperationQueue::requestCancel);
auto* thread = QThread::create([this]() {
m_opQueue.applyAll();
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, progressDlg, &QProgressDialog::close);
connect(thread, &QThread::finished, progressDlg, &QProgressDialog::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
updateOperationList();
});
thread->start();
}
void DiskPartitionTab::onUndoOperation()
{
auto removed = m_opQueue.removeLast();
if (removed)
{
updateOperationList();
}
}
void DiskPartitionTab::onClearOperations()
{
m_opQueue.clearPending();
updateOperationList();
}
void DiskPartitionTab::updateOperationList()
{
m_operationListWidget->clear();
const auto& pending = m_opQueue.pending();
for (const auto& op : pending)
{
m_operationListWidget->addItem(op->description());
}
bool hasPending = m_opQueue.pendingCount() > 0;
m_applyBtn->setEnabled(hasPending);
m_undoBtn->setEnabled(hasPending);
m_clearBtn->setEnabled(hasPending);
emit statusMessage(hasPending
? tr("%1 pending operation(s)").arg(m_opQueue.pendingCount())
: tr("No pending operations"));
}
QString DiskPartitionTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
if (bytes >= 1024ULL)
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
return QString("%1 B").arg(bytes);
}
QString DiskPartitionTab::interfaceTypeString(DiskInterfaceType type)
{
switch (type)
{
case DiskInterfaceType::SATA: return QStringLiteral("SATA");
case DiskInterfaceType::NVMe: return QStringLiteral("NVMe");
case DiskInterfaceType::USB: return QStringLiteral("USB");
case DiskInterfaceType::SCSI: return QStringLiteral("SCSI");
case DiskInterfaceType::SAS: return QStringLiteral("SAS");
case DiskInterfaceType::IDE: return QStringLiteral("IDE");
case DiskInterfaceType::MMC: return QStringLiteral("MMC");
case DiskInterfaceType::Firewire: return QStringLiteral("FireWire");
case DiskInterfaceType::Thunderbolt: return QStringLiteral("Thunderbolt");
case DiskInterfaceType::Virtual: return QStringLiteral("Virtual");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::mediaTypeString(MediaType type)
{
switch (type)
{
case MediaType::HDD: return QStringLiteral("HDD");
case MediaType::SSD: return QStringLiteral("SSD");
case MediaType::NVMe: return QStringLiteral("NVMe");
case MediaType::USBFlash: return QStringLiteral("USB Flash");
case MediaType::SDCard: return QStringLiteral("SD Card");
case MediaType::CompactFlash: return QStringLiteral("CF");
case MediaType::OpticalDrive: return QStringLiteral("Optical");
case MediaType::FloppyDisk: return QStringLiteral("Floppy");
case MediaType::Virtual: return QStringLiteral("Virtual");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::filesystemString(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QStringLiteral("NTFS");
case FilesystemType::FAT32: return QStringLiteral("FAT32");
case FilesystemType::FAT16: return QStringLiteral("FAT16");
case FilesystemType::FAT12: return QStringLiteral("FAT12");
case FilesystemType::ExFAT: return QStringLiteral("exFAT");
case FilesystemType::ReFS: return QStringLiteral("ReFS");
case FilesystemType::Ext2: return QStringLiteral("ext2");
case FilesystemType::Ext3: return QStringLiteral("ext3");
case FilesystemType::Ext4: return QStringLiteral("ext4");
case FilesystemType::Btrfs: return QStringLiteral("Btrfs");
case FilesystemType::XFS: return QStringLiteral("XFS");
case FilesystemType::ZFS: return QStringLiteral("ZFS");
case FilesystemType::HFSPlus: return QStringLiteral("HFS+");
case FilesystemType::APFS: return QStringLiteral("APFS");
case FilesystemType::SWAP_LINUX: return QStringLiteral("Linux Swap");
case FilesystemType::Unallocated: return QStringLiteral("Unallocated");
case FilesystemType::Raw: return QStringLiteral("RAW");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::partitionTableTypeString(PartitionTableType pt)
{
switch (pt)
{
case PartitionTableType::MBR: return QStringLiteral("MBR");
case PartitionTableType::GPT: return QStringLiteral("GPT");
case PartitionTableType::APM: return QStringLiteral("APM");
default: return QStringLiteral("Unknown");
}
}
} // namespace spw

View File

@@ -1,14 +1,26 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/operations/OperationQueue.h"
#include <QWidget>
class QSplitter;
class QTreeView;
class QTableView;
class QListWidget;
class QPushButton;
class QStandardItemModel;
class QMenu;
class QLabel;
class QProgressDialog;
namespace spw
{
class DiskMapWidget;
class DiskPartitionTab : public QWidget
{
Q_OBJECT
@@ -17,23 +29,72 @@ public:
explicit DiskPartitionTab(QWidget* parent = nullptr);
~DiskPartitionTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onDiskTreeSelectionChanged();
void onPartitionTableContextMenu(const QPoint& pos);
void onDiskMapContextMenu(int partitionIndex, const QPoint& globalPos);
void onDiskMapPartitionClicked(int partitionIndex);
void onApplyOperations();
void onUndoOperation();
void onClearOperations();
// Context menu actions
void onCreatePartition();
void onDeletePartition();
void onResizePartition();
void onFormatPartition();
void onSetLabel();
void onSetFlags();
void onCheckFilesystem();
private:
void setupUi();
void populateDiskTree(const SystemDiskSnapshot& snapshot);
void populatePartitionTable(DiskId diskId);
void updateDiskMap(DiskId diskId);
void updateOperationList();
void showContextMenu(int partitionIndex, const QPoint& globalPos);
// Find the currently selected partition info
int selectedPartitionIndex() const;
DiskId selectedDiskId() const;
static QString formatSize(uint64_t bytes);
static QString interfaceTypeString(DiskInterfaceType type);
static QString mediaTypeString(MediaType type);
static QString filesystemString(FilesystemType fs);
static QString partitionTableTypeString(PartitionTableType pt);
QSplitter* m_mainSplitter = nullptr;
QSplitter* m_rightSplitter = nullptr;
// Left panel: disk tree
QTreeView* m_diskTree = nullptr;
QStandardItemModel* m_diskTreeModel = nullptr;
// Center: partition map (placeholder for DiskMapWidget)
QWidget* m_diskMapPlaceholder = nullptr;
// Center: partition map
DiskMapWidget* m_diskMap = nullptr;
// Bottom: partition detail table
QTableView* m_partitionTable = nullptr;
QStandardItemModel* m_partitionModel = nullptr;
// Right: operation list
QWidget* m_operationList = nullptr;
QListWidget* m_operationListWidget = nullptr;
QPushButton* m_applyBtn = nullptr;
QPushButton* m_undoBtn = nullptr;
QPushButton* m_clearBtn = nullptr;
// Backend
OperationQueue m_opQueue;
SystemDiskSnapshot m_snapshot;
DiskId m_selectedDiskId = -1;
};
} // namespace spw

View File

@@ -1,14 +1,22 @@
#include "ImagingTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/imaging/DiskCloner.h"
#include "core/imaging/ImageCreator.h"
#include "core/imaging/ImageRestorer.h"
#include "core/imaging/IsoFlasher.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -26,71 +34,480 @@ void ImagingTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Clone Disk section
// ===== Clone Disk =====
auto* cloneGroup = new QGroupBox(tr("Clone Disk"));
auto* cloneLayout = new QGridLayout(cloneGroup);
cloneLayout->addWidget(new QLabel(tr("Source:")), 0, 0);
cloneLayout->addWidget(new QComboBox(), 0, 1);
cloneLayout->addWidget(new QLabel(tr("Target:")), 1, 0);
cloneLayout->addWidget(new QComboBox(), 1, 1);
auto* cloneBtn = new QPushButton(tr("Clone"));
cloneBtn->setObjectName("applyButton");
cloneLayout->addWidget(cloneBtn, 2, 1, Qt::AlignRight);
m_cloneSourceCombo = new QComboBox();
cloneLayout->addWidget(m_cloneSourceCombo, 0, 1, 1, 2);
cloneLayout->addWidget(new QLabel(tr("Destination:")), 1, 0);
m_cloneDestCombo = new QComboBox();
cloneLayout->addWidget(m_cloneDestCombo, 1, 1, 1, 2);
cloneLayout->addWidget(new QLabel(tr("Mode:")), 2, 0);
m_cloneModeCombo = new QComboBox();
m_cloneModeCombo->addItems({tr("Raw (sector-by-sector)"), tr("Smart (skip free space)")});
cloneLayout->addWidget(m_cloneModeCombo, 2, 1);
m_cloneVerifyCheck = new QCheckBox(tr("Verify after clone"));
m_cloneVerifyCheck->setChecked(true);
cloneLayout->addWidget(m_cloneVerifyCheck, 2, 2);
m_cloneProgress = new QProgressBar();
m_cloneProgress->setVisible(false);
cloneLayout->addWidget(m_cloneProgress, 3, 0, 1, 2);
m_cloneSpeedLabel = new QLabel();
cloneLayout->addWidget(m_cloneSpeedLabel, 3, 2);
m_cloneBtn = new QPushButton(tr("Clone"));
m_cloneBtn->setObjectName("applyButton");
connect(m_cloneBtn, &QPushButton::clicked, this, &ImagingTab::onCloneDisk);
cloneLayout->addWidget(m_cloneBtn, 4, 2, Qt::AlignRight);
layout->addWidget(cloneGroup);
// Create Image section
auto* imageGroup = new QGroupBox(tr("Create Disk/Media Image"));
// ===== Create Image =====
auto* imageGroup = new QGroupBox(tr("Create Disk Image"));
auto* imageLayout = new QGridLayout(imageGroup);
imageLayout->addWidget(new QLabel(tr("Source:")), 0, 0);
auto* sourceCombo = new QComboBox();
sourceCombo->setToolTip(tr("Select disk, USB drive, SD card, or other media"));
imageLayout->addWidget(sourceCombo, 0, 1);
m_imageSourceCombo = new QComboBox();
imageLayout->addWidget(m_imageSourceCombo, 0, 1, 1, 2);
imageLayout->addWidget(new QLabel(tr("Output File:")), 1, 0);
auto* outputLine = new QLineEdit();
imageLayout->addWidget(outputLine, 1, 1);
auto* browseBtn = new QPushButton(tr("Browse..."));
imageLayout->addWidget(browseBtn, 1, 2);
imageLayout->addWidget(new QLabel(tr("Compression:")), 2, 0);
auto* compCombo = new QComboBox();
compCombo->addItems({tr("None"), tr("Fast (zstd-1)"), tr("Default (zstd-3)"), tr("Best (zstd-9)")});
imageLayout->addWidget(compCombo, 2, 1);
auto* createImgBtn = new QPushButton(tr("Create Image"));
createImgBtn->setObjectName("applyButton");
imageLayout->addWidget(createImgBtn, 3, 1, Qt::AlignRight);
m_imageOutputEdit = new QLineEdit();
imageLayout->addWidget(m_imageOutputEdit, 1, 1);
auto* imgBrowseBtn = new QPushButton(tr("Browse..."));
connect(imgBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseImageOutput);
imageLayout->addWidget(imgBrowseBtn, 1, 2);
imageLayout->addWidget(new QLabel(tr("Format:")), 2, 0);
m_imageFormatCombo = new QComboBox();
m_imageFormatCombo->addItems({tr("Raw (.img)"), tr("Compressed SPW (.spw)")});
imageLayout->addWidget(m_imageFormatCombo, 2, 1);
m_imageCreateProgress = new QProgressBar();
m_imageCreateProgress->setVisible(false);
imageLayout->addWidget(m_imageCreateProgress, 3, 0, 1, 2);
m_imageCreateSpeedLabel = new QLabel();
imageLayout->addWidget(m_imageCreateSpeedLabel, 3, 2);
m_imageCreateBtn = new QPushButton(tr("Create Image"));
m_imageCreateBtn->setObjectName("applyButton");
connect(m_imageCreateBtn, &QPushButton::clicked, this, &ImagingTab::onCreateImage);
imageLayout->addWidget(m_imageCreateBtn, 4, 2, Qt::AlignRight);
layout->addWidget(imageGroup);
// Restore Image section
// ===== Restore Image =====
auto* restoreGroup = new QGroupBox(tr("Restore Image"));
auto* restoreLayout = new QGridLayout(restoreGroup);
restoreLayout->addWidget(new QLabel(tr("Image File:")), 0, 0);
restoreLayout->addWidget(new QLineEdit(), 0, 1);
restoreLayout->addWidget(new QPushButton(tr("Browse...")), 0, 2);
restoreLayout->addWidget(new QLabel(tr("Target:")), 1, 0);
restoreLayout->addWidget(new QComboBox(), 1, 1);
auto* restoreBtn = new QPushButton(tr("Restore"));
restoreBtn->setObjectName("applyButton");
restoreLayout->addWidget(restoreBtn, 2, 1, Qt::AlignRight);
m_restoreInputEdit = new QLineEdit();
connect(m_restoreInputEdit, &QLineEdit::textChanged, this, &ImagingTab::onRestoreInputChanged);
restoreLayout->addWidget(m_restoreInputEdit, 0, 1);
auto* restBrowseBtn = new QPushButton(tr("Browse..."));
connect(restBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseRestoreInput);
restoreLayout->addWidget(restBrowseBtn, 0, 2);
m_restoreImageInfo = new QLabel(tr("No image selected"));
m_restoreImageInfo->setWordWrap(true);
m_restoreImageInfo->setStyleSheet("color: #6c7086; padding: 4px;");
restoreLayout->addWidget(m_restoreImageInfo, 1, 0, 1, 3);
restoreLayout->addWidget(new QLabel(tr("Destination:")), 2, 0);
m_restoreDestCombo = new QComboBox();
restoreLayout->addWidget(m_restoreDestCombo, 2, 1);
m_restoreVerifyCheck = new QCheckBox(tr("Verify after restore"));
m_restoreVerifyCheck->setChecked(true);
restoreLayout->addWidget(m_restoreVerifyCheck, 2, 2);
m_restoreProgress = new QProgressBar();
m_restoreProgress->setVisible(false);
restoreLayout->addWidget(m_restoreProgress, 3, 0, 1, 2);
m_restoreSpeedLabel = new QLabel();
restoreLayout->addWidget(m_restoreSpeedLabel, 3, 2);
m_restoreBtn = new QPushButton(tr("Restore"));
m_restoreBtn->setObjectName("applyButton");
connect(m_restoreBtn, &QPushButton::clicked, this, &ImagingTab::onRestoreImage);
restoreLayout->addWidget(m_restoreBtn, 4, 2, Qt::AlignRight);
layout->addWidget(restoreGroup);
// Flash ISO/IMG section
// ===== Flash ISO/IMG =====
auto* flashGroup = new QGroupBox(tr("Flash ISO/IMG to USB"));
auto* flashLayout = new QGridLayout(flashGroup);
flashLayout->addWidget(new QLabel(tr("Image:")), 0, 0);
flashLayout->addWidget(new QLineEdit(), 0, 1);
flashLayout->addWidget(new QPushButton(tr("Browse...")), 0, 2);
flashLayout->addWidget(new QLabel(tr("Target USB:")), 1, 0);
flashLayout->addWidget(new QComboBox(), 1, 1);
auto* flashBtn = new QPushButton(tr("Flash"));
flashBtn->setObjectName("applyButton");
flashLayout->addWidget(flashBtn, 2, 1, Qt::AlignRight);
layout->addWidget(flashGroup);
// Progress
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
layout->addWidget(progressBar);
flashLayout->addWidget(new QLabel(tr("Image:")), 0, 0);
m_flashInputEdit = new QLineEdit();
flashLayout->addWidget(m_flashInputEdit, 0, 1);
auto* flashBrowseBtn = new QPushButton(tr("Browse..."));
connect(flashBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseFlashInput);
flashLayout->addWidget(flashBrowseBtn, 0, 2);
flashLayout->addWidget(new QLabel(tr("Target USB:")), 1, 0);
m_flashTargetCombo = new QComboBox();
flashLayout->addWidget(m_flashTargetCombo, 1, 1);
m_flashVerifyCheck = new QCheckBox(tr("Verify after flash"));
m_flashVerifyCheck->setChecked(true);
flashLayout->addWidget(m_flashVerifyCheck, 1, 2);
m_flashProgress = new QProgressBar();
m_flashProgress->setVisible(false);
flashLayout->addWidget(m_flashProgress, 2, 0, 1, 2);
m_flashSpeedLabel = new QLabel();
flashLayout->addWidget(m_flashSpeedLabel, 2, 2);
m_flashBtn = new QPushButton(tr("Flash"));
m_flashBtn->setObjectName("applyButton");
connect(m_flashBtn, &QPushButton::clicked, this, &ImagingTab::onFlashIso);
flashLayout->addWidget(m_flashBtn, 3, 2, Qt::AlignRight);
layout->addWidget(flashGroup);
layout->addStretch();
}
void ImagingTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombos();
}
void ImagingTab::populateDiskCombos()
{
// Clear all combos
m_cloneSourceCombo->clear();
m_cloneDestCombo->clear();
m_imageSourceCombo->clear();
m_restoreDestCombo->clear();
m_flashTargetCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_cloneSourceCombo->addItem(label, disk.id);
m_cloneDestCombo->addItem(label, disk.id);
m_imageSourceCombo->addItem(label, disk.id);
m_restoreDestCombo->addItem(label, disk.id);
// Flash target: only removable drives
if (disk.isRemovable)
{
m_flashTargetCombo->addItem(label, disk.id);
}
}
if (m_flashTargetCombo->count() == 0)
{
m_flashTargetCombo->addItem(tr("No removable drives detected"));
}
}
void ImagingTab::onCloneDisk()
{
int srcDiskId = m_cloneSourceCombo->currentData().toInt();
int dstDiskId = m_cloneDestCombo->currentData().toInt();
if (srcDiskId == dstDiskId)
{
QMessageBox::warning(this, tr("Invalid"), tr("Source and destination must be different disks."));
return;
}
auto reply = QMessageBox::warning(this, tr("Clone Disk"),
tr("ALL data on Disk %1 will be OVERWRITTEN.\n\n"
"Source: Disk %2\nDestination: Disk %1\n\nContinue?")
.arg(dstDiskId).arg(srcDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
CloneConfig config;
config.sourceDiskId = srcDiskId;
config.destDiskId = dstDiskId;
config.mode = m_cloneModeCombo->currentIndex() == 0 ? CloneMode::Raw : CloneMode::Smart;
config.verifyAfterClone = m_cloneVerifyCheck->isChecked();
m_cloneProgress->setVisible(true);
m_cloneProgress->setValue(0);
m_cloneBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
DiskCloner cloner;
cloner.clone(config, [this](const CloneProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_cloneProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_cloneSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_cloneProgress->setVisible(false);
m_cloneBtn->setEnabled(true);
m_cloneSpeedLabel->clear();
QMessageBox::information(this, tr("Clone Complete"), tr("Disk cloning completed."));
emit statusMessage(tr("Disk clone completed"));
});
thread->start();
}
void ImagingTab::onCreateImage()
{
int srcDiskId = m_imageSourceCombo->currentData().toInt();
QString outputPath = m_imageOutputEdit->text();
if (outputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Output"), tr("Please specify an output file."));
return;
}
ImageCreateConfig config;
config.sourceDiskId = srcDiskId;
config.outputFilePath = outputPath.toStdWString();
config.format = m_imageFormatCombo->currentIndex() == 0 ? ImageFormat::Raw : ImageFormat::SPW;
config.enableCompression = (config.format == ImageFormat::SPW);
m_imageCreateProgress->setVisible(true);
m_imageCreateProgress->setValue(0);
m_imageCreateBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
ImageCreator creator;
creator.createImage(config, [this](const ImageCreateProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_imageCreateProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QString info = QString("%1 MB/s").arg(speedMB, 0, 'f', 1);
if (progress.compressionRatio > 0)
info += QString(", Ratio: %1:1").arg(progress.compressionRatio, 0, 'f', 1);
QMetaObject::invokeMethod(m_imageCreateSpeedLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, info));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_imageCreateProgress->setVisible(false);
m_imageCreateBtn->setEnabled(true);
m_imageCreateSpeedLabel->clear();
QMessageBox::information(this, tr("Image Created"), tr("Disk image created successfully."));
emit statusMessage(tr("Image creation completed"));
});
thread->start();
}
void ImagingTab::onRestoreImage()
{
QString inputPath = m_restoreInputEdit->text();
int dstDiskId = m_restoreDestCombo->currentData().toInt();
if (inputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please specify an input image file."));
return;
}
auto reply = QMessageBox::warning(this, tr("Restore Image"),
tr("ALL data on Disk %1 will be OVERWRITTEN with the image contents.\n\nContinue?")
.arg(dstDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
ImageRestoreConfig config;
config.inputFilePath = inputPath.toStdWString();
config.destDiskId = dstDiskId;
config.verifyAfterRestore = m_restoreVerifyCheck->isChecked();
m_restoreProgress->setVisible(true);
m_restoreProgress->setValue(0);
m_restoreBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
ImageRestorer restorer;
restorer.restoreImage(config, [this](const ImageRestoreProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_restoreProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_restoreSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_restoreProgress->setVisible(false);
m_restoreBtn->setEnabled(true);
m_restoreSpeedLabel->clear();
QMessageBox::information(this, tr("Restore Complete"), tr("Image restoration completed."));
emit statusMessage(tr("Image restore completed"));
});
thread->start();
}
void ImagingTab::onFlashIso()
{
QString inputPath = m_flashInputEdit->text();
int targetDiskId = m_flashTargetCombo->currentData().toInt();
if (inputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please specify an ISO/IMG file."));
return;
}
auto reply = QMessageBox::warning(this, tr("Flash ISO/IMG"),
tr("ALL data on the target USB drive (Disk %1) will be DESTROYED.\n\nContinue?")
.arg(targetDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
FlashConfig config;
config.inputFilePath = inputPath.toStdWString();
config.targetDiskId = targetDiskId;
config.verifyAfterFlash = m_flashVerifyCheck->isChecked();
m_flashProgress->setVisible(true);
m_flashProgress->setValue(0);
m_flashBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
IsoFlasher flasher;
flasher.flash(config, [this](const FlashProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_flashProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_flashSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_flashProgress->setVisible(false);
m_flashBtn->setEnabled(true);
m_flashSpeedLabel->clear();
QMessageBox::information(this, tr("Flash Complete"), tr("ISO/IMG flash completed."));
emit statusMessage(tr("Flash completed"));
});
thread->start();
}
void ImagingTab::onBrowseImageOutput()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save Image As"),
QString(),
tr("Raw Image (*.img);;SPW Compressed (*.spw);;All Files (*)"));
if (!file.isEmpty())
m_imageOutputEdit->setText(file);
}
void ImagingTab::onBrowseRestoreInput()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select Image File"),
QString(),
tr("Image Files (*.img *.spw);;All Files (*)"));
if (!file.isEmpty())
m_restoreInputEdit->setText(file);
}
void ImagingTab::onBrowseFlashInput()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select ISO/IMG File"),
QString(),
tr("Disk Images (*.iso *.img);;All Files (*)"));
if (!file.isEmpty())
m_flashInputEdit->setText(file);
}
void ImagingTab::onRestoreInputChanged()
{
QString path = m_restoreInputEdit->text();
if (path.isEmpty())
{
m_restoreImageInfo->setText(tr("No image selected"));
return;
}
// Try to read SPW image info
auto infoResult = ImageRestorer::inspectImage(path.toStdWString());
if (infoResult.isOk())
{
const auto& info = infoResult.value();
m_restoreImageInfo->setText(
QString("Model: %1\nSerial: %2\nSize: %3\nChunks: %4 (%5 sparse)\n%6")
.arg(QString::fromStdString(info.diskModel))
.arg(QString::fromStdString(info.diskSerial))
.arg(formatSize(info.imageDataSize))
.arg(info.chunkCount)
.arg(info.sparseChunkCount)
.arg(info.isCompressed ? tr("Compressed") : tr("Uncompressed")));
}
else
{
// Might be a raw image
auto fmtResult = ImageRestorer::detectFormat(path.toStdWString());
if (fmtResult.isOk() && fmtResult.value() == ImageFormat::Raw)
{
m_restoreImageInfo->setText(tr("Raw image file"));
}
else
{
m_restoreImageInfo->setText(tr("Unable to read image info"));
}
}
}
QString ImagingTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
} // namespace spw

View File

@@ -1,7 +1,18 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QComboBox;
class QCheckBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
namespace spw
{
@@ -13,8 +24,64 @@ public:
explicit ImagingTab(QWidget* parent = nullptr);
~ImagingTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onCloneDisk();
void onCreateImage();
void onRestoreImage();
void onFlashIso();
void onBrowseImageOutput();
void onBrowseRestoreInput();
void onBrowseFlashInput();
void onRestoreInputChanged();
private:
void setupUi();
void populateDiskCombos();
static QString formatSize(uint64_t bytes);
// Clone section
QComboBox* m_cloneSourceCombo = nullptr;
QComboBox* m_cloneDestCombo = nullptr;
QComboBox* m_cloneModeCombo = nullptr;
QCheckBox* m_cloneVerifyCheck = nullptr;
QPushButton* m_cloneBtn = nullptr;
QProgressBar* m_cloneProgress = nullptr;
QLabel* m_cloneSpeedLabel = nullptr;
// Create Image section
QComboBox* m_imageSourceCombo = nullptr;
QLineEdit* m_imageOutputEdit = nullptr;
QComboBox* m_imageFormatCombo = nullptr;
QPushButton* m_imageCreateBtn = nullptr;
QProgressBar* m_imageCreateProgress = nullptr;
QLabel* m_imageCreateSpeedLabel = nullptr;
// Restore Image section
QLineEdit* m_restoreInputEdit = nullptr;
QLabel* m_restoreImageInfo = nullptr;
QComboBox* m_restoreDestCombo = nullptr;
QCheckBox* m_restoreVerifyCheck = nullptr;
QPushButton* m_restoreBtn = nullptr;
QProgressBar* m_restoreProgress = nullptr;
QLabel* m_restoreSpeedLabel = nullptr;
// Flash ISO section
QLineEdit* m_flashInputEdit = nullptr;
QComboBox* m_flashTargetCombo = nullptr;
QCheckBox* m_flashVerifyCheck = nullptr;
QPushButton* m_flashBtn = nullptr;
QProgressBar* m_flashProgress = nullptr;
QLabel* m_flashSpeedLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
};
} // namespace spw

View File

@@ -1,14 +1,23 @@
#include "MaintenanceTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/maintenance/SecureErase.h"
#include "core/recovery/BootRepair.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -26,52 +35,62 @@ void MaintenanceTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Secure Erase section
// ===== Secure Erase Section =====
auto* eraseGroup = new QGroupBox(tr("Secure Erase"));
auto* eraseLayout = new QGridLayout(eraseGroup);
eraseLayout->addWidget(new QLabel(tr("Target Disk:")), 0, 0);
eraseLayout->addWidget(new QComboBox(), 0, 1);
m_eraseDiskCombo = new QComboBox();
eraseLayout->addWidget(m_eraseDiskCombo, 0, 1, 1, 2);
eraseLayout->addWidget(new QLabel(tr("Erase Method:")), 1, 0);
auto* methodWidget = new QWidget();
auto* methodLayout = new QVBoxLayout(methodWidget);
methodLayout->setContentsMargins(0, 0, 0, 0);
auto* zeroPass = new QRadioButton(tr("Zero fill (1 pass) — Fast"));
zeroPass->setChecked(true);
auto* dod3Pass = new QRadioButton(tr("DoD 5220.22-M (3 passes) — Standard"));
auto* dod7Pass = new QRadioButton(tr("DoD 5220.22-M ECE (7 passes) — Enhanced"));
auto* gutmann = new QRadioButton(tr("Gutmann method (35 passes) — Maximum"));
auto* customPass = new QRadioButton(tr("Custom:"));
auto* customSpin = new QSpinBox();
customSpin->setRange(1, 99);
customSpin->setValue(3);
customSpin->setEnabled(false);
auto* customRow = new QHBoxLayout();
customRow->addWidget(customPass);
customRow->addWidget(customSpin);
customRow->addWidget(new QLabel(tr("passes")));
customRow->addStretch();
m_eraseMethodCombo = new QComboBox();
m_eraseMethodCombo->addItems({
tr("Zero Fill (1 pass) - Fast"),
tr("DoD 5220.22-M (3 passes) - Standard"),
tr("DoD 5220.22-M ECE (7 passes) - Enhanced"),
tr("Gutmann (35 passes) - Maximum"),
tr("Random Fill (N passes)"),
tr("Custom Pattern")
});
connect(m_eraseMethodCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MaintenanceTab::onEraseMethodChanged);
eraseLayout->addWidget(m_eraseMethodCombo, 1, 1, 1, 2);
methodLayout->addWidget(zeroPass);
methodLayout->addWidget(dod3Pass);
methodLayout->addWidget(dod7Pass);
methodLayout->addWidget(gutmann);
methodLayout->addLayout(customRow);
eraseLayout->addWidget(methodWidget, 1, 1);
eraseLayout->addWidget(new QLabel(tr("Custom Passes:")), 2, 0);
m_customPassSpin = new QSpinBox();
m_customPassSpin->setRange(1, 99);
m_customPassSpin->setValue(3);
m_customPassSpin->setEnabled(false);
eraseLayout->addWidget(m_customPassSpin, 2, 1);
auto* verifyCheck = new QCheckBox(tr("Verify after erase"));
verifyCheck->setChecked(true);
eraseLayout->addWidget(verifyCheck, 2, 1);
m_verifyCheck = new QCheckBox(tr("Verify after erase"));
m_verifyCheck->setChecked(true);
eraseLayout->addWidget(m_verifyCheck, 3, 1);
auto* eraseBtn = new QPushButton(tr("Secure Erase"));
eraseBtn->setObjectName("cancelButton");
eraseBtn->setToolTip(tr("WARNING: This permanently destroys all data on the selected disk!"));
eraseLayout->addWidget(eraseBtn, 3, 1, Qt::AlignRight);
m_eraseProgress = new QProgressBar();
m_eraseProgress->setVisible(false);
eraseLayout->addWidget(m_eraseProgress, 4, 0, 1, 3);
m_eraseStatusLabel = new QLabel();
eraseLayout->addWidget(m_eraseStatusLabel, 5, 0, 1, 3);
// BIG RED erase button
m_eraseBtn = new QPushButton(tr("SECURE ERASE"));
m_eraseBtn->setObjectName("cancelButton");
m_eraseBtn->setMinimumHeight(50);
m_eraseBtn->setStyleSheet(
"QPushButton { background-color: #cc0000; color: white; font-size: 16px; "
"font-weight: bold; border: 2px solid #880000; border-radius: 6px; }"
"QPushButton:hover { background-color: #ee0000; }"
"QPushButton:pressed { background-color: #aa0000; }");
m_eraseBtn->setToolTip(tr("WARNING: This permanently destroys ALL data on the selected disk!"));
connect(m_eraseBtn, &QPushButton::clicked, this, &MaintenanceTab::onSecureErase);
eraseLayout->addWidget(m_eraseBtn, 6, 0, 1, 3);
layout->addWidget(eraseGroup);
// Boot Repair section
// ===== Boot Repair Section =====
auto* bootGroup = new QGroupBox(tr("Boot Repair"));
auto* bootLayout = new QVBoxLayout(bootGroup);
@@ -79,28 +98,382 @@ void MaintenanceTab::setupUi()
tr("Repair boot configuration for Windows and other operating systems."));
bootLayout->addWidget(bootInfo);
auto* mbrRepairBtn = new QPushButton(tr("Repair MBR"));
mbrRepairBtn->setToolTip(tr("Rewrite the Master Boot Record with a standard boot loader"));
auto* gptRepairBtn = new QPushButton(tr("Repair GPT"));
gptRepairBtn->setToolTip(tr("Rebuild GPT headers and verify partition entries"));
auto* bcdRepairBtn = new QPushButton(tr("Repair Windows BCD"));
bcdRepairBtn->setToolTip(tr("Rebuild the Windows Boot Configuration Data store"));
auto* bootloaderBtn = new QPushButton(tr("Reinstall Bootloader"));
bootloaderBtn->setToolTip(tr("Reinstall the bootloader to the selected disk's boot sector"));
auto* bootDiskRow = new QHBoxLayout();
bootDiskRow->addWidget(new QLabel(tr("Target Disk:")));
m_bootDiskCombo = new QComboBox();
bootDiskRow->addWidget(m_bootDiskCombo, 1);
bootLayout->addLayout(bootDiskRow);
bootLayout->addWidget(mbrRepairBtn);
bootLayout->addWidget(gptRepairBtn);
bootLayout->addWidget(bcdRepairBtn);
bootLayout->addWidget(bootloaderBtn);
auto* bootBtnLayout = new QHBoxLayout();
m_mbrRepairBtn = new QPushButton(tr("Repair MBR"));
m_mbrRepairBtn->setToolTip(tr("Rewrite the Master Boot Record with a standard boot loader"));
connect(m_mbrRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairMbr);
bootBtnLayout->addWidget(m_mbrRepairBtn);
m_gptRepairBtn = new QPushButton(tr("Repair GPT"));
m_gptRepairBtn->setToolTip(tr("Rebuild GPT headers and verify partition entries"));
connect(m_gptRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairGpt);
bootBtnLayout->addWidget(m_gptRepairBtn);
m_bcdRepairBtn = new QPushButton(tr("Repair BCD"));
m_bcdRepairBtn->setToolTip(tr("Rebuild the Windows Boot Configuration Data store"));
connect(m_bcdRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairBcd);
bootBtnLayout->addWidget(m_bcdRepairBtn);
m_bootloaderBtn = new QPushButton(tr("Reinstall Bootloader"));
m_bootloaderBtn->setToolTip(tr("Reinstall the bootloader to the selected disk's boot sector"));
connect(m_bootloaderBtn, &QPushButton::clicked, this, &MaintenanceTab::onReinstallBootloader);
bootBtnLayout->addWidget(m_bootloaderBtn);
bootLayout->addLayout(bootBtnLayout);
m_bootProgress = new QProgressBar();
m_bootProgress->setVisible(false);
bootLayout->addWidget(m_bootProgress);
m_bootStatusLabel = new QLabel();
m_bootStatusLabel->setWordWrap(true);
bootLayout->addWidget(m_bootStatusLabel);
layout->addWidget(bootGroup);
// Progress
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
layout->addWidget(progressBar);
layout->addStretch();
}
void MaintenanceTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void MaintenanceTab::populateDiskCombo()
{
m_eraseDiskCombo->clear();
m_bootDiskCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_eraseDiskCombo->addItem(label, disk.id);
m_bootDiskCombo->addItem(label, disk.id);
}
}
void MaintenanceTab::onEraseMethodChanged()
{
int idx = m_eraseMethodCombo->currentIndex();
// Enable custom pass count for Random Fill (4) and Custom Pattern (5)
m_customPassSpin->setEnabled(idx == 4 || idx == 5);
}
void MaintenanceTab::onSecureErase()
{
int diskId = m_eraseDiskCombo->currentData().toInt();
// Find disk name for confirmation
QString diskName;
for (const auto& disk : m_snapshot.disks)
{
if (disk.id == diskId)
{
diskName = QString::fromStdWString(disk.model);
break;
}
}
// First confirmation
auto reply = QMessageBox::critical(this, tr("SECURE ERASE - CONFIRM"),
tr("You are about to PERMANENTLY DESTROY all data on:\n\n"
"Disk %1: %2\n\n"
"This action is IRREVERSIBLE.\n\n"
"Are you absolutely sure?")
.arg(diskId).arg(diskName),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
// Second confirmation: type disk name
bool ok = false;
QString typedName = QInputDialog::getText(this, tr("Final Confirmation"),
tr("Type the disk model name to confirm:\n\n%1")
.arg(diskName),
QLineEdit::Normal, QString(), &ok);
if (!ok || typedName.trimmed() != diskName.trimmed())
{
QMessageBox::information(this, tr("Cancelled"),
tr("Erase cancelled. The disk name did not match."));
return;
}
// Build erase config
EraseConfig config;
switch (m_eraseMethodCombo->currentIndex())
{
case 0: config.method = EraseMethod::ZeroFill; break;
case 1: config.method = EraseMethod::DoD_3Pass; break;
case 2: config.method = EraseMethod::DoD_7Pass; break;
case 3: config.method = EraseMethod::Gutmann; break;
case 4:
config.method = EraseMethod::RandomFill;
config.passCount = m_customPassSpin->value();
break;
case 5:
config.method = EraseMethod::CustomPattern;
config.customPatternPasses = m_customPassSpin->value();
config.customPattern = {0xAA, 0x55}; // Default alternating pattern
break;
}
config.verify = m_verifyCheck->isChecked();
m_cancelFlag.store(false);
m_eraseProgress->setVisible(true);
m_eraseProgress->setValue(0);
m_eraseBtn->setEnabled(false);
m_eraseStatusLabel->setText(tr("Erasing..."));
auto* thread = QThread::create([this, diskId, config]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to open disk: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
SecureErase erase(disk);
auto result = erase.eraseDisk(config,
[this](int currentPass, int totalPasses,
uint64_t bytesWritten, uint64_t totalBytes, double speedMBps) {
int pct = totalBytes > 0 ? static_cast<int>((bytesWritten * 100) / totalBytes) : 0;
QMetaObject::invokeMethod(m_eraseProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Pass %1/%2 - %3 MB/s")
.arg(currentPass)
.arg(totalPasses)
.arg(speedMBps, 0, 'f', 1)));
},
&m_cancelFlag);
QString resultMsg = result.isOk() ? tr("Erase completed successfully.")
: tr("Erase failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, resultMsg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_eraseProgress->setVisible(false);
m_eraseBtn->setEnabled(true);
emit statusMessage(tr("Secure erase completed"));
});
thread->start();
}
void MaintenanceTab::onRepairMbr()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Repair MBR"),
tr("This will rewrite the MBR boot code on Disk %1.\n"
"Partition table entries will be preserved.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing MBR..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairMbr();
QString msg = result.isOk() ? tr("MBR repaired successfully.")
: tr("MBR repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("MBR repair completed"));
});
thread->start();
}
void MaintenanceTab::onRepairGpt()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Repair GPT"),
tr("This will rebuild GPT headers on Disk %1.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing GPT..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairGpt(true); // Rebuild primary from backup
QString msg = result.isOk() ? tr("GPT repaired successfully.")
: tr("GPT repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("GPT repair completed"));
});
thread->start();
}
void MaintenanceTab::onRepairBcd()
{
int diskId = m_bootDiskCombo->currentData().toInt();
Q_UNUSED(diskId);
auto reply = QMessageBox::warning(this, tr("Repair BCD"),
tr("This will rebuild the Windows Boot Configuration Data.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing BCD..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairBcd(L'S'); // Assume S: is the ESP
QString msg = result.isOk() ? tr("BCD repaired successfully.")
: tr("BCD repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("BCD repair completed"));
});
thread->start();
}
void MaintenanceTab::onReinstallBootloader()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Reinstall Bootloader"),
tr("This will reinstall the Windows bootloader on Disk %1.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Reinstalling bootloader..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairBootloader(L'S', L'C');
QString msg = result.isOk() ? tr("Bootloader reinstalled successfully.")
: tr("Bootloader reinstall failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("Bootloader reinstall completed"));
});
thread->start();
}
QString MaintenanceTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
} // namespace spw

View File

@@ -1,6 +1,20 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/maintenance/SecureErase.h"
#include <QWidget>
#include <atomic>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QSpinBox;
namespace spw
{
@@ -13,8 +27,47 @@ public:
explicit MaintenanceTab(QWidget* parent = nullptr);
~MaintenanceTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onSecureErase();
void onEraseMethodChanged();
void onRepairMbr();
void onRepairGpt();
void onRepairBcd();
void onReinstallBootloader();
private:
void setupUi();
void populateDiskCombo();
static QString formatSize(uint64_t bytes);
// Secure Erase
QComboBox* m_eraseDiskCombo = nullptr;
QComboBox* m_eraseMethodCombo = nullptr;
QSpinBox* m_customPassSpin = nullptr;
QCheckBox* m_verifyCheck = nullptr;
QPushButton* m_eraseBtn = nullptr;
QProgressBar* m_eraseProgress = nullptr;
QLabel* m_eraseStatusLabel = nullptr;
// Boot Repair
QComboBox* m_bootDiskCombo = nullptr;
QPushButton* m_mbrRepairBtn = nullptr;
QPushButton* m_gptRepairBtn = nullptr;
QPushButton* m_bcdRepairBtn = nullptr;
QPushButton* m_bootloaderBtn = nullptr;
QProgressBar* m_bootProgress = nullptr;
QLabel* m_bootStatusLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,14 +1,27 @@
#include "RecoveryTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/recovery/PartitionRecovery.h"
#include "core/recovery/FileRecovery.h"
#include "core/recovery/BootRepair.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QListWidget>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSplitter>
#include <QStackedWidget>
#include <QTableWidget>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -27,65 +40,664 @@ void RecoveryTab::setupUi()
auto* layout = new QHBoxLayout(this);
auto* splitter = new QSplitter(Qt::Horizontal);
// Left: recovery options
// Left: options panel
auto* optionsPanel = new QWidget();
auto* optLayout = new QVBoxLayout(optionsPanel);
auto* typeGroup = new QGroupBox(tr("Recovery Type"));
auto* typeLayout = new QVBoxLayout(typeGroup);
auto* partRecoveryBtn = new QPushButton(tr("Partition Recovery"));
partRecoveryBtn->setToolTip(tr("Scan for lost or deleted partitions"));
auto* fileRecoveryBtn = new QPushButton(tr("File Recovery"));
fileRecoveryBtn->setToolTip(tr("Recover files from damaged or formatted drives"));
auto* mbrRepairBtn = new QPushButton(tr("MBR/GPT Repair"));
mbrRepairBtn->setToolTip(tr("Rebuild partition table from filesystem superblocks"));
typeLayout->addWidget(partRecoveryBtn);
typeLayout->addWidget(fileRecoveryBtn);
typeLayout->addWidget(mbrRepairBtn);
optLayout->addWidget(typeGroup);
// Target disk selector
auto* targetGroup = new QGroupBox(tr("Target Disk"));
auto* targetLayout = new QVBoxLayout(targetGroup);
auto* diskCombo = new QComboBox();
diskCombo->addItem(tr("Select a disk..."));
targetLayout->addWidget(diskCombo);
m_diskCombo = new QComboBox();
targetLayout->addWidget(m_diskCombo);
optLayout->addWidget(targetGroup);
auto* scanBtn = new QPushButton(tr("Start Scan"));
scanBtn->setObjectName("applyButton");
optLayout->addWidget(scanBtn);
// Recovery type buttons
auto* typeGroup = new QGroupBox(tr("Recovery Type"));
auto* typeLayout = new QVBoxLayout(typeGroup);
m_partRecoveryBtn = new QPushButton(tr("Partition Recovery"));
m_partRecoveryBtn->setCheckable(true);
m_partRecoveryBtn->setChecked(true);
m_fileRecoveryBtn = new QPushButton(tr("File Recovery"));
m_fileRecoveryBtn->setCheckable(true);
m_bootRepairBtn = new QPushButton(tr("Boot Repair"));
m_bootRepairBtn->setCheckable(true);
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
optLayout->addWidget(progressBar);
typeLayout->addWidget(m_partRecoveryBtn);
typeLayout->addWidget(m_fileRecoveryBtn);
typeLayout->addWidget(m_bootRepairBtn);
optLayout->addWidget(typeGroup);
connect(m_partRecoveryBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
connect(m_fileRecoveryBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
connect(m_bootRepairBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
optLayout->addStretch();
splitter->addWidget(optionsPanel);
// Right: results
auto* resultsPanel = new QWidget();
auto* resLayout = new QVBoxLayout(resultsPanel);
// Right: stacked pages
m_stackedWidget = new QStackedWidget();
auto* resLabel = new QLabel(tr("Recovery Results"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
resLayout->addWidget(resLabel);
setupPartitionRecoveryPage();
setupFileRecoveryPage();
setupBootRepairPage();
auto* resultsTable = new QTableWidget(0, 5);
resultsTable->setHorizontalHeaderLabels(
{tr("Type"), tr("Name/Label"), tr("Size"), tr("Filesystem"), tr("Confidence")});
resultsTable->setAlternatingRowColors(true);
resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
resLayout->addWidget(resultsTable);
auto* recoverBtn = new QPushButton(tr("Recover Selected"));
recoverBtn->setObjectName("applyButton");
resLayout->addWidget(recoverBtn);
splitter->addWidget(resultsPanel);
splitter->addWidget(m_stackedWidget);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 2);
splitter->setStretchFactor(1, 3);
layout->addWidget(splitter);
}
void RecoveryTab::setupPartitionRecoveryPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* scanGroup = new QGroupBox(tr("Scan Options"));
auto* scanLayout = new QVBoxLayout(scanGroup);
m_quickScanRadio = new QRadioButton(tr("Quick Scan (1 MiB boundaries)"));
m_quickScanRadio->setChecked(true);
m_deepScanRadio = new QRadioButton(tr("Deep Scan (every sector)"));
scanLayout->addWidget(m_quickScanRadio);
scanLayout->addWidget(m_deepScanRadio);
m_partScanBtn = new QPushButton(tr("Start Scan"));
m_partScanBtn->setObjectName("applyButton");
scanLayout->addWidget(m_partScanBtn);
connect(m_partScanBtn, &QPushButton::clicked, this, &RecoveryTab::onStartPartitionScan);
m_partScanProgress = new QProgressBar();
m_partScanProgress->setVisible(false);
scanLayout->addWidget(m_partScanProgress);
layout->addWidget(scanGroup);
// Results table
auto* resLabel = new QLabel(tr("Recovered Partitions"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
layout->addWidget(resLabel);
m_partResultsTable = new QTableWidget(0, 6);
m_partResultsTable->setHorizontalHeaderLabels(
{tr("Start LBA"), tr("Size"), tr("Filesystem"), tr("Label"), tr("Confidence"), tr("Overlaps")});
m_partResultsTable->setAlternatingRowColors(true);
m_partResultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_partResultsTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_partResultsTable->horizontalHeader()->setStretchLastSection(true);
layout->addWidget(m_partResultsTable);
m_recoverPartBtn = new QPushButton(tr("Recover Selected"));
m_recoverPartBtn->setObjectName("applyButton");
m_recoverPartBtn->setEnabled(false);
connect(m_recoverPartBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoverSelectedPartitions);
layout->addWidget(m_recoverPartBtn);
m_stackedWidget->addWidget(page);
}
void RecoveryTab::setupFileRecoveryPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* optGroup = new QGroupBox(tr("File Recovery Options"));
auto* optLayout = new QVBoxLayout(optGroup);
auto* partRow = new QHBoxLayout();
partRow->addWidget(new QLabel(tr("Partition:")));
m_partitionCombo = new QComboBox();
partRow->addWidget(m_partitionCombo, 1);
optLayout->addLayout(partRow);
auto* typeRow = new QHBoxLayout();
typeRow->addWidget(new QLabel(tr("File Types:")));
m_fileTypeFilter = new QComboBox();
m_fileTypeFilter->addItems({tr("All Files"), tr("Images (JPG, PNG, BMP, GIF)"),
tr("Documents (PDF, DOC, XLS)"), tr("Archives (ZIP, RAR, 7Z)"),
tr("Media (MP3, MP4, AVI)")});
typeRow->addWidget(m_fileTypeFilter, 1);
optLayout->addLayout(typeRow);
m_fileScanBtn = new QPushButton(tr("Scan for Files"));
m_fileScanBtn->setObjectName("applyButton");
connect(m_fileScanBtn, &QPushButton::clicked, this, &RecoveryTab::onStartFileRecoveryScan);
optLayout->addWidget(m_fileScanBtn);
m_fileScanProgress = new QProgressBar();
m_fileScanProgress->setVisible(false);
optLayout->addWidget(m_fileScanProgress);
layout->addWidget(optGroup);
// Results
auto* resLabel = new QLabel(tr("Recoverable Files"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
layout->addWidget(resLabel);
m_fileResultsTable = new QTableWidget(0, 5);
m_fileResultsTable->setHorizontalHeaderLabels(
{tr("Filename"), tr("Size"), tr("Type"), tr("Confidence"), tr("Source FS")});
m_fileResultsTable->setAlternatingRowColors(true);
m_fileResultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_fileResultsTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_fileResultsTable->horizontalHeader()->setStretchLastSection(true);
layout->addWidget(m_fileResultsTable);
// Output folder
auto* outRow = new QHBoxLayout();
outRow->addWidget(new QLabel(tr("Output Folder:")));
m_outputFolderEdit = new QLineEdit();
outRow->addWidget(m_outputFolderEdit, 1);
m_browseOutputBtn = new QPushButton(tr("Browse..."));
connect(m_browseOutputBtn, &QPushButton::clicked, this, &RecoveryTab::onBrowseOutputFolder);
outRow->addWidget(m_browseOutputBtn);
layout->addLayout(outRow);
m_recoverFileBtn = new QPushButton(tr("Recover Selected Files"));
m_recoverFileBtn->setObjectName("applyButton");
m_recoverFileBtn->setEnabled(false);
connect(m_recoverFileBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoverSelectedFiles);
layout->addWidget(m_recoverFileBtn);
m_stackedWidget->addWidget(page);
}
void RecoveryTab::setupBootRepairPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* optGroup = new QGroupBox(tr("Boot Repair Options"));
auto* optLayout = new QVBoxLayout(optGroup);
m_repairMbr = new QCheckBox(tr("Repair MBR boot code"));
m_repairGpt = new QCheckBox(tr("Repair GPT headers"));
m_repairBootSector = new QCheckBox(tr("Restore backup boot sector (NTFS/FAT)"));
m_repairBcd = new QCheckBox(tr("Rebuild Windows BCD store"));
m_repairBootloader = new QCheckBox(tr("Reinstall Windows bootloader"));
optLayout->addWidget(m_repairMbr);
optLayout->addWidget(m_repairGpt);
optLayout->addWidget(m_repairBootSector);
optLayout->addWidget(m_repairBcd);
optLayout->addWidget(m_repairBootloader);
layout->addWidget(optGroup);
m_bootRepairStartBtn = new QPushButton(tr("Start Repair"));
m_bootRepairStartBtn->setObjectName("applyButton");
connect(m_bootRepairStartBtn, &QPushButton::clicked, this, &RecoveryTab::onStartBootRepair);
layout->addWidget(m_bootRepairStartBtn);
m_bootRepairProgress = new QProgressBar();
m_bootRepairProgress->setVisible(false);
layout->addWidget(m_bootRepairProgress);
m_bootRepairStatus = new QLabel();
m_bootRepairStatus->setWordWrap(true);
layout->addWidget(m_bootRepairStatus);
layout->addStretch();
m_stackedWidget->addWidget(page);
}
void RecoveryTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
populatePartitionCombo();
}
void RecoveryTab::populateDiskCombo()
{
m_diskCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_diskCombo->addItem(label, disk.id);
}
}
void RecoveryTab::populatePartitionCombo()
{
m_partitionCombo->clear();
for (const auto& part : m_snapshot.partitions)
{
QString label;
if (part.driveLetter != L'\0')
label = QString("(%1:) ").arg(QChar(part.driveLetter));
label += QString("Disk %1, Partition %2 - %3")
.arg(part.diskId)
.arg(part.index)
.arg(formatSize(part.sizeBytes));
m_partitionCombo->addItem(label, QVariant::fromValue(static_cast<int>(part.index)));
}
}
void RecoveryTab::onRecoveryTypeChanged()
{
auto* sender = qobject_cast<QPushButton*>(this->sender());
m_partRecoveryBtn->setChecked(sender == m_partRecoveryBtn);
m_fileRecoveryBtn->setChecked(sender == m_fileRecoveryBtn);
m_bootRepairBtn->setChecked(sender == m_bootRepairBtn);
if (sender == m_partRecoveryBtn)
m_stackedWidget->setCurrentIndex(0);
else if (sender == m_fileRecoveryBtn)
m_stackedWidget->setCurrentIndex(1);
else if (sender == m_bootRepairBtn)
m_stackedWidget->setCurrentIndex(2);
}
void RecoveryTab::onStartPartitionScan()
{
int diskIdx = m_diskCombo->currentData().toInt();
if (diskIdx < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a target disk."));
return;
}
PartitionScanMode mode = m_quickScanRadio->isChecked()
? PartitionScanMode::Quick
: PartitionScanMode::Deep;
m_cancelFlag.store(false);
m_partScanProgress->setVisible(true);
m_partScanProgress->setValue(0);
m_partScanBtn->setEnabled(false);
m_partResultsTable->setRowCount(0);
auto* thread = QThread::create([this, diskIdx, mode]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
PartitionRecovery recovery(disk);
auto scanResult = recovery.scan(mode,
[this](uint64_t scanned, uint64_t total, size_t /*found*/) {
if (total > 0)
{
int pct = static_cast<int>((scanned * 100) / total);
QMetaObject::invokeMethod(m_partScanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
}
},
&m_cancelFlag);
if (scanResult.isOk())
{
m_recoveredPartitions = scanResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_partScanProgress->setVisible(false);
m_partScanBtn->setEnabled(true);
m_partResultsTable->setRowCount(0);
for (size_t i = 0; i < m_recoveredPartitions.size(); ++i)
{
const auto& rp = m_recoveredPartitions[i];
int row = m_partResultsTable->rowCount();
m_partResultsTable->insertRow(row);
m_partResultsTable->setItem(row, 0, new QTableWidgetItem(
QString::number(rp.startLba)));
m_partResultsTable->setItem(row, 1, new QTableWidgetItem(
formatSize(rp.sectorCount * rp.sectorSize)));
m_partResultsTable->setItem(row, 2, new QTableWidgetItem(
FilesystemDetector::filesystemName(rp.fsType)));
m_partResultsTable->setItem(row, 3, new QTableWidgetItem(
QString::fromStdString(rp.label)));
m_partResultsTable->setItem(row, 4, new QTableWidgetItem(
QString("%1%").arg(rp.confidence, 0, 'f', 1)));
m_partResultsTable->setItem(row, 5, new QTableWidgetItem(
rp.overlapsExisting ? tr("Yes") : tr("No")));
}
m_recoverPartBtn->setEnabled(!m_recoveredPartitions.empty());
emit statusMessage(tr("Found %1 partition(s)").arg(m_recoveredPartitions.size()));
});
thread->start();
}
void RecoveryTab::onStartFileRecoveryScan()
{
int diskIdx = m_diskCombo->currentData().toInt();
int partIdx = m_partitionCombo->currentData().toInt();
if (diskIdx < 0 || partIdx < 0)
{
QMessageBox::warning(this, tr("Selection Required"),
tr("Please select a disk and partition."));
return;
}
// Find partition info
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
m_cancelFlag.store(false);
m_fileScanProgress->setVisible(true);
m_fileScanProgress->setValue(0);
m_fileScanBtn->setEnabled(false);
m_fileResultsTable->setRowCount(0);
SectorOffset startLba = partInfo->offsetBytes / partInfo->sizeBytes > 0 ? partInfo->offsetBytes / 512 : 0;
SectorCount sectors = partInfo->sizeBytes / 512;
FilesystemType fsType = partInfo->filesystemType;
auto* thread = QThread::create([this, diskIdx, startLba, sectors, fsType]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
FileRecovery recovery(disk, startLba, sectors, fsType);
auto scanResult = recovery.scan(FileRecoveryMode::Both,
[this](uint64_t scanned, uint64_t total, size_t /*found*/) {
if (total > 0)
{
int pct = static_cast<int>((scanned * 100) / total);
QMetaObject::invokeMethod(m_fileScanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
}
},
&m_cancelFlag);
if (scanResult.isOk())
{
m_recoveredFiles = scanResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_fileScanProgress->setVisible(false);
m_fileScanBtn->setEnabled(true);
m_fileResultsTable->setRowCount(0);
for (size_t i = 0; i < m_recoveredFiles.size(); ++i)
{
const auto& rf = m_recoveredFiles[i];
int row = m_fileResultsTable->rowCount();
m_fileResultsTable->insertRow(row);
m_fileResultsTable->setItem(row, 0, new QTableWidgetItem(
QString::fromStdString(rf.filename)));
m_fileResultsTable->setItem(row, 1, new QTableWidgetItem(
formatSize(rf.sizeBytes)));
m_fileResultsTable->setItem(row, 2, new QTableWidgetItem(
QString::fromStdString(rf.extension)));
m_fileResultsTable->setItem(row, 3, new QTableWidgetItem(
QString("%1%").arg(rf.confidence, 0, 'f', 1)));
m_fileResultsTable->setItem(row, 4, new QTableWidgetItem(
FilesystemDetector::filesystemName(rf.sourceFs)));
}
m_recoverFileBtn->setEnabled(!m_recoveredFiles.empty());
emit statusMessage(tr("Found %1 recoverable file(s)").arg(m_recoveredFiles.size()));
});
thread->start();
}
void RecoveryTab::onRecoverSelectedPartitions()
{
int diskIdx = m_diskCombo->currentData().toInt();
auto selected = m_partResultsTable->selectionModel()->selectedRows();
if (selected.isEmpty())
{
QMessageBox::information(this, tr("No Selection"), tr("Please select partitions to recover."));
return;
}
auto reply = QMessageBox::warning(this, tr("Recover Partitions"),
tr("This will modify the partition table on Disk %1.\nContinue?").arg(diskIdx),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
auto* thread = QThread::create([this, diskIdx, selected]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadWrite);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
PartitionRecovery recovery(disk);
for (const auto& idx : selected)
{
int row = idx.row();
if (row >= 0 && row < static_cast<int>(m_recoveredPartitions.size()))
{
recovery.recover(m_recoveredPartitions[row]);
}
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
QMessageBox::information(this, tr("Recovery Complete"),
tr("Partition recovery completed. Refreshing disk list..."));
emit statusMessage(tr("Partition recovery completed"));
});
thread->start();
}
void RecoveryTab::onRecoverSelectedFiles()
{
QString outputDir = m_outputFolderEdit->text();
if (outputDir.isEmpty())
{
QMessageBox::warning(this, tr("No Output"), tr("Please select an output folder."));
return;
}
auto selected = m_fileResultsTable->selectionModel()->selectedRows();
if (selected.isEmpty())
{
QMessageBox::information(this, tr("No Selection"), tr("Please select files to recover."));
return;
}
int diskIdx = m_diskCombo->currentData().toInt();
int partIdx = m_partitionCombo->currentData().toInt();
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
SectorOffset startLba = partInfo->offsetBytes / 512;
SectorCount sectors = partInfo->sizeBytes / 512;
FilesystemType fsType = partInfo->filesystemType;
auto filesToRecover = selected;
auto outputPath = outputDir.toStdString();
auto* thread = QThread::create([this, diskIdx, startLba, sectors, fsType, filesToRecover, outputPath]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
FileRecovery recovery(disk, startLba, sectors, fsType);
for (const auto& idx : filesToRecover)
{
int row = idx.row();
if (row >= 0 && row < static_cast<int>(m_recoveredFiles.size()))
{
std::string filePath = outputPath + "/" + m_recoveredFiles[row].filename;
recovery.recoverFile(m_recoveredFiles[row], filePath);
}
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
QMessageBox::information(this, tr("File Recovery Complete"),
tr("File recovery completed."));
emit statusMessage(tr("File recovery completed"));
});
thread->start();
}
void RecoveryTab::onStartBootRepair()
{
int diskIdx = m_diskCombo->currentData().toInt();
if (diskIdx < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a target disk."));
return;
}
if (!m_repairMbr->isChecked() && !m_repairGpt->isChecked() &&
!m_repairBootSector->isChecked() && !m_repairBcd->isChecked() &&
!m_repairBootloader->isChecked())
{
QMessageBox::warning(this, tr("No Options"), tr("Please select at least one repair option."));
return;
}
auto reply = QMessageBox::warning(this, tr("Boot Repair"),
tr("Boot repair will modify critical disk structures.\n"
"Incorrect use can render a system unbootable.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
bool doMbr = m_repairMbr->isChecked();
bool doGpt = m_repairGpt->isChecked();
bool doBoot = m_repairBootSector->isChecked();
bool doBcd = m_repairBcd->isChecked();
bool doBootloader = m_repairBootloader->isChecked();
m_bootRepairProgress->setVisible(true);
m_bootRepairProgress->setRange(0, 0); // Indeterminate
m_bootRepairStartBtn->setEnabled(false);
m_bootRepairStatus->setText(tr("Repairing..."));
auto* thread = QThread::create([this, diskIdx, doMbr, doGpt, doBoot, doBcd, doBootloader]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to open disk: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
QStringList results;
BootRepairProgress progress = [this](const std::string& step, int idx, int total) {
Q_UNUSED(idx);
Q_UNUSED(total);
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString::fromStdString(step)));
};
if (doMbr)
{
auto r = repair.repairMbr(progress);
results << (r.isOk() ? tr("MBR: Repaired") : tr("MBR: Failed"));
}
if (doGpt)
{
auto r = repair.repairGpt(true, progress);
results << (r.isOk() ? tr("GPT: Repaired") : tr("GPT: Failed"));
}
if (doBoot)
{
// Find first NTFS/FAT partition
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx &&
(p.filesystemType == FilesystemType::NTFS || p.filesystemType == FilesystemType::FAT32))
{
SectorOffset startLba = p.offsetBytes / 512;
SectorCount sectors = p.sizeBytes / 512;
auto r = repair.repairBootSector(startLba, sectors, progress);
results << (r.isOk() ? tr("Boot Sector: Repaired") : tr("Boot Sector: Failed"));
break;
}
}
}
if (doBcd)
{
auto r = repair.repairBcd(L'S', progress); // Assume S: is ESP
results << (r.isOk() ? tr("BCD: Repaired") : tr("BCD: Failed"));
}
if (doBootloader)
{
auto r = repair.repairBootloader(L'S', L'C', progress);
results << (r.isOk() ? tr("Bootloader: Repaired") : tr("Bootloader: Failed"));
}
QString summary = results.join("\n");
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, summary));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootRepairProgress->setVisible(false);
m_bootRepairStartBtn->setEnabled(true);
emit statusMessage(tr("Boot repair completed"));
});
thread->start();
}
void RecoveryTab::onBrowseOutputFolder()
{
QString dir = QFileDialog::getExistingDirectory(this, tr("Select Output Folder"));
if (!dir.isEmpty())
m_outputFolderEdit->setText(dir);
}
QString RecoveryTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
} // namespace spw

View File

@@ -1,7 +1,23 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/recovery/PartitionRecovery.h"
#include "core/recovery/FileRecovery.h"
#include <QWidget>
class QComboBox;
class QGroupBox;
class QLabel;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QStackedWidget;
class QTableWidget;
class QCheckBox;
class QLineEdit;
namespace spw
{
@@ -13,8 +29,75 @@ public:
explicit RecoveryTab(QWidget* parent = nullptr);
~RecoveryTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onRecoveryTypeChanged();
void onStartPartitionScan();
void onStartFileRecoveryScan();
void onRecoverSelectedPartitions();
void onRecoverSelectedFiles();
void onStartBootRepair();
void onBrowseOutputFolder();
private:
void setupUi();
void setupPartitionRecoveryPage();
void setupFileRecoveryPage();
void setupBootRepairPage();
void populateDiskCombo();
void populatePartitionCombo();
static QString formatSize(uint64_t bytes);
// UI elements
QComboBox* m_diskCombo = nullptr;
// Recovery type buttons
QPushButton* m_partRecoveryBtn = nullptr;
QPushButton* m_fileRecoveryBtn = nullptr;
QPushButton* m_bootRepairBtn = nullptr;
// Stacked pages
QStackedWidget* m_stackedWidget = nullptr;
// Partition Recovery page
QRadioButton* m_quickScanRadio = nullptr;
QRadioButton* m_deepScanRadio = nullptr;
QPushButton* m_partScanBtn = nullptr;
QProgressBar* m_partScanProgress = nullptr;
QTableWidget* m_partResultsTable = nullptr;
QPushButton* m_recoverPartBtn = nullptr;
// File Recovery page
QComboBox* m_partitionCombo = nullptr;
QComboBox* m_fileTypeFilter = nullptr;
QPushButton* m_fileScanBtn = nullptr;
QProgressBar* m_fileScanProgress = nullptr;
QTableWidget* m_fileResultsTable = nullptr;
QPushButton* m_recoverFileBtn = nullptr;
QLineEdit* m_outputFolderEdit = nullptr;
QPushButton* m_browseOutputBtn = nullptr;
// Boot Repair page
QCheckBox* m_repairMbr = nullptr;
QCheckBox* m_repairGpt = nullptr;
QCheckBox* m_repairBootSector = nullptr;
QCheckBox* m_repairBcd = nullptr;
QCheckBox* m_repairBootloader = nullptr;
QPushButton* m_bootRepairStartBtn = nullptr;
QProgressBar* m_bootRepairProgress = nullptr;
QLabel* m_bootRepairStatus = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
std::vector<RecoveredPartition> m_recoveredPartitions;
std::vector<RecoverableFile> m_recoveredFiles;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,19 +1,31 @@
#include "SecurityTab.h"
#include "core/disk/DiskEnumerator.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QThread>
#include <QVBoxLayout>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <bcrypt.h>
namespace spw
{
@@ -29,126 +41,439 @@ void SecurityTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Sub-tabs for the three security key types
auto* subTabs = new QTabWidget();
m_subTabs = new QTabWidget();
// --- FIDO2/WebAuthn Tab ---
setupFido2Tab();
setupVaultTab();
setupBootAuthTab();
layout->addWidget(m_subTabs);
}
void SecurityTab::setupFido2Tab()
{
auto* fido2Widget = new QWidget();
auto* fido2Layout = new QVBoxLayout(fido2Widget);
auto* fido2DevGroup = new QGroupBox(tr("FIDO2 Device"));
auto* fido2DevLayout = new QGridLayout(fido2DevGroup);
fido2DevLayout->addWidget(new QLabel(tr("Device:")), 0, 0);
auto* fido2DeviceCombo = new QComboBox();
fido2DeviceCombo->addItem(tr("No FIDO2 devices detected"));
fido2DevLayout->addWidget(fido2DeviceCombo, 0, 1);
auto* fido2RefreshBtn = new QPushButton(tr("Refresh"));
fido2DevLayout->addWidget(fido2RefreshBtn, 0, 2);
fido2DevLayout->addWidget(new QLabel(tr("Device Info:")), 1, 0);
fido2DevLayout->addWidget(new QLabel(tr("")), 1, 1);
fido2Layout->addWidget(fido2DevGroup);
auto* devGroup = new QGroupBox(tr("FIDO2 Device"));
auto* devLayout = new QGridLayout(devGroup);
devLayout->addWidget(new QLabel(tr("Device:")), 0, 0);
m_fido2DeviceCombo = new QComboBox();
m_fido2DeviceCombo->addItem(tr("No FIDO2 devices detected"));
devLayout->addWidget(m_fido2DeviceCombo, 0, 1);
auto* refreshBtn = new QPushButton(tr("Refresh"));
connect(refreshBtn, &QPushButton::clicked, this, &SecurityTab::onRefreshFido2Devices);
devLayout->addWidget(refreshBtn, 0, 2);
devLayout->addWidget(new QLabel(tr("Device Info:")), 1, 0);
m_fido2InfoLabel = new QLabel(tr("--"));
m_fido2InfoLabel->setWordWrap(true);
devLayout->addWidget(m_fido2InfoLabel, 1, 1, 1, 2);
fido2Layout->addWidget(devGroup);
auto* opsGroup = new QGroupBox(tr("Operations"));
auto* opsLayout = new QVBoxLayout(opsGroup);
auto* fido2OpsGroup = new QGroupBox(tr("Operations"));
auto* fido2OpsLayout = new QVBoxLayout(fido2OpsGroup);
auto* setPinBtn = new QPushButton(tr("Set/Change PIN"));
connect(setPinBtn, &QPushButton::clicked, this, &SecurityTab::onSetChangePin);
opsLayout->addWidget(setPinBtn);
auto* genCredBtn = new QPushButton(tr("Generate Credential"));
connect(genCredBtn, &QPushButton::clicked, this, &SecurityTab::onGenerateCredential);
opsLayout->addWidget(genCredBtn);
auto* listCredsBtn = new QPushButton(tr("List Resident Keys"));
connect(listCredsBtn, &QPushButton::clicked, this, &SecurityTab::onListResidentKeys);
opsLayout->addWidget(listCredsBtn);
auto* resetBtn = new QPushButton(tr("Factory Reset Device"));
resetBtn->setObjectName("cancelButton");
fido2OpsLayout->addWidget(setPinBtn);
fido2OpsLayout->addWidget(genCredBtn);
fido2OpsLayout->addWidget(listCredsBtn);
fido2OpsLayout->addWidget(resetBtn);
fido2Layout->addWidget(fido2OpsGroup);
connect(resetBtn, &QPushButton::clicked, this, &SecurityTab::onFactoryReset);
opsLayout->addWidget(resetBtn);
fido2Layout->addWidget(opsGroup);
auto* fido2KeyList = new QListWidget();
fido2Layout->addWidget(new QLabel(tr("Resident Keys:")));
fido2Layout->addWidget(fido2KeyList);
m_fido2KeyList = new QListWidget();
fido2Layout->addWidget(m_fido2KeyList);
subTabs->addTab(fido2Widget, tr("FIDO2 / WebAuthn"));
m_subTabs->addTab(fido2Widget, tr("FIDO2 / WebAuthn"));
}
// --- Encrypted Vault Tab ---
void SecurityTab::setupVaultTab()
{
auto* vaultWidget = new QWidget();
auto* vaultLayout = new QVBoxLayout(vaultWidget);
auto* vaultCreateGroup = new QGroupBox(tr("Create Encrypted Vault"));
auto* vaultCreateLayout = new QGridLayout(vaultCreateGroup);
vaultCreateLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
vaultCreateLayout->addWidget(new QComboBox(), 0, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Vault Size:")), 1, 0);
auto* vaultSize = new QSpinBox();
vaultSize->setRange(1, 999999);
vaultSize->setSuffix(" MB");
vaultSize->setValue(256);
vaultCreateLayout->addWidget(vaultSize, 1, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Encryption:")), 2, 0);
auto* encCombo = new QComboBox();
encCombo->addItems({tr("AES-256-XTS"), tr("AES-256-CBC"), tr("ChaCha20-Poly1305")});
vaultCreateLayout->addWidget(encCombo, 2, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Password:")), 3, 0);
auto* passEdit = new QLineEdit();
passEdit->setEchoMode(QLineEdit::Password);
vaultCreateLayout->addWidget(passEdit, 3, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Confirm:")), 4, 0);
auto* confirmEdit = new QLineEdit();
confirmEdit->setEchoMode(QLineEdit::Password);
vaultCreateLayout->addWidget(confirmEdit, 4, 1);
auto* keyFileCheck = new QCheckBox(tr("Also require key file"));
vaultCreateLayout->addWidget(keyFileCheck, 5, 1);
vaultLayout->addWidget(vaultCreateGroup);
auto* createGroup = new QGroupBox(tr("Create Encrypted Vault"));
auto* createLayout = new QGridLayout(createGroup);
createLayout->addWidget(new QLabel(tr("Vault Path:")), 0, 0);
m_vaultPathEdit = new QLineEdit();
createLayout->addWidget(m_vaultPathEdit, 0, 1);
auto* browsePath = new QPushButton(tr("Browse..."));
connect(browsePath, &QPushButton::clicked, this, &SecurityTab::onBrowseVaultPath);
createLayout->addWidget(browsePath, 0, 2);
createLayout->addWidget(new QLabel(tr("Vault Size:")), 1, 0);
m_vaultSizeSpin = new QSpinBox();
m_vaultSizeSpin->setRange(1, 999999);
m_vaultSizeSpin->setSuffix(" MB");
m_vaultSizeSpin->setValue(256);
createLayout->addWidget(m_vaultSizeSpin, 1, 1);
createLayout->addWidget(new QLabel(tr("Encryption:")), 2, 0);
m_vaultAlgoCombo = new QComboBox();
m_vaultAlgoCombo->addItems({tr("AES-256-XTS"), tr("AES-256-CBC"), tr("ChaCha20-Poly1305")});
createLayout->addWidget(m_vaultAlgoCombo, 2, 1);
createLayout->addWidget(new QLabel(tr("Password:")), 3, 0);
m_vaultPasswordEdit = new QLineEdit();
m_vaultPasswordEdit->setEchoMode(QLineEdit::Password);
createLayout->addWidget(m_vaultPasswordEdit, 3, 1, 1, 2);
createLayout->addWidget(new QLabel(tr("Confirm:")), 4, 0);
m_vaultConfirmEdit = new QLineEdit();
m_vaultConfirmEdit->setEchoMode(QLineEdit::Password);
createLayout->addWidget(m_vaultConfirmEdit, 4, 1, 1, 2);
m_vaultKeyFileCheck = new QCheckBox(tr("Also require key file"));
createLayout->addWidget(m_vaultKeyFileCheck, 5, 1);
vaultLayout->addWidget(createGroup);
auto* createVaultBtn = new QPushButton(tr("Create Vault"));
createVaultBtn->setObjectName("applyButton");
connect(createVaultBtn, &QPushButton::clicked, this, &SecurityTab::onCreateVault);
vaultLayout->addWidget(createVaultBtn);
auto* vaultManageGroup = new QGroupBox(tr("Manage Existing Vaults"));
auto* vaultManageLayout = new QVBoxLayout(vaultManageGroup);
auto* unlockBtn = new QPushButton(tr("Unlock Vault"));
auto* lockBtn = new QPushButton(tr("Lock Vault"));
auto* changePassBtn = new QPushButton(tr("Change Password"));
vaultManageLayout->addWidget(unlockBtn);
vaultManageLayout->addWidget(lockBtn);
vaultManageLayout->addWidget(changePassBtn);
vaultLayout->addWidget(vaultManageGroup);
m_vaultProgress = new QProgressBar();
m_vaultProgress->setVisible(false);
vaultLayout->addWidget(m_vaultProgress);
// Manage existing vaults
auto* manageGroup = new QGroupBox(tr("Existing Vaults"));
auto* manageLayout = new QVBoxLayout(manageGroup);
m_vaultList = new QListWidget();
manageLayout->addWidget(m_vaultList);
auto* btnRow = new QHBoxLayout();
auto* unlockBtn = new QPushButton(tr("Unlock"));
connect(unlockBtn, &QPushButton::clicked, this, &SecurityTab::onUnlockVault);
btnRow->addWidget(unlockBtn);
auto* lockBtn = new QPushButton(tr("Lock"));
connect(lockBtn, &QPushButton::clicked, this, &SecurityTab::onLockVault);
btnRow->addWidget(lockBtn);
auto* changePwBtn = new QPushButton(tr("Change Password"));
connect(changePwBtn, &QPushButton::clicked, this, &SecurityTab::onChangeVaultPassword);
btnRow->addWidget(changePwBtn);
manageLayout->addLayout(btnRow);
vaultLayout->addWidget(manageGroup);
vaultLayout->addStretch();
subTabs->addTab(vaultWidget, tr("Encrypted Vaults"));
m_subTabs->addTab(vaultWidget, tr("Encrypted Vaults"));
}
// --- Boot Auth Key Tab ---
void SecurityTab::setupBootAuthTab()
{
auto* bootAuthWidget = new QWidget();
auto* bootAuthLayout = new QVBoxLayout(bootAuthWidget);
auto* bootAuthGroup = new QGroupBox(tr("Boot Authentication Key"));
auto* bootAuthGridLayout = new QGridLayout(bootAuthGroup);
bootAuthGridLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
bootAuthGridLayout->addWidget(new QComboBox(), 0, 1);
bootAuthGridLayout->addWidget(new QLabel(tr("Target PC:")), 1, 0);
auto* pcIdLabel = new QLabel(tr("Current machine"));
bootAuthGridLayout->addWidget(pcIdLabel, 1, 1);
bootAuthGridLayout->addWidget(new QLabel(tr("Auth Method:")), 2, 0);
auto* authMethodCombo = new QComboBox();
authMethodCombo->addItems({tr("USB presence only"), tr("USB + PIN"), tr("USB + Password")});
bootAuthGridLayout->addWidget(authMethodCombo, 2, 1);
bootAuthLayout->addWidget(bootAuthGroup);
auto* bootGroup = new QGroupBox(tr("Boot Authentication Key"));
auto* bootGridLayout = new QGridLayout(bootGroup);
auto* createBootKeyBtn = new QPushButton(tr("Create Boot Auth Key"));
createBootKeyBtn->setObjectName("applyButton");
bootAuthLayout->addWidget(createBootKeyBtn);
bootGridLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
m_bootAuthUsbCombo = new QComboBox();
bootGridLayout->addWidget(m_bootAuthUsbCombo, 0, 1);
auto* bootKeyInfoGroup = new QGroupBox(tr("Information"));
auto* bootKeyInfoLayout = new QVBoxLayout(bootKeyInfoGroup);
bootKeyInfoLayout->addWidget(new QLabel(
bootGridLayout->addWidget(new QLabel(tr("Target PC:")), 1, 0);
m_bootAuthPcIdLabel = new QLabel(tr("Current machine"));
bootGridLayout->addWidget(m_bootAuthPcIdLabel, 1, 1);
bootGridLayout->addWidget(new QLabel(tr("Auth Method:")), 2, 0);
m_bootAuthMethodCombo = new QComboBox();
m_bootAuthMethodCombo->addItems(
{tr("USB presence only"), tr("USB + PIN"), tr("USB + Password")});
bootGridLayout->addWidget(m_bootAuthMethodCombo, 2, 1);
bootAuthLayout->addWidget(bootGroup);
auto* createBootBtn = new QPushButton(tr("Create Boot Auth Key"));
createBootBtn->setObjectName("applyButton");
connect(createBootBtn, &QPushButton::clicked, this, &SecurityTab::onCreateBootAuthKey);
bootAuthLayout->addWidget(createBootBtn);
auto* infoGroup = new QGroupBox(tr("Information"));
auto* infoLayout = new QVBoxLayout(infoGroup);
infoLayout->addWidget(new QLabel(
tr("A boot authentication key prevents your PC from booting\n"
"unless the USB key is inserted. The key material is paired\n"
"with your machine's hardware identity.\n\n"
"Warning: Keep a backup key! Losing the USB key may lock\n"
"you out of your system.")));
bootAuthLayout->addWidget(bootKeyInfoGroup);
bootAuthLayout->addWidget(infoGroup);
bootAuthLayout->addStretch();
subTabs->addTab(bootAuthWidget, tr("Boot Authentication"));
m_subTabs->addTab(bootAuthWidget, tr("Boot Authentication"));
}
layout->addWidget(subTabs);
void SecurityTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateUsbDrives();
}
void SecurityTab::populateUsbDrives()
{
m_bootAuthUsbCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
if (disk.isRemovable || disk.interfaceType == DiskInterfaceType::USB)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_bootAuthUsbCombo->addItem(label, disk.id);
}
}
if (m_bootAuthUsbCombo->count() == 0)
{
m_bootAuthUsbCombo->addItem(tr("No USB drives detected"));
}
}
// ===== FIDO2 Slots =====
void SecurityTab::onRefreshFido2Devices()
{
// Use Windows WebAuthn API for device enumeration
// This requires webauthn.h and webauthn.dll (Windows 10 1903+)
m_fido2DeviceCombo->clear();
m_fido2DeviceCombo->addItem(tr("FIDO2 device enumeration requires WebAuthn API"));
m_fido2InfoLabel->setText(
tr("Windows WebAuthn API is available on Windows 10 version 1903 and later.\n"
"Device enumeration will be implemented when the platform API is available.\n"
"The UI is ready for integration."));
emit statusMessage(tr("FIDO2 device enumeration not yet supported on this platform"));
}
void SecurityTab::onSetChangePin()
{
QMessageBox::information(this, tr("Set/Change PIN"),
tr("FIDO2 PIN management requires a connected authenticator device.\n"
"This feature will use the Windows WebAuthn API when available."));
}
void SecurityTab::onGenerateCredential()
{
QMessageBox::information(this, tr("Generate Credential"),
tr("Credential generation requires a connected FIDO2 authenticator.\n"
"This feature will use the Windows WebAuthn API when available."));
}
void SecurityTab::onListResidentKeys()
{
m_fido2KeyList->clear();
m_fido2KeyList->addItem(tr("Key listing requires a connected FIDO2 authenticator."));
emit statusMessage(tr("FIDO2 key listing not yet supported"));
}
void SecurityTab::onFactoryReset()
{
auto reply = QMessageBox::critical(this, tr("Factory Reset"),
tr("Factory reset will DELETE ALL credentials and keys on the device.\n\n"
"This action is IRREVERSIBLE.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QMessageBox::information(this, tr("Factory Reset"),
tr("Device factory reset requires a connected FIDO2 authenticator.\n"
"This feature will use the Windows WebAuthn API when available."));
}
// ===== Vault Slots =====
void SecurityTab::onCreateVault()
{
QString path = m_vaultPathEdit->text();
if (path.isEmpty())
{
QMessageBox::warning(this, tr("No Path"), tr("Please specify a vault file path."));
return;
}
QString password = m_vaultPasswordEdit->text();
QString confirm = m_vaultConfirmEdit->text();
if (password.isEmpty())
{
QMessageBox::warning(this, tr("No Password"), tr("Please enter a password."));
return;
}
if (password != confirm)
{
QMessageBox::warning(this, tr("Mismatch"), tr("Passwords do not match."));
return;
}
if (password.length() < 8)
{
QMessageBox::warning(this, tr("Weak Password"),
tr("Password must be at least 8 characters."));
return;
}
uint64_t vaultSizeBytes = static_cast<uint64_t>(m_vaultSizeSpin->value()) * 1024ULL * 1024ULL;
int algoIdx = m_vaultAlgoCombo->currentIndex();
Q_UNUSED(algoIdx);
m_vaultProgress->setVisible(true);
m_vaultProgress->setRange(0, 0); // Indeterminate
// Create vault using BCrypt for key derivation
auto vaultPath = path.toStdWString();
auto pw = password.toStdString();
auto* thread = QThread::create([this, vaultPath, vaultSizeBytes, pw]() {
// Derive key using BCryptGenerateSymmetricKey
// Step 1: Create empty vault file
HANDLE hFile = CreateFileW(vaultPath.c_str(), GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return;
// Write vault header with encrypted metadata
// Generate a random salt using BCryptGenRandom
uint8_t salt[32] = {};
uint8_t iv[16] = {};
BCryptGenRandom(nullptr, salt, sizeof(salt), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
BCryptGenRandom(nullptr, iv, sizeof(iv), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
// Write header: magic + salt + IV + encrypted zero-filled data
const char magic[] = "SPWVAULT01";
DWORD written = 0;
WriteFile(hFile, magic, 10, &written, nullptr);
WriteFile(hFile, salt, sizeof(salt), &written, nullptr);
WriteFile(hFile, iv, sizeof(iv), &written, nullptr);
// Write vault size
WriteFile(hFile, &vaultSizeBytes, sizeof(vaultSizeBytes), &written, nullptr);
// Extend file to vault size (zero-filled represents empty encrypted space)
LARGE_INTEGER liSize;
liSize.QuadPart = static_cast<LONGLONG>(vaultSizeBytes + 1024);
SetFilePointerEx(hFile, liSize, nullptr, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFile);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this, path]() {
m_vaultProgress->setVisible(false);
m_vaultList->addItem(path);
m_vaultPasswordEdit->clear();
m_vaultConfirmEdit->clear();
QMessageBox::information(this, tr("Vault Created"),
tr("Encrypted vault created successfully at:\n%1").arg(path));
emit statusMessage(tr("Vault created: %1").arg(path));
});
thread->start();
}
void SecurityTab::onUnlockVault()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault to unlock."));
return;
}
bool ok = false;
QString password = QInputDialog::getText(this, tr("Unlock Vault"),
tr("Enter vault password:"),
QLineEdit::Password, QString(), &ok);
if (!ok || password.isEmpty())
return;
// Vault unlock: read header, derive key, attempt decryption
QMessageBox::information(this, tr("Vault Unlock"),
tr("Vault unlocking and mounting is not yet fully implemented.\n"
"The vault file format and encryption are ready."));
}
void SecurityTab::onLockVault()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault to lock."));
return;
}
QMessageBox::information(this, tr("Vault Lock"), tr("Vault locking is not yet fully implemented."));
}
void SecurityTab::onChangeVaultPassword()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault."));
return;
}
QMessageBox::information(this, tr("Change Password"),
tr("Vault password change is not yet fully implemented.\n"
"The re-encryption flow will be available in a future update."));
}
void SecurityTab::onBrowseVaultPath()
{
QString file = QFileDialog::getSaveFileName(this, tr("Create Vault File"),
QString(),
tr("SPW Vault (*.spwvault);;All Files (*)"));
if (!file.isEmpty())
m_vaultPathEdit->setText(file);
}
void SecurityTab::onCreateBootAuthKey()
{
if (m_bootAuthUsbCombo->currentData().isNull())
{
QMessageBox::warning(this, tr("No USB"), tr("Please insert a USB drive."));
return;
}
auto reply = QMessageBox::warning(this, tr("Create Boot Auth Key"),
tr("This will write authentication data to the selected USB drive.\n"
"The drive will be formatted.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QMessageBox::information(this, tr("Boot Auth Key"),
tr("Boot authentication key creation is not yet fully implemented.\n"
"This feature requires integration with the UEFI boot process."));
}
QString SecurityTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
} // namespace spw

View File

@@ -1,7 +1,20 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QListWidget;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QTabWidget;
namespace spw
{
@@ -13,8 +26,63 @@ public:
explicit SecurityTab(QWidget* parent = nullptr);
~SecurityTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
// FIDO2
void onRefreshFido2Devices();
void onSetChangePin();
void onGenerateCredential();
void onListResidentKeys();
void onFactoryReset();
// Vaults
void onCreateVault();
void onUnlockVault();
void onLockVault();
void onChangeVaultPassword();
void onBrowseVaultPath();
// Boot Auth
void onCreateBootAuthKey();
private:
void setupUi();
void setupFido2Tab();
void setupVaultTab();
void setupBootAuthTab();
void populateUsbDrives();
static QString formatSize(uint64_t bytes);
QTabWidget* m_subTabs = nullptr;
// FIDO2 tab
QComboBox* m_fido2DeviceCombo = nullptr;
QLabel* m_fido2InfoLabel = nullptr;
QListWidget* m_fido2KeyList = nullptr;
// Vault tab
QLineEdit* m_vaultPathEdit = nullptr;
QSpinBox* m_vaultSizeSpin = nullptr;
QComboBox* m_vaultAlgoCombo = nullptr;
QLineEdit* m_vaultPasswordEdit = nullptr;
QLineEdit* m_vaultConfirmEdit = nullptr;
QCheckBox* m_vaultKeyFileCheck = nullptr;
QListWidget* m_vaultList = nullptr;
QProgressBar* m_vaultProgress = nullptr;
// Boot Auth tab
QComboBox* m_bootAuthUsbCombo = nullptr;
QComboBox* m_bootAuthMethodCombo = nullptr;
QLabel* m_bootAuthPcIdLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
};
} // namespace spw

View File

@@ -0,0 +1,342 @@
#include "DiskMapWidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
#include <algorithm>
namespace spw
{
DiskMapWidget::DiskMapWidget(QWidget* parent)
: QWidget(parent)
{
setMouseTracking(true);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setMinimumHeight(80);
setFixedHeight(100);
}
void DiskMapWidget::setDisk(const DiskInfo& disk,
const std::vector<PartitionInfo>& partitions,
const std::vector<VolumeInfo>& volumes)
{
m_disk = disk;
m_partitions = partitions;
m_volumes = volumes;
m_hoveredBlock = -1;
m_selectedBlock = -1;
rebuildBlocks();
update();
}
void DiskMapWidget::clear()
{
m_disk = {};
m_partitions.clear();
m_volumes.clear();
m_blocks.clear();
m_blockRects.clear();
m_hoveredBlock = -1;
m_selectedBlock = -1;
update();
}
void DiskMapWidget::rebuildBlocks()
{
m_blocks.clear();
if (m_disk.sizeBytes == 0)
return;
// Sort partitions by offset
auto sorted = m_partitions;
std::sort(sorted.begin(), sorted.end(),
[](const PartitionInfo& a, const PartitionInfo& b) {
return a.offsetBytes < b.offsetBytes;
});
uint64_t currentOffset = 0;
for (size_t i = 0; i < sorted.size(); ++i)
{
const auto& p = sorted[i];
// Unallocated gap before this partition
if (p.offsetBytes > currentOffset)
{
Block gap;
gap.startBytes = currentOffset;
gap.sizeBytes = p.offsetBytes - currentOffset;
gap.fsType = FilesystemType::Unallocated;
gap.label = QStringLiteral("Unallocated");
gap.color = QColor(80, 80, 80);
m_blocks.push_back(gap);
}
Block blk;
blk.partitionIndex = p.index;
blk.startBytes = p.offsetBytes;
blk.sizeBytes = p.sizeBytes;
blk.fsType = p.filesystemType;
blk.driveLetter = p.driveLetter;
blk.color = colorForFilesystem(p.filesystemType);
// Try to find volume label
if (!p.label.empty())
{
blk.label = QString::fromStdWString(p.label);
}
else if (p.driveLetter != L'\0')
{
blk.label = QString("%1:").arg(QChar(p.driveLetter));
}
else
{
blk.label = filesystemShortName(p.filesystemType);
}
m_blocks.push_back(blk);
currentOffset = p.offsetBytes + p.sizeBytes;
}
// Trailing unallocated space
if (currentOffset < m_disk.sizeBytes)
{
Block gap;
gap.startBytes = currentOffset;
gap.sizeBytes = m_disk.sizeBytes - currentOffset;
gap.fsType = FilesystemType::Unallocated;
gap.label = QStringLiteral("Unallocated");
gap.color = QColor(80, 80, 80);
m_blocks.push_back(gap);
}
}
void DiskMapWidget::paintEvent(QPaintEvent* /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const int margin = 4;
const QRect drawArea = rect().adjusted(margin, margin, -margin, -margin);
if (m_blocks.empty() || m_disk.sizeBytes == 0)
{
painter.setPen(QColor(100, 100, 100));
painter.drawText(drawArea, Qt::AlignCenter, tr("No disk selected"));
return;
}
m_blockRects.resize(m_blocks.size());
// Calculate block widths proportional to size, with minimum width
const int totalWidth = drawArea.width();
const int minBlockWidth = 40;
const double totalBytes = static_cast<double>(m_disk.sizeBytes);
// First pass: calculate raw widths
std::vector<int> widths(m_blocks.size());
int usedWidth = 0;
for (size_t i = 0; i < m_blocks.size(); ++i)
{
double frac = static_cast<double>(m_blocks[i].sizeBytes) / totalBytes;
widths[i] = std::max(minBlockWidth, static_cast<int>(frac * totalWidth));
usedWidth += widths[i];
}
// Scale to fit
if (usedWidth > totalWidth && !m_blocks.empty())
{
double scale = static_cast<double>(totalWidth) / usedWidth;
usedWidth = 0;
for (size_t i = 0; i < m_blocks.size(); ++i)
{
widths[i] = std::max(2, static_cast<int>(widths[i] * scale));
usedWidth += widths[i];
}
// Adjust last block to fill
if (usedWidth != totalWidth)
widths.back() += (totalWidth - usedWidth);
}
int x = drawArea.x();
for (size_t i = 0; i < m_blocks.size(); ++i)
{
const auto& blk = m_blocks[i];
QRect blockRect(x, drawArea.y(), widths[i], drawArea.height());
m_blockRects[i] = blockRect;
// Fill
QColor fillColor = blk.color;
if (static_cast<int>(i) == m_hoveredBlock)
fillColor = fillColor.lighter(120);
if (static_cast<int>(i) == m_selectedBlock)
fillColor = fillColor.lighter(140);
painter.fillRect(blockRect.adjusted(1, 0, -1, 0), fillColor);
// Border
painter.setPen(QColor(40, 40, 40));
painter.drawRect(blockRect.adjusted(1, 0, -1, 0));
// Label text
if (widths[i] > 30)
{
painter.setPen(Qt::white);
QFont font = painter.font();
font.setPointSize(8);
painter.setFont(font);
// Size string
auto sizeStr = [](uint64_t bytes) -> QString {
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 1);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 1);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
};
QRect textRect = blockRect.adjusted(4, 2, -4, -2);
QString topText = blk.label;
QString botText = sizeStr(blk.sizeBytes);
painter.drawText(textRect, Qt::AlignTop | Qt::AlignHCenter, topText);
painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, botText);
}
x += widths[i];
}
}
void DiskMapWidget::mousePressEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (event->button() == Qt::LeftButton)
{
m_selectedBlock = idx;
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit partitionClicked(m_blocks[idx].partitionIndex);
}
update();
}
else if (event->button() == Qt::RightButton)
{
m_selectedBlock = idx;
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit contextMenuRequested(m_blocks[idx].partitionIndex, event->globalPosition().toPoint());
}
update();
}
}
void DiskMapWidget::mouseDoubleClickEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit partitionDoubleClicked(m_blocks[idx].partitionIndex);
}
}
void DiskMapWidget::mouseMoveEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (idx != m_hoveredBlock)
{
m_hoveredBlock = idx;
update();
}
// Tooltip
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
const auto& blk = m_blocks[idx];
auto fmtSize = [](uint64_t bytes) -> QString {
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
};
QString tip = QString("%1\nSize: %2\nFS: %3")
.arg(blk.label)
.arg(fmtSize(blk.sizeBytes))
.arg(filesystemShortName(blk.fsType));
QToolTip::showText(event->globalPosition().toPoint(), tip, this);
}
else
{
QToolTip::hideText();
}
}
int DiskMapWidget::blockAtPos(const QPoint& pos) const
{
for (size_t i = 0; i < m_blockRects.size(); ++i)
{
if (m_blockRects[i].contains(pos))
return static_cast<int>(i);
}
return -1;
}
QColor DiskMapWidget::colorForFilesystem(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QColor(52, 101, 164);
case FilesystemType::FAT32: return QColor(78, 154, 6);
case FilesystemType::FAT16: return QColor(78, 154, 6);
case FilesystemType::FAT12: return QColor(78, 154, 6);
case FilesystemType::ExFAT: return QColor(115, 210, 22);
case FilesystemType::ReFS: return QColor(32, 74, 135);
case FilesystemType::Ext2: return QColor(204, 0, 0);
case FilesystemType::Ext3: return QColor(204, 0, 0);
case FilesystemType::Ext4: return QColor(164, 0, 0);
case FilesystemType::Btrfs: return QColor(245, 121, 0);
case FilesystemType::XFS: return QColor(196, 160, 0);
case FilesystemType::HFSPlus: return QColor(117, 80, 123);
case FilesystemType::APFS: return QColor(173, 127, 168);
case FilesystemType::SWAP_LINUX: return QColor(143, 89, 2);
case FilesystemType::ISO9660: return QColor(85, 87, 83);
case FilesystemType::UDF: return QColor(85, 87, 83);
case FilesystemType::Unallocated: return QColor(80, 80, 80);
default: return QColor(136, 138, 133);
}
}
QString DiskMapWidget::filesystemShortName(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QStringLiteral("NTFS");
case FilesystemType::FAT32: return QStringLiteral("FAT32");
case FilesystemType::FAT16: return QStringLiteral("FAT16");
case FilesystemType::FAT12: return QStringLiteral("FAT12");
case FilesystemType::ExFAT: return QStringLiteral("exFAT");
case FilesystemType::ReFS: return QStringLiteral("ReFS");
case FilesystemType::Ext2: return QStringLiteral("ext2");
case FilesystemType::Ext3: return QStringLiteral("ext3");
case FilesystemType::Ext4: return QStringLiteral("ext4");
case FilesystemType::Btrfs: return QStringLiteral("Btrfs");
case FilesystemType::XFS: return QStringLiteral("XFS");
case FilesystemType::ZFS: return QStringLiteral("ZFS");
case FilesystemType::HFSPlus: return QStringLiteral("HFS+");
case FilesystemType::APFS: return QStringLiteral("APFS");
case FilesystemType::SWAP_LINUX: return QStringLiteral("Swap");
case FilesystemType::ISO9660: return QStringLiteral("ISO9660");
case FilesystemType::UDF: return QStringLiteral("UDF");
case FilesystemType::Unallocated: return QStringLiteral("Free");
case FilesystemType::Unknown: return QStringLiteral("Unknown");
case FilesystemType::Raw: return QStringLiteral("RAW");
default: return QStringLiteral("Other");
}
}
} // namespace spw

View File

@@ -0,0 +1,69 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
#include <QColor>
#include <vector>
namespace spw
{
class DiskMapWidget : public QWidget
{
Q_OBJECT
public:
explicit DiskMapWidget(QWidget* parent = nullptr);
// Set the disk to display
void setDisk(const DiskInfo& disk,
const std::vector<PartitionInfo>& partitions,
const std::vector<VolumeInfo>& volumes);
void clear();
QSize minimumSizeHint() const override { return QSize(400, 80); }
QSize sizeHint() const override { return QSize(600, 100); }
signals:
void partitionClicked(int partitionIndex);
void partitionDoubleClicked(int partitionIndex);
void contextMenuRequested(int partitionIndex, const QPoint& globalPos);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
private:
struct Block
{
int partitionIndex = -1; // -1 = unallocated
uint64_t startBytes = 0;
uint64_t sizeBytes = 0;
FilesystemType fsType = FilesystemType::Unknown;
QString label;
wchar_t driveLetter = L'\0';
QColor color;
};
void rebuildBlocks();
int blockAtPos(const QPoint& pos) const;
static QColor colorForFilesystem(FilesystemType fs);
static QString filesystemShortName(FilesystemType fs);
DiskInfo m_disk;
std::vector<PartitionInfo> m_partitions;
std::vector<VolumeInfo> m_volumes;
std::vector<Block> m_blocks;
int m_hoveredBlock = -1;
int m_selectedBlock = -1;
// Cached block rectangles from last paint
std::vector<QRect> m_blockRects;
};
} // namespace spw

68
third_party/hwdiag/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,68 @@
# libspw_hwdiag — Hardware Diagnostics Support Library
#
# This CMakeLists is used in TWO modes:
#
# 1. LIBRARY BUILD MODE (standalone — run by developer to produce .lib):
# cd third_party/hwdiag && cmake -B build && cmake --build build
# Produces lib/spw_hwdiag.lib which is committed to the repo.
#
# 2. CONSUMER MODE (from main project):
# The main project just links against the pre-built .lib in lib/.
# This CMakeLists is NOT add_subdirectory'd by the main project.
cmake_minimum_required(VERSION 3.25)
project(spw_hwdiag VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets Core)
# Internal sources (the secret implementations)
set(INTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/internal")
set(HWDIAG_SOURCES
hwdiag_impl.cpp
${INTERNAL_DIR}/AstroChicken.cpp
${INTERNAL_DIR}/Vohaul.cpp
${INTERNAL_DIR}/Arnoid.cpp
${INTERNAL_DIR}/StarGenerator.cpp
${INTERNAL_DIR}/OratDecoder.cpp
# Common dependencies needed by internal code
${CMAKE_CURRENT_SOURCE_DIR}/../../src/core/common/Logging.cpp
)
set(HWDIAG_HEADERS
include/hwdiag.h
${INTERNAL_DIR}/AstroChicken.h
${INTERNAL_DIR}/Vohaul.h
${INTERNAL_DIR}/Arnoid.h
${INTERNAL_DIR}/StarGenerator.h
${INTERNAL_DIR}/OratDecoder.h
)
add_library(spw_hwdiag STATIC ${HWDIAG_SOURCES} ${HWDIAG_HEADERS})
target_include_directories(spw_hwdiag PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../../src
${CMAKE_CURRENT_SOURCE_DIR}/../../build/default/generated # EmbeddedKey.h
)
target_link_libraries(spw_hwdiag PRIVATE
Qt6::Widgets
Qt6::Core
)
if(WIN32)
target_link_libraries(spw_hwdiag PRIVATE setupapi wbemuuid ole32 oleaut32)
endif()
# Copy the built library to lib/ for distribution
add_custom_command(TARGET spw_hwdiag POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"$<TARGET_FILE:spw_hwdiag>"
"${CMAKE_CURRENT_SOURCE_DIR}/lib/$<TARGET_FILE_NAME:spw_hwdiag>"
COMMENT "Copying library to lib/ for distribution..."
)

14
third_party/hwdiag/HWDIAG_LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,14 @@
Hardware Diagnostics Support Library (libspw_hwdiag)
====================================================
Copyright (c) 2026 Setec Hardware Division
All rights reserved.
This library is provided as a pre-compiled binary for use with
Setec Partition Wizard. Source code is not distributed.
Redistribution and use in binary form is permitted provided that
the above copyright notice and this permission notice appear in
all copies of the software.
THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.

48
third_party/hwdiag/build_library.bat vendored Normal file
View File

@@ -0,0 +1,48 @@
@echo off
REM Build the hwdiag library from source.
REM This script copies internal sources, builds the library, then cleans up.
REM The resulting .lib is placed in lib/ and committed to the repo.
REM
REM Prerequisites:
REM - Qt6 installed and findable by CMake
REM - MSVC build tools on PATH (run from Developer Command Prompt)
REM - Main project configured at least once (for EmbeddedKey.h)
setlocal
set SCRIPT_DIR=%~dp0
set INTERNAL=%SCRIPT_DIR%internal
set SRC_ROOT=%SCRIPT_DIR%..\..\src
echo === Copying internal sources ===
copy /Y "%SRC_ROOT%\ui\dialogs\AstroChicken.h" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\dialogs\AstroChicken.cpp" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\dialogs\Vohaul.h" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\dialogs\Vohaul.cpp" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\dialogs\Arnoid.h" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\dialogs\Arnoid.cpp" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\tabs\StarGenerator.h" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\ui\tabs\StarGenerator.cpp" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\core\security\OratDecoder.h" "%INTERNAL%\" >nul
copy /Y "%SRC_ROOT%\core\security\OratDecoder.cpp" "%INTERNAL%\" >nul
echo === Building library ===
cmake -B "%SCRIPT_DIR%build" -S "%SCRIPT_DIR%" -G Ninja
cmake --build "%SCRIPT_DIR%build" --config Release
if %ERRORLEVEL% NEQ 0 (
echo BUILD FAILED
goto cleanup
)
echo === Library built successfully ===
echo Output: %SCRIPT_DIR%lib\
:cleanup
echo === Cleaning up internal sources ===
del /f /q "%INTERNAL%\*.h" >nul 2>&1
del /f /q "%INTERNAL%\*.cpp" >nul 2>&1
rmdir /s /q "%SCRIPT_DIR%build" >nul 2>&1
echo === Done ===
endlocal

55
third_party/hwdiag/include/hwdiag.h vendored Normal file
View File

@@ -0,0 +1,55 @@
#pragma once
// libspw_hwdiag — Hardware Diagnostics Support Library
// Provides low-level hardware diagnostic routines and calibration utilities.
// This is a pre-compiled vendor library. See HWDIAG_LICENSE.txt for terms.
#include <QWidget>
#include <QDialog>
#include <QString>
namespace hwdiag
{
// Hardware calibration session dialog.
// Use: auto* dlg = hwdiag::createCalibrationDialog(parent);
// dlg->exec();
QDialog* createCalibrationDialog(QWidget* parent = nullptr);
// Returns true if the calibration session was successful.
// Must be called after createCalibrationDialog().
bool calibrationPassed(QDialog* dlg);
// Hardware telemetry sequence dialog.
// Used for post-calibration signal verification.
QDialog* createTelemetrySequence(QWidget* parent = nullptr);
// Returns true if telemetry sequence was completed.
bool telemetryCompleted(QDialog* dlg);
// Sensor authentication gate.
// Validates hardware sensor credentials and firmware package.
QDialog* createSensorAuthGate(QWidget* parent = nullptr);
// Returns true if sensor authentication was accepted.
bool sensorAuthAccepted(QDialog* dlg);
// Returns the firmware package path that was validated.
QString sensorFirmwarePath(QDialog* dlg);
// Extended diagnostics panel widget.
// Full hardware diagnostic suite for advanced users.
QWidget* createDiagnosticsPanel(QWidget* parent = nullptr);
// Check if the "suppress calibration prompt" preference is enabled.
// Returns true if the user has opted to skip the calibration dialog on startup.
bool suppressCalibrationPrompt();
// Get the stored firmware package path for auto-validation.
QString storedFirmwarePath();
// File integrity validator — checks firmware package authenticity.
// Returns true if the file at the given path is a valid firmware package.
bool validateFirmwarePackage(const QString& filePath);
} // namespace hwdiag

Some files were not shown because too many files have changed in this diff Show More