v1.1.0 — SD card recovery, 25 new filesystem types, UI theme overhaul
- SD card recovery tool: detects and repairs cards Windows can't see, including mid-format failures, corrupted partition tables, RAW media. Uses IOCTL_DISK_CREATE_DISK + IOCTL_DISK_SET_DRIVE_LAYOUT_EX directly. - 25 new filesystem types: F2FS, JFFS2, NILFS2, FATX, STFS, GDFX, PS2MC, VHD, VHDX, VMDK, QCOW2, VDI, RVZ, WUA, WBFS, NRG, CDFS, HDFS + more - Detection routines for all new types with correct magic number signatures - UI theme: bright green replaced with pale grayish peach, all text white - hwdiag rebuilt in Release mode; added build_hwdiag_release.bat - garbage.xtx auto-copied to build output directory post-build
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -23,3 +23,8 @@ src/core/security/OratDecoder.*
|
|||||||
third_party/hwdiag/internal/
|
third_party/hwdiag/internal/
|
||||||
third_party/hwdiag/build/
|
third_party/hwdiag/build/
|
||||||
third_party/hwdiag/hwdiag_impl.cpp
|
third_party/hwdiag/hwdiag_impl.cpp
|
||||||
|
SetecPartitionWizard-v1.0.0-debug-x64/
|
||||||
|
third_party/hwdiag/build_release/
|
||||||
|
third_party/hwdiag/build_rel/
|
||||||
|
hwdiag_build.log
|
||||||
|
*.zip
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**A free, open-source disk utility that does everything the paid tools do — without the monthly subscription.**
|
**A free, open-source disk utility that does everything the paid tools do — without the monthly subscription.**
|
||||||
|
|
||||||
Tired of Acronis, EaseUS, and Partition Magic charging you $50/year for basic disk operations? So were we. Setec Partition Wizard is a comprehensive C++17/Qt6 disk utility covering partition management, formatting, recovery, imaging, diagnostics, security, and maintenance — all in one tool, completely free.
|
Tired of Acronis, EaseUS, and Partition Magic charging you $50/year for basic disk operations? So were we. Setec Partition Wizard is a comprehensive C++17/Qt6 disk utility covering partition management, formatting, recovery, imaging, diagnostics, security, maintenance, and SD card recovery — all in one tool, completely free.
|
||||||
|
|
||||||
[](https://ibb.co/GNfswHt)
|
[](https://ibb.co/GNfswHt)
|
||||||
|
|
||||||
@@ -18,13 +18,34 @@ Tired of Acronis, EaseUS, and Partition Magic charging you $50/year for basic di
|
|||||||
- Raw partition table editing for advanced users
|
- Raw partition table editing for advanced users
|
||||||
|
|
||||||
### Formatting — Every Filesystem You Can Think Of
|
### Formatting — Every Filesystem You Can Think Of
|
||||||
- **Modern:** NTFS, FAT32/16/12, exFAT, ReFS
|
|
||||||
- **Linux:** ext2/3/4, Btrfs, XFS, JFS, ReiserFS, Linux swap
|
|
||||||
- **Apple:** HFS+, APFS (read-only detection), HFS Classic
|
|
||||||
- **Legacy & Retro:** HPFS (OS/2), Minix, Amiga Fast File System, BeOS BFS, QNX4, UFS (BSD), Xenix, Coherent, SysV, ADFS (Acorn), UDF, ISO9660
|
|
||||||
- **Special:** RomFS, CramFS, SquashFS, VFAT, UMSDOS
|
|
||||||
|
|
||||||
Yes, we support filesystems from the 1980s. Because why not.
|
**Modern Windows:**
|
||||||
|
NTFS, FAT32/16/12, exFAT, ReFS
|
||||||
|
|
||||||
|
**Linux / Open Source:**
|
||||||
|
ext2/3/4, Btrfs, XFS, ZFS, JFS, ReiserFS, Reiser4, F2FS, JFFS2, NILFS2, Linux swap
|
||||||
|
|
||||||
|
**Apple:**
|
||||||
|
HFS+, APFS (read-only detection), HFS Classic, MFS
|
||||||
|
|
||||||
|
**Legacy & Retro (because why not):**
|
||||||
|
HPFS (OS/2), Minix, Amiga Fast File System (AFFS/OFS), BeOS BFS, QNX4/6, UFS (BSD), FFS, Xenix, Coherent, SysV, VxFS, ADFS (Acorn), UDF, ISO9660, RomFS, CramFS, SquashFS, VFAT, UMSDOS
|
||||||
|
|
||||||
|
**Console & Gaming:**
|
||||||
|
FATX (Xbox / Xbox 360), STFS (Xbox 360 packages), GDFX (Xbox Game Disc), PS2 Memory Card
|
||||||
|
|
||||||
|
**Virtual Disk Images:**
|
||||||
|
VHD, VHDX (Hyper-V), VMDK (VMware), QCOW2 (QEMU), VDI (VirtualBox)
|
||||||
|
|
||||||
|
**Disc & Archive Images:**
|
||||||
|
RVZ/WIA (Dolphin Wii), WUA (Cemu Wii U), WBFS (Wii Backup), NRG (Nero), MDF (Alcohol 120%), CDI (DiscJuggler)
|
||||||
|
|
||||||
|
### SD Card Recovery
|
||||||
|
- **Detects SD/microSD cards Windows cannot see** — finds cards with corrupted partition tables, interrupted formats, and RAW/uninitialized media
|
||||||
|
- Scans by bus type, removable flag, and model keywords — even finds cards with no partition table at all
|
||||||
|
- **Full repair workflow:** clean partition table → reinitialize → format to FAT32/exFAT/NTFS
|
||||||
|
- Auto-selects exFAT for cards over 32 GB
|
||||||
|
- Uses raw IOCTL operations (`IOCTL_DISK_CREATE_DISK`, `IOCTL_DISK_SET_DRIVE_LAYOUT_EX`) to bypass Windows volume manager limitations
|
||||||
|
|
||||||
### Recovery
|
### Recovery
|
||||||
- **Deleted partition recovery** — scans for lost MBR/GPT partition entries
|
- **Deleted partition recovery** — scans for lost MBR/GPT partition entries
|
||||||
@@ -56,6 +77,7 @@ Yes, we support filesystems from the 1980s. Because why not.
|
|||||||
- Gutmann 35-pass method
|
- Gutmann 35-pass method
|
||||||
- Random fill and custom byte patterns
|
- Random fill and custom byte patterns
|
||||||
- **Boot repair** with MBR reconstruction and BCD rebuilding
|
- **Boot repair** with MBR reconstruction and BCD rebuilding
|
||||||
|
- **SD Card Recovery** — repair cards that Windows refuses to recognize
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,8 +101,13 @@ Yes, we support filesystems from the 1980s. Because why not.
|
|||||||
## Building From Source
|
## Building From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Debug build
|
||||||
cmake --preset default
|
cmake --preset default
|
||||||
cmake --build --preset default
|
cmake --build --preset default
|
||||||
|
|
||||||
|
# Release build
|
||||||
|
cmake --preset release
|
||||||
|
cmake --build --preset release
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Requirements
|
### Build Requirements
|
||||||
@@ -106,7 +133,7 @@ src/
|
|||||||
│ ├── recovery/ # Partition recovery, file carving, boot repair
|
│ ├── recovery/ # Partition recovery, file carving, boot repair
|
||||||
│ ├── diagnostics/ # Benchmarks, surface scan
|
│ ├── diagnostics/ # Benchmarks, surface scan
|
||||||
│ ├── imaging/ # Disk cloner, image creator/restorer, ISO flasher
|
│ ├── imaging/ # Disk cloner, image creator/restorer, ISO flasher
|
||||||
│ ├── maintenance/ # Secure erase
|
│ ├── maintenance/ # Secure erase, SD card recovery
|
||||||
│ └── security/ # Encrypted vaults, FIDO2, boot auth
|
│ └── security/ # Encrypted vaults, FIDO2, boot auth
|
||||||
├── ui/ # spw_ui static library (Qt Widgets)
|
├── ui/ # spw_ui static library (Qt Widgets)
|
||||||
│ ├── MainWindow # Tab container with visual disk map
|
│ ├── MainWindow # Tab container with visual disk map
|
||||||
@@ -122,6 +149,7 @@ src/
|
|||||||
- **Operation queue** — changes are queued, previewed, then applied atomically
|
- **Operation queue** — changes are queued, previewed, then applied atomically
|
||||||
- **Removable-only safety** — ISO flasher refuses to write to fixed disks
|
- **Removable-only safety** — ISO flasher refuses to write to fixed disks
|
||||||
- **Admin required** — raw disk I/O requires elevation; app checks and prompts
|
- **Admin required** — raw disk I/O requires elevation; app checks and prompts
|
||||||
|
- **SD card recovery uses raw IOCTLs** — bypasses volume manager to reach cards Windows won't mount
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -129,7 +157,9 @@ src/
|
|||||||
|
|
||||||
> *"Don't forget to look UP UP at space."*
|
> *"Don't forget to look UP UP at space."*
|
||||||
|
|
||||||
Some say if you press **F5** while the application is running, something unexpected happens. Something involving a riddle about nothing, a dark void, and a very particular file that only your build can produce.
|
Press **F5** while the application is running. Something unexpected happens.
|
||||||
|
|
||||||
|
A riddle. A dark void. And a very particular file that ships with every build — one that looks like garbage in a hex editor, but says something in a text editor. Find it. Read it. Then find the file that only *your* build can produce.
|
||||||
|
|
||||||
Those who grew up cleaning floors on the SCS Deepship 86 might feel right at home. The janitor always did have a knack for finding hidden things.
|
Those who grew up cleaning floors on the SCS Deepship 86 might feel right at home. The janitor always did have a knack for finding hidden things.
|
||||||
|
|
||||||
|
|||||||
60
build_hwdiag_release.bat
Normal file
60
build_hwdiag_release.bat
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableDelayedExpansion
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
set "PATH=%VCDIR%\bin\Hostx64\x64;%SDKDIR%\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"
|
||||||
|
set "INCLUDE=%VCDIR%\include;%SDKDIR%\Include\%WINSDK_VER%\ucrt;%SDKDIR%\Include\%WINSDK_VER%\um;%SDKDIR%\Include\%WINSDK_VER%\shared;%SDKDIR%\Include\%WINSDK_VER%\winrt"
|
||||||
|
set "LIB=%VCDIR%\lib\x64;%SDKDIR%\Lib\%WINSDK_VER%\ucrt\x64;%SDKDIR%\Lib\%WINSDK_VER%\um\x64"
|
||||||
|
|
||||||
|
set "HWDIAG_DIR=%~dp0third_party\hwdiag"
|
||||||
|
set "INTERNAL=%HWDIAG_DIR%\internal"
|
||||||
|
set "SRC_ROOT=%~dp0src"
|
||||||
|
|
||||||
|
echo === Copying internal sources ===
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\AstroChicken.h" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\AstroChicken.cpp" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\Vohaul.h" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\Vohaul.cpp" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\Arnoid.h" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\dialogs\Arnoid.cpp" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\tabs\StarGenerator.h" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\ui\tabs\StarGenerator.cpp" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\core\security\OratDecoder.h" "%INTERNAL%\" >nul 2>&1
|
||||||
|
copy /Y "%SRC_ROOT%\core\security\OratDecoder.cpp" "%INTERNAL%\" >nul 2>&1
|
||||||
|
|
||||||
|
echo === Configuring hwdiag (Release) ===
|
||||||
|
cmake -B "%HWDIAG_DIR%\build_rel" -S "%HWDIAG_DIR%" ^
|
||||||
|
-G Ninja ^
|
||||||
|
-DCMAKE_BUILD_TYPE=Release ^
|
||||||
|
-DCMAKE_PREFIX_PATH=C:\Qt\6.10.0\msvc2022_64 ^
|
||||||
|
-DCMAKE_MAKE_PROGRAM=C:\Qt\Tools\Ninja\ninja.exe
|
||||||
|
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo CONFIGURE FAILED
|
||||||
|
goto cleanup
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === Building hwdiag (Release) ===
|
||||||
|
cmake --build "%HWDIAG_DIR%\build_rel"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo BUILD FAILED
|
||||||
|
goto cleanup
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === hwdiag Release library built successfully ===
|
||||||
|
echo Output: %HWDIAG_DIR%\lib\spw_hwdiag.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 "%HWDIAG_DIR%\build_rel" >nul 2>&1
|
||||||
|
|
||||||
|
echo === Done ===
|
||||||
|
endlocal
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
QMainWindow {
|
QMainWindow {
|
||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTabWidget::pane {
|
QTabWidget::pane {
|
||||||
@@ -12,7 +12,7 @@ QTabWidget::pane {
|
|||||||
|
|
||||||
QTabBar::tab {
|
QTabBar::tab {
|
||||||
background-color: #313244;
|
background-color: #313244;
|
||||||
color: #bac2de;
|
color: #ffffff;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
@@ -22,8 +22,8 @@ QTabBar::tab {
|
|||||||
|
|
||||||
QTabBar::tab:selected {
|
QTabBar::tab:selected {
|
||||||
background-color: #45475a;
|
background-color: #45475a;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border-bottom: 2px solid #89b4fa;
|
border-bottom: 2px solid #d4a574;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTabBar::tab:hover:!selected {
|
QTabBar::tab:hover:!selected {
|
||||||
@@ -32,10 +32,10 @@ QTabBar::tab:hover:!selected {
|
|||||||
|
|
||||||
QTreeView, QTableView, QListView {
|
QTreeView, QTableView, QListView {
|
||||||
background-color: #181825;
|
background-color: #181825;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
selection-background-color: #45475a;
|
selection-background-color: #45475a;
|
||||||
selection-color: #cdd6f4;
|
selection-color: #ffffff;
|
||||||
alternate-background-color: #1e1e2e;
|
alternate-background-color: #1e1e2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +45,14 @@ QTreeView::item:hover, QTableView::item:hover {
|
|||||||
|
|
||||||
QHeaderView::section {
|
QHeaderView::section {
|
||||||
background-color: #313244;
|
background-color: #313244;
|
||||||
color: #bac2de;
|
color: #ffffff;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #45475a;
|
background-color: #45475a;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border: 1px solid #585b70;
|
border: 1px solid #585b70;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
@@ -68,13 +68,13 @@ QPushButton:pressed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#applyButton {
|
QPushButton#applyButton {
|
||||||
background-color: #a6e3a1;
|
background-color: #d4a574;
|
||||||
color: #1e1e2e;
|
color: #1e1e2e;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#applyButton:hover {
|
QPushButton#applyButton:hover {
|
||||||
background-color: #94e2d5;
|
background-color: #c49060;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#cancelButton {
|
QPushButton#cancelButton {
|
||||||
@@ -87,17 +87,17 @@ QProgressBar {
|
|||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
QProgressBar::chunk {
|
QProgressBar::chunk {
|
||||||
background-color: #89b4fa;
|
background-color: #d4a574;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenuBar {
|
QMenuBar {
|
||||||
background-color: #181825;
|
background-color: #181825;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMenuBar::item:selected {
|
QMenuBar::item:selected {
|
||||||
@@ -106,7 +106,7 @@ QMenuBar::item:selected {
|
|||||||
|
|
||||||
QMenu {
|
QMenu {
|
||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ QToolBar {
|
|||||||
|
|
||||||
QStatusBar {
|
QStatusBar {
|
||||||
background-color: #181825;
|
background-color: #181825;
|
||||||
color: #a6adc8;
|
color: #ffffff;
|
||||||
border-top: 1px solid #45475a;
|
border-top: 1px solid #45475a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ QGroupBox {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
QGroupBox::title {
|
QGroupBox::title {
|
||||||
@@ -147,19 +147,63 @@ QGroupBox::title {
|
|||||||
|
|
||||||
QLineEdit, QSpinBox, QComboBox {
|
QLineEdit, QSpinBox, QComboBox {
|
||||||
background-color: #313244;
|
background-color: #313244;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QLineEdit:focus, QSpinBox:focus, QComboBox:focus {
|
QLineEdit:focus, QSpinBox:focus, QComboBox:focus {
|
||||||
border-color: #89b4fa;
|
border-color: #d4a574;
|
||||||
}
|
}
|
||||||
|
|
||||||
QToolTip {
|
QToolTip {
|
||||||
background-color: #313244;
|
background-color: #313244;
|
||||||
color: #cdd6f4;
|
color: #ffffff;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QLabel {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCheckBox, QRadioButton {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextEdit, QPlainTextEdit {
|
||||||
|
background-color: #181825;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #45475a;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background-color: #181825;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background-color: #45475a;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:vertical:hover {
|
||||||
|
background-color: #585b70;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar:horizontal {
|
||||||
|
background-color: #181825;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal {
|
||||||
|
background-color: #45475a;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScrollBar::handle:horizontal:hover {
|
||||||
|
background-color: #585b70;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ target_link_libraries(SetecPartitionWizard PRIVATE
|
|||||||
spw_ui
|
spw_ui
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Copy garbage.xtx to build output directory
|
||||||
|
add_custom_command(TARGET SetecPartitionWizard POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_SOURCE_DIR}/resources/garbage.xtx"
|
||||||
|
"$<TARGET_FILE_DIR:SetecPartitionWizard>/garbage.xtx"
|
||||||
|
COMMENT "Copying garbage.xtx to build directory..."
|
||||||
|
)
|
||||||
|
|
||||||
# Deploy Qt DLLs on install
|
# Deploy Qt DLLs on install
|
||||||
include(${CMAKE_SOURCE_DIR}/cmake/QtDeployHelper.cmake)
|
include(${CMAKE_SOURCE_DIR}/cmake/QtDeployHelper.cmake)
|
||||||
spw_deploy_qt(SetecPartitionWizard)
|
spw_deploy_qt(SetecPartitionWizard)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ set(CORE_SOURCES
|
|||||||
|
|
||||||
# Maintenance
|
# Maintenance
|
||||||
maintenance/SecureErase.cpp
|
maintenance/SecureErase.cpp
|
||||||
|
maintenance/SdCardRecovery.cpp
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
security/EncryptedVault.cpp
|
security/EncryptedVault.cpp
|
||||||
@@ -76,6 +77,7 @@ set(CORE_HEADERS
|
|||||||
imaging/ImageRestorer.h
|
imaging/ImageRestorer.h
|
||||||
imaging/IsoFlasher.h
|
imaging/IsoFlasher.h
|
||||||
maintenance/SecureErase.h
|
maintenance/SecureErase.h
|
||||||
|
maintenance/SdCardRecovery.h
|
||||||
security/EncryptedVault.h
|
security/EncryptedVault.h
|
||||||
security/Fido2Manager.h
|
security/Fido2Manager.h
|
||||||
security/BootAuthenticator.h
|
security/BootAuthenticator.h
|
||||||
|
|||||||
@@ -94,9 +94,39 @@ enum class FilesystemType
|
|||||||
VFAT, // Virtual FAT (long filename extension)
|
VFAT, // Virtual FAT (long filename extension)
|
||||||
UMSDOS, // Unix on MS-DOS filesystem
|
UMSDOS, // Unix on MS-DOS filesystem
|
||||||
|
|
||||||
|
// Flash-optimized
|
||||||
|
F2FS, // Flash-Friendly File System (Samsung)
|
||||||
|
JFFS2, // Journalling Flash File System v2
|
||||||
|
NILFS2, // New Implementation of a Log-structured File System
|
||||||
|
|
||||||
|
// Console / gaming
|
||||||
|
FATX, // Xbox / Xbox 360 filesystem
|
||||||
|
STFS, // Xbox 360 Secure Transacted File System
|
||||||
|
GDFX, // Xbox Game Disc Format (XDVDFS)
|
||||||
|
PS2MC, // PS2 Memory Card filesystem
|
||||||
|
|
||||||
|
// Virtual disk images
|
||||||
|
VHD, // Microsoft Virtual Hard Disk
|
||||||
|
VHDX, // Microsoft Hyper-V Virtual Hard Disk
|
||||||
|
VMDK, // VMware Virtual Machine Disk
|
||||||
|
QCOW2, // QEMU Copy-On-Write v2
|
||||||
|
VDI, // VirtualBox Disk Image
|
||||||
|
|
||||||
|
// Disc images / archives
|
||||||
|
RVZ, // Dolphin Wii disc image
|
||||||
|
WUA, // Cemu Wii U archive (ZArchive)
|
||||||
|
WBFs, // Wii Backup File System
|
||||||
|
NRG, // Nero disc image
|
||||||
|
MDF, // Alcohol 120% disc image
|
||||||
|
CDI, // DiscJuggler disc image
|
||||||
|
|
||||||
|
// Optical media
|
||||||
|
CDFS, // CD-ROM File System (MSCDEX)
|
||||||
|
|
||||||
// Network / special (read-only detection)
|
// Network / special (read-only detection)
|
||||||
NFS,
|
NFS,
|
||||||
SMB,
|
SMB,
|
||||||
|
HDFS, // Hadoop Distributed File System (marker only)
|
||||||
SWAP_LINUX,
|
SWAP_LINUX,
|
||||||
SWAP_SOLARIS,
|
SWAP_SOLARIS,
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,29 @@ Result<FilesystemDetection> FilesystemDetector::detect(
|
|||||||
if (detectQnx4(readFunc, detection)) return detection;
|
if (detectQnx4(readFunc, detection)) return detection;
|
||||||
if (detectLinuxSwap(readFunc, detection, volumeSize)) return detection;
|
if (detectLinuxSwap(readFunc, detection, volumeSize)) return detection;
|
||||||
|
|
||||||
|
// Flash-optimized filesystems
|
||||||
|
if (detectF2fs(readFunc, detection)) return detection;
|
||||||
|
if (detectJffs2(readFunc, detection)) return detection;
|
||||||
|
if (detectNilfs2(readFunc, detection)) return detection;
|
||||||
|
|
||||||
|
// Console / gaming filesystems
|
||||||
|
if (detectFatx(readFunc, detection)) return detection;
|
||||||
|
if (detectStfs(readFunc, detection)) return detection;
|
||||||
|
if (detectGdfx(readFunc, detection)) return detection;
|
||||||
|
if (detectPs2mc(readFunc, detection)) return detection;
|
||||||
|
|
||||||
|
// Virtual disk images
|
||||||
|
if (detectVhdx(readFunc, detection)) return detection;
|
||||||
|
if (detectVmdk(readFunc, detection)) return detection;
|
||||||
|
if (detectQcow2(readFunc, detection)) return detection;
|
||||||
|
if (detectVdi(readFunc, detection)) return detection;
|
||||||
|
if (detectVhd(readFunc, detection, volumeSize)) return detection;
|
||||||
|
|
||||||
|
// Disc images
|
||||||
|
if (detectRvz(readFunc, detection)) return detection;
|
||||||
|
if (detectNrg(readFunc, detection, volumeSize)) return detection;
|
||||||
|
if (detectWbfs(readFunc, detection)) return detection;
|
||||||
|
|
||||||
// FAT last because its detection is the most heuristic-dependent
|
// FAT last because its detection is the most heuristic-dependent
|
||||||
if (detectFat(readFunc, detection)) return detection;
|
if (detectFat(readFunc, detection)) return detection;
|
||||||
|
|
||||||
@@ -1208,10 +1231,483 @@ const char* FilesystemDetector::filesystemName(FilesystemType type)
|
|||||||
case FilesystemType::SMB: return "SMB";
|
case FilesystemType::SMB: return "SMB";
|
||||||
case FilesystemType::SWAP_LINUX: return "Linux Swap";
|
case FilesystemType::SWAP_LINUX: return "Linux Swap";
|
||||||
case FilesystemType::SWAP_SOLARIS: return "Solaris Swap";
|
case FilesystemType::SWAP_SOLARIS: return "Solaris Swap";
|
||||||
|
case FilesystemType::F2FS: return "F2FS";
|
||||||
|
case FilesystemType::JFFS2: return "JFFS2";
|
||||||
|
case FilesystemType::NILFS2: return "NILFS2";
|
||||||
|
case FilesystemType::FATX: return "FATX (Xbox)";
|
||||||
|
case FilesystemType::STFS: return "STFS (Xbox 360)";
|
||||||
|
case FilesystemType::GDFX: return "GDFX (Xbox Disc)";
|
||||||
|
case FilesystemType::PS2MC: return "PS2 Memory Card";
|
||||||
|
case FilesystemType::VHD: return "VHD";
|
||||||
|
case FilesystemType::VHDX: return "VHDX";
|
||||||
|
case FilesystemType::VMDK: return "VMDK";
|
||||||
|
case FilesystemType::QCOW2: return "QCOW2";
|
||||||
|
case FilesystemType::VDI: return "VDI";
|
||||||
|
case FilesystemType::RVZ: return "RVZ (Wii)";
|
||||||
|
case FilesystemType::WUA: return "WUA (Wii U)";
|
||||||
|
case FilesystemType::WBFs: return "WBFS (Wii)";
|
||||||
|
case FilesystemType::NRG: return "NRG (Nero)";
|
||||||
|
case FilesystemType::MDF: return "MDF (Alcohol)";
|
||||||
|
case FilesystemType::CDI: return "CDI (DiscJuggler)";
|
||||||
|
case FilesystemType::CDFS: return "CDFS";
|
||||||
|
case FilesystemType::HDFS: return "HDFS";
|
||||||
case FilesystemType::Raw: return "Raw";
|
case FilesystemType::Raw: return "Raw";
|
||||||
case FilesystemType::Unallocated: return "Unallocated";
|
case FilesystemType::Unallocated: return "Unallocated";
|
||||||
}
|
}
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// F2FS detection
|
||||||
|
// Magic: 0xF2F52010 at offset 0x400 (1024)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectF2fs(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0x400, 128);
|
||||||
|
if (data.size() < 128) return false;
|
||||||
|
|
||||||
|
// F2FS magic at offset 0 of superblock (which is at partition offset 0x400)
|
||||||
|
uint32_t magic = readLE32(data.data());
|
||||||
|
if (magic != 0xF2F52010)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::F2FS;
|
||||||
|
out.description = "F2FS (Flash-Friendly File System)";
|
||||||
|
|
||||||
|
// Major/minor version at offset 4 and 6
|
||||||
|
uint16_t majorVer = readLE16(data.data() + 4);
|
||||||
|
uint16_t minorVer = readLE16(data.data() + 6);
|
||||||
|
(void)majorVer; (void)minorVer;
|
||||||
|
|
||||||
|
// log_blocksize at offset 38 (usually 12 = 4096 bytes)
|
||||||
|
uint32_t logBlocksize = readLE32(data.data() + 38);
|
||||||
|
if (logBlocksize >= 10 && logBlocksize <= 16)
|
||||||
|
out.blockSize = 1u << logBlocksize;
|
||||||
|
|
||||||
|
// Volume name at offset 0x6A0 - 0x400 = 0x2A0 from start of superblock, Unicode
|
||||||
|
// (need to read more data for that)
|
||||||
|
auto labelData = safeRead(readFunc, 0x400 + 0x2A0, 512);
|
||||||
|
if (labelData.size() >= 64)
|
||||||
|
{
|
||||||
|
std::string label;
|
||||||
|
for (size_t i = 0; i < 64; i += 2)
|
||||||
|
{
|
||||||
|
uint16_t ch = readLE16(labelData.data() + i);
|
||||||
|
if (ch == 0) break;
|
||||||
|
if (ch < 128) label += static_cast<char>(ch);
|
||||||
|
}
|
||||||
|
if (!label.empty())
|
||||||
|
out.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID at offset 0x460 - 0x400 = 0x60 from superblock start
|
||||||
|
if (data.size() >= 0x70)
|
||||||
|
{
|
||||||
|
char uuid[48];
|
||||||
|
const uint8_t* u = data.data() + 0x60;
|
||||||
|
snprintf(uuid, sizeof(uuid),
|
||||||
|
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||||
|
u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
|
||||||
|
u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]);
|
||||||
|
out.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// JFFS2 detection
|
||||||
|
// Magic: 0x1985 (little-endian) or 0x8519 (big-endian) at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectJffs2(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 12);
|
||||||
|
if (data.size() < 12) return false;
|
||||||
|
|
||||||
|
uint16_t magic = readLE16(data.data());
|
||||||
|
if (magic != 0x1985 && magic != 0x8519)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Validate node type (bits 0-15 of nodetype at offset 2)
|
||||||
|
uint16_t nodetype = readLE16(data.data() + 2);
|
||||||
|
// Strip high compatibility bits
|
||||||
|
uint16_t nodeBase = nodetype & 0x00FF;
|
||||||
|
// Valid JFFS2 node types: 1=DIRENT, 2=INODE, 3=CLEAN, 6=PADDING, 0xE0=SUMMARY
|
||||||
|
if (nodeBase != 0x01 && nodeBase != 0x02 && nodeBase != 0x03 &&
|
||||||
|
nodeBase != 0x06 && nodeBase != 0xE0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::JFFS2;
|
||||||
|
out.description = "JFFS2 (Journalling Flash File System v2)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NILFS2 detection
|
||||||
|
// Magic: 0x3434 at offset 0x406 (superblock at 0x400, magic at +6)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectNilfs2(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0x400, 128);
|
||||||
|
if (data.size() < 128) return false;
|
||||||
|
|
||||||
|
// NILFS2 magic at offset 6 in the superblock
|
||||||
|
uint16_t magic = readLE16(data.data() + 6);
|
||||||
|
if (magic != 0x3434)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::NILFS2;
|
||||||
|
out.description = "NILFS2";
|
||||||
|
|
||||||
|
// Block size: stored as log2 at offset 0x0E
|
||||||
|
uint32_t logBlock = readLE32(data.data() + 0x0E);
|
||||||
|
if (logBlock >= 10 && logBlock <= 16)
|
||||||
|
out.blockSize = 1u << logBlock;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FATX detection (Xbox / Xbox 360)
|
||||||
|
// Magic: "FATX" (0x58544146 LE or 0x46415458 BE) at partition start
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectFatx(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 0x200);
|
||||||
|
if (data.size() < 0x200) return false;
|
||||||
|
|
||||||
|
// Check FATX magic (bytes "FATX" at offset 0)
|
||||||
|
if (!memEqual(data.data(), "FATX", 4))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Validate SectorsPerCluster (must be power of 2, max 0x80)
|
||||||
|
uint32_t spc = readLE32(data.data() + 0x08);
|
||||||
|
if (!isPowerOf2(spc) || spc > 0x80)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::FATX;
|
||||||
|
out.description = "FATX (Xbox)";
|
||||||
|
out.blockSize = spc * 512;
|
||||||
|
|
||||||
|
// Volume name at offset 0x10 (up to 32 Unicode chars)
|
||||||
|
std::string label;
|
||||||
|
for (int i = 0; i < 32; ++i)
|
||||||
|
{
|
||||||
|
uint16_t ch = readLE16(data.data() + 0x10 + i * 2);
|
||||||
|
if (ch == 0 || ch == 0xFFFF) break;
|
||||||
|
if (ch < 128) label += static_cast<char>(ch);
|
||||||
|
}
|
||||||
|
if (!label.empty())
|
||||||
|
out.label = label;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STFS detection (Xbox 360 content packages)
|
||||||
|
// Magic: "CON " (0x434F4E20), "LIVE" (0x4C495645), "PIRS" (0x50495253) at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectStfs(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 8);
|
||||||
|
if (data.size() < 4) return false;
|
||||||
|
|
||||||
|
uint32_t magic = readBE32(data.data());
|
||||||
|
if (magic != 0x434F4E20 && // "CON "
|
||||||
|
magic != 0x4C495645 && // "LIVE"
|
||||||
|
magic != 0x50495253) // "PIRS"
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::STFS;
|
||||||
|
out.description = "STFS (Xbox 360 Package)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GDFX detection (Xbox Game Disc Format)
|
||||||
|
// Magic: "MICROSOFT*XBOX*MEDIA" at various sector offsets
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectGdfx(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
static const char kGdfxMagic[] = "MICROSOFT*XBOX*MEDIA";
|
||||||
|
constexpr size_t kMagicLen = 20;
|
||||||
|
|
||||||
|
// Check all known GDFX offsets
|
||||||
|
static const uint64_t offsets[] = {
|
||||||
|
0x10000, // Raw XGD (SDK)
|
||||||
|
0x18310000, // XGD1 (original Xbox)
|
||||||
|
0xFDA0000, // XGD2
|
||||||
|
0x2090000, // XGD3
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto off : offsets)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, off, 32);
|
||||||
|
if (data.size() >= kMagicLen && memEqual(data.data(), kGdfxMagic, kMagicLen))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::GDFX;
|
||||||
|
out.description = "GDFX (Xbox Game Disc)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PS2 Memory Card detection
|
||||||
|
// Magic: "Sony PS2 Memory Card Format " at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectPs2mc(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 128);
|
||||||
|
if (data.size() < 40) return false;
|
||||||
|
|
||||||
|
// Check the first 28 bytes of the magic string
|
||||||
|
if (!memEqual(data.data(), "Sony PS2 Memory Card Format ", 28))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::PS2MC;
|
||||||
|
out.description = "PS2 Memory Card";
|
||||||
|
|
||||||
|
// Page size at offset 0x28 (uint16)
|
||||||
|
if (data.size() >= 0x2C)
|
||||||
|
{
|
||||||
|
uint16_t pageSize = readLE16(data.data() + 0x28);
|
||||||
|
uint16_t pagesPerCluster = readLE16(data.data() + 0x2A);
|
||||||
|
if (pageSize > 0 && pagesPerCluster > 0)
|
||||||
|
out.blockSize = pageSize * pagesPerCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VHD detection (Microsoft Virtual Hard Disk)
|
||||||
|
// Footer magic: "conectix" at last 512 bytes of file, or at offset 0 for fixed VHD
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectVhd(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize)
|
||||||
|
{
|
||||||
|
// VHD footer can be at offset 0 (dynamic/differencing) as a copy
|
||||||
|
auto data = safeRead(readFunc, 0, 512);
|
||||||
|
if (data.size() < 512) return false;
|
||||||
|
|
||||||
|
if (memEqual(data.data(), "conectix", 8))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::VHD;
|
||||||
|
out.description = "VHD (Virtual Hard Disk)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For fixed VHD, footer is at the very end — check if volumeSize is known
|
||||||
|
if (volumeSize >= 512)
|
||||||
|
{
|
||||||
|
auto footer = safeRead(readFunc, volumeSize - 512, 512);
|
||||||
|
if (footer.size() >= 8 && memEqual(footer.data(), "conectix", 8))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::VHD;
|
||||||
|
out.description = "VHD (Virtual Hard Disk)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VHDX detection
|
||||||
|
// Magic: "vhdxfile" at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectVhdx(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 16);
|
||||||
|
if (data.size() < 8) return false;
|
||||||
|
|
||||||
|
if (memEqual(data.data(), "vhdxfile", 8))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::VHDX;
|
||||||
|
out.description = "VHDX (Hyper-V Virtual Hard Disk)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VMDK detection
|
||||||
|
// Magic: "KDMV" (0x564D444B LE) for sparse extent, or "# Disk DescriptorFile" text
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectVmdk(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 64);
|
||||||
|
if (data.size() < 4) return false;
|
||||||
|
|
||||||
|
// Sparse VMDK: magic "KDMV" at offset 0
|
||||||
|
uint32_t magic = readLE32(data.data());
|
||||||
|
if (magic == 0x564D444B) // "KDMV" LE
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::VMDK;
|
||||||
|
out.description = "VMDK (VMware Virtual Disk)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text descriptor VMDK
|
||||||
|
if (data.size() >= 21 && memEqual(data.data(), "# Disk DescriptorFile", 21))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::VMDK;
|
||||||
|
out.description = "VMDK (VMware Virtual Disk)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// QCOW2 detection
|
||||||
|
// Magic: "QFI\xFB" (0x514649FB BE) at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectQcow2(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 16);
|
||||||
|
if (data.size() < 8) return false;
|
||||||
|
|
||||||
|
uint32_t magic = readBE32(data.data());
|
||||||
|
if (magic != 0x514649FB)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32_t version = readBE32(data.data() + 4);
|
||||||
|
if (version != 2 && version != 3)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::QCOW2;
|
||||||
|
out.description = (version == 3) ? "QCOW2 v3 (QEMU)" : "QCOW2 (QEMU)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VDI detection (VirtualBox)
|
||||||
|
// Magic: 0xBEDA107F at offset 0x40
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectVdi(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 0x50);
|
||||||
|
if (data.size() < 0x48) return false;
|
||||||
|
|
||||||
|
// VDI signature at offset 0x40
|
||||||
|
uint32_t magic = readLE32(data.data() + 0x40);
|
||||||
|
if (magic != 0xBEDA107F)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
out.type = FilesystemType::VDI;
|
||||||
|
out.description = "VDI (VirtualBox Disk Image)";
|
||||||
|
|
||||||
|
// Check for "<<< " image creation marker at offset 0
|
||||||
|
if (memEqual(data.data(), "\x7F\x10\xDA\xBE", 4) ||
|
||||||
|
memEqual(data.data() + 0x40, "\x7F\x10\xDA\xBE", 4))
|
||||||
|
{
|
||||||
|
// Already matched
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RVZ detection (Dolphin Wii disc image)
|
||||||
|
// Magic: 0x015A5652 LE ("RVZ\x01") at offset 0
|
||||||
|
// Also WIA: 0x01414957 LE ("WIA\x01")
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectRvz(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 8);
|
||||||
|
if (data.size() < 4) return false;
|
||||||
|
|
||||||
|
uint32_t magic = readLE32(data.data());
|
||||||
|
if (magic == 0x015A5652) // "RVZ\x01"
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::RVZ;
|
||||||
|
out.description = "RVZ (Dolphin Wii Disc Image)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIA format (predecessor to RVZ, same family)
|
||||||
|
if (magic == 0x01414957) // "WIA\x01"
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::RVZ;
|
||||||
|
out.description = "WIA (Dolphin Wii Disc Image)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NRG detection (Nero disc image)
|
||||||
|
// Magic: "NER5" or "NERO" at end of file (footer-based)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectNrg(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize)
|
||||||
|
{
|
||||||
|
if (volumeSize < 12)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// NRG v2: "NER5" at (filesize - 12)
|
||||||
|
auto footer = safeRead(readFunc, volumeSize - 12, 12);
|
||||||
|
if (footer.size() >= 4)
|
||||||
|
{
|
||||||
|
if (memEqual(footer.data(), "NER5", 4))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::NRG;
|
||||||
|
out.description = "NRG v2 (Nero Disc Image)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NRG v1: "NERO" at (filesize - 8)
|
||||||
|
footer = safeRead(readFunc, volumeSize - 8, 8);
|
||||||
|
if (footer.size() >= 4)
|
||||||
|
{
|
||||||
|
if (memEqual(footer.data(), "NERO", 4))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::NRG;
|
||||||
|
out.description = "NRG v1 (Nero Disc Image)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WBFS detection (Wii Backup File System)
|
||||||
|
// Magic: "WBFS" at offset 0
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool FilesystemDetector::detectWbfs(const DiskReadCallback& readFunc, FilesystemDetection& out)
|
||||||
|
{
|
||||||
|
auto data = safeRead(readFunc, 0, 16);
|
||||||
|
if (data.size() < 4) return false;
|
||||||
|
|
||||||
|
if (memEqual(data.data(), "WBFS", 4))
|
||||||
|
{
|
||||||
|
out.type = FilesystemType::WBFs;
|
||||||
|
out.description = "WBFS (Wii Backup File System)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace spw
|
} // namespace spw
|
||||||
|
|||||||
@@ -85,6 +85,29 @@ private:
|
|||||||
static bool detectRomFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
static bool detectRomFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
static bool detectLinuxSwap(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize);
|
static bool detectLinuxSwap(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize);
|
||||||
|
|
||||||
|
// Flash-optimized
|
||||||
|
static bool detectF2fs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectJffs2(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectNilfs2(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
|
||||||
|
// Console / gaming
|
||||||
|
static bool detectFatx(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectStfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectGdfx(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectPs2mc(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
|
||||||
|
// Virtual disk images
|
||||||
|
static bool detectVhd(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize);
|
||||||
|
static bool detectVhdx(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectVmdk(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectQcow2(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectVdi(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
|
||||||
|
// Disc images
|
||||||
|
static bool detectRvz(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
static bool detectNrg(const DiskReadCallback& readFunc, FilesystemDetection& out, uint64_t volumeSize);
|
||||||
|
static bool detectWbfs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||||
|
|
||||||
// Helper: safely read bytes through the callback, returning empty vector on failure
|
// 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);
|
static std::vector<uint8_t> safeRead(const DiskReadCallback& readFunc, uint64_t offset, uint32_t size);
|
||||||
};
|
};
|
||||||
|
|||||||
500
src/core/maintenance/SdCardRecovery.cpp
Normal file
500
src/core/maintenance/SdCardRecovery.cpp
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
#include "SdCardRecovery.h"
|
||||||
|
|
||||||
|
#include "../disk/RawDiskHandle.h"
|
||||||
|
#include "../disk/DiskEnumerator.h"
|
||||||
|
#include "../common/Logging.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winioctl.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cwctype>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace spw
|
||||||
|
{
|
||||||
|
|
||||||
|
bool SdCardRecovery::looksLikeSdCard(const DiskInfo& disk)
|
||||||
|
{
|
||||||
|
// SD/MMC bus type
|
||||||
|
if (disk.interfaceType == DiskInterfaceType::MMC)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Removable + small size (up to 2TB covers SDXC)
|
||||||
|
if (disk.isRemovable && disk.sizeBytes > 0 && disk.sizeBytes <= 2199023255552ULL)
|
||||||
|
{
|
||||||
|
// Check model string for SD/MMC keywords
|
||||||
|
std::wstring modelLower = disk.model;
|
||||||
|
std::transform(modelLower.begin(), modelLower.end(), modelLower.begin(),
|
||||||
|
[](wchar_t c) { return static_cast<wchar_t>(std::towlower(c)); });
|
||||||
|
if (modelLower.find(L"sd") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"mmc") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"sdhc") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"sdxc") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"micro") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"card") != std::wstring::npos ||
|
||||||
|
modelLower.find(L"reader") != std::wstring::npos)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also match USB removable devices under ~256GB (likely USB card readers)
|
||||||
|
if (disk.interfaceType == DiskInterfaceType::USB)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SdCardInfo> SdCardRecovery::analyzeDisk(DiskId diskId)
|
||||||
|
{
|
||||||
|
SdCardInfo info;
|
||||||
|
info.diskId = diskId;
|
||||||
|
|
||||||
|
// Get disk info from enumerator
|
||||||
|
auto diskInfoResult = DiskEnumerator::getDiskInfo(diskId);
|
||||||
|
if (diskInfoResult.isError())
|
||||||
|
return diskInfoResult.error();
|
||||||
|
|
||||||
|
const auto& di = diskInfoResult.value();
|
||||||
|
info.model = di.model;
|
||||||
|
info.serialNumber = di.serialNumber;
|
||||||
|
info.sizeBytes = di.sizeBytes;
|
||||||
|
info.sectorSize = di.sectorSize;
|
||||||
|
info.interfaceType = di.interfaceType;
|
||||||
|
|
||||||
|
if (info.sizeBytes == 0)
|
||||||
|
{
|
||||||
|
info.status = SdCardStatus::NoMedia;
|
||||||
|
info.statusDescription = L"Card reader detected but no media inserted";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to open the disk and read partition table
|
||||||
|
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadOnly);
|
||||||
|
if (diskResult.isError())
|
||||||
|
{
|
||||||
|
info.status = SdCardStatus::Unknown;
|
||||||
|
info.statusDescription = L"Cannot open disk for analysis";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& disk = diskResult.value();
|
||||||
|
|
||||||
|
// Try to get drive layout
|
||||||
|
auto layoutResult = disk.getDriveLayout();
|
||||||
|
if (layoutResult.isError())
|
||||||
|
{
|
||||||
|
// No valid partition table at all
|
||||||
|
info.status = SdCardStatus::NoPartitionTable;
|
||||||
|
info.statusDescription = L"No valid partition table found (MBR or GPT)";
|
||||||
|
info.hasPartitions = false;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& layout = layoutResult.value();
|
||||||
|
|
||||||
|
// Check for valid partitions
|
||||||
|
int validPartitions = 0;
|
||||||
|
for (const auto& part : layout.partitions)
|
||||||
|
{
|
||||||
|
if (part.partitionLength > 0 && part.partitionNumber > 0)
|
||||||
|
++validPartitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validPartitions == 0)
|
||||||
|
{
|
||||||
|
info.status = SdCardStatus::CorruptPartition;
|
||||||
|
info.statusDescription = L"Partition table exists but contains no valid entries";
|
||||||
|
info.hasPartitions = false;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.hasPartitions = true;
|
||||||
|
|
||||||
|
// Check if any partition has a drive letter (visible to Windows)
|
||||||
|
auto snapshotResult = DiskEnumerator::getSystemSnapshot();
|
||||||
|
if (snapshotResult.isOk())
|
||||||
|
{
|
||||||
|
for (const auto& part : snapshotResult.value().partitions)
|
||||||
|
{
|
||||||
|
if (part.diskId == diskId && part.driveLetter != L'\0')
|
||||||
|
{
|
||||||
|
info.hasDriveLetter = true;
|
||||||
|
info.driveLetter = part.driveLetter;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if filesystem is recognized
|
||||||
|
bool hasRecognizedFs = false;
|
||||||
|
for (const auto& part : snapshotResult.value().partitions)
|
||||||
|
{
|
||||||
|
if (part.diskId == diskId &&
|
||||||
|
part.filesystemType != FilesystemType::Unknown &&
|
||||||
|
part.filesystemType != FilesystemType::Unallocated)
|
||||||
|
{
|
||||||
|
hasRecognizedFs = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRecognizedFs)
|
||||||
|
{
|
||||||
|
info.status = SdCardStatus::RawFilesystem;
|
||||||
|
info.statusDescription = L"Partition exists but filesystem is RAW or unrecognized";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.status = SdCardStatus::Healthy;
|
||||||
|
info.statusDescription = L"Card is healthy";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<SdCardInfo>> SdCardRecovery::detectSdCards()
|
||||||
|
{
|
||||||
|
std::vector<SdCardInfo> cards;
|
||||||
|
|
||||||
|
auto disksResult = DiskEnumerator::enumerateDisks();
|
||||||
|
if (disksResult.isError())
|
||||||
|
return disksResult.error();
|
||||||
|
|
||||||
|
for (const auto& disk : disksResult.value())
|
||||||
|
{
|
||||||
|
if (!looksLikeSdCard(disk))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto analysisResult = analyzeDisk(disk.id);
|
||||||
|
if (analysisResult.isOk())
|
||||||
|
cards.push_back(std::move(analysisResult.value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try to find disks that SetupAPI detects but enumerateDisks might
|
||||||
|
// miss due to having zero partitions — scan PhysicalDrive0..31 directly
|
||||||
|
for (int i = 0; i < 32; ++i)
|
||||||
|
{
|
||||||
|
// Skip if we already found this disk
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& card : cards)
|
||||||
|
{
|
||||||
|
if (card.diskId == i)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Try to open the disk directly
|
||||||
|
auto diskResult = RawDiskHandle::open(i, DiskAccessMode::ReadOnly);
|
||||||
|
if (diskResult.isError())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto& disk = diskResult.value();
|
||||||
|
auto geomResult = disk.getGeometry();
|
||||||
|
if (geomResult.isError())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if it's removable media
|
||||||
|
if (geomResult.value().mediaType == RemovableMedia ||
|
||||||
|
geomResult.value().mediaType == FixedMedia)
|
||||||
|
{
|
||||||
|
// Only include small removable disks (likely SD cards)
|
||||||
|
auto totalBytes = geomResult.value().totalBytes;
|
||||||
|
if (totalBytes > 0 && totalBytes <= 2199023255552ULL)
|
||||||
|
{
|
||||||
|
auto analysisResult = analyzeDisk(i);
|
||||||
|
if (analysisResult.isOk())
|
||||||
|
{
|
||||||
|
auto& info = analysisResult.value();
|
||||||
|
if (info.model.empty())
|
||||||
|
info.model = L"Unknown Removable Disk";
|
||||||
|
cards.push_back(std::move(info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> SdCardRecovery::cleanDisk(RawDiskHandle& disk, uint64_t diskSize)
|
||||||
|
{
|
||||||
|
HANDLE hDisk = disk.nativeHandle();
|
||||||
|
|
||||||
|
// First zero out the first and last 1MB to destroy any existing
|
||||||
|
// partition tables (MBR at sector 0, GPT at sector 1 + backup at end)
|
||||||
|
constexpr uint64_t kCleanSize = 1048576; // 1 MB
|
||||||
|
std::vector<uint8_t> zeros(static_cast<size_t>(kCleanSize), 0);
|
||||||
|
|
||||||
|
// Zero the beginning
|
||||||
|
LARGE_INTEGER offset;
|
||||||
|
offset.QuadPart = 0;
|
||||||
|
if (!SetFilePointerEx(hDisk, offset, nullptr, FILE_BEGIN))
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
|
||||||
|
"Failed to seek to disk start");
|
||||||
|
|
||||||
|
DWORD written = 0;
|
||||||
|
if (!WriteFile(hDisk, zeros.data(), static_cast<DWORD>(kCleanSize), &written, nullptr))
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
|
||||||
|
"Failed to zero disk start");
|
||||||
|
|
||||||
|
// Zero the end (backup GPT)
|
||||||
|
if (diskSize > kCleanSize)
|
||||||
|
{
|
||||||
|
offset.QuadPart = static_cast<LONGLONG>(diskSize - kCleanSize);
|
||||||
|
if (SetFilePointerEx(hDisk, offset, nullptr, FILE_BEGIN))
|
||||||
|
{
|
||||||
|
WriteFile(hDisk, zeros.data(), static_cast<DWORD>(kCleanSize), &written, nullptr);
|
||||||
|
// Failure to zero end is non-fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now use IOCTL_DISK_CREATE_DISK to create a fresh MBR
|
||||||
|
CREATE_DISK createDisk;
|
||||||
|
std::memset(&createDisk, 0, sizeof(createDisk));
|
||||||
|
createDisk.PartitionStyle = PARTITION_STYLE_MBR;
|
||||||
|
createDisk.Mbr.Signature = GetTickCount(); // Random-ish signature
|
||||||
|
|
||||||
|
DWORD bytesReturned = 0;
|
||||||
|
if (!DeviceIoControl(hDisk, IOCTL_DISK_CREATE_DISK,
|
||||||
|
&createDisk, sizeof(createDisk),
|
||||||
|
nullptr, 0, &bytesReturned, nullptr))
|
||||||
|
{
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
|
||||||
|
"IOCTL_DISK_CREATE_DISK failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<void>::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> SdCardRecovery::createPartition(RawDiskHandle& disk, uint64_t diskSize,
|
||||||
|
uint32_t sectorSize, FilesystemType fs)
|
||||||
|
{
|
||||||
|
HANDLE hDisk = disk.nativeHandle();
|
||||||
|
|
||||||
|
// Allocate buffer for DRIVE_LAYOUT_INFORMATION_EX with 4 MBR entries
|
||||||
|
size_t bufSize = sizeof(DRIVE_LAYOUT_INFORMATION_EX)
|
||||||
|
+ 3 * sizeof(PARTITION_INFORMATION_EX);
|
||||||
|
std::vector<uint8_t> buf(bufSize, 0);
|
||||||
|
auto* layout = reinterpret_cast<DRIVE_LAYOUT_INFORMATION_EX*>(buf.data());
|
||||||
|
|
||||||
|
layout->PartitionStyle = PARTITION_STYLE_MBR;
|
||||||
|
layout->PartitionCount = 4; // MBR always has 4 entries
|
||||||
|
layout->Mbr.Signature = GetTickCount();
|
||||||
|
|
||||||
|
// Single partition starting at 1MB offset (aligned)
|
||||||
|
uint64_t partOffset = 1048576; // 1 MB alignment
|
||||||
|
if (partOffset < static_cast<uint64_t>(sectorSize))
|
||||||
|
partOffset = sectorSize;
|
||||||
|
|
||||||
|
uint64_t partLength = diskSize - partOffset;
|
||||||
|
// Align partition length down to sector boundary
|
||||||
|
partLength = (partLength / sectorSize) * sectorSize;
|
||||||
|
|
||||||
|
auto& part = layout->PartitionEntry[0];
|
||||||
|
part.PartitionStyle = PARTITION_STYLE_MBR;
|
||||||
|
part.StartingOffset.QuadPart = static_cast<LONGLONG>(partOffset);
|
||||||
|
part.PartitionLength.QuadPart = static_cast<LONGLONG>(partLength);
|
||||||
|
part.PartitionNumber = 1;
|
||||||
|
part.RewritePartition = TRUE;
|
||||||
|
|
||||||
|
// Set MBR partition type
|
||||||
|
switch (fs)
|
||||||
|
{
|
||||||
|
case FilesystemType::FAT32:
|
||||||
|
// FAT32 LBA
|
||||||
|
part.Mbr.PartitionType = partLength > 4294967296ULL ? 0x0C : 0x0B;
|
||||||
|
break;
|
||||||
|
case FilesystemType::ExFAT:
|
||||||
|
part.Mbr.PartitionType = 0x07; // exFAT uses type 0x07 same as NTFS
|
||||||
|
break;
|
||||||
|
case FilesystemType::NTFS:
|
||||||
|
part.Mbr.PartitionType = 0x07;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
part.Mbr.PartitionType = 0x0B; // Default to FAT32
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
part.Mbr.BootIndicator = FALSE;
|
||||||
|
part.Mbr.RecognizedPartition = TRUE;
|
||||||
|
|
||||||
|
// Zero out the remaining 3 MBR entries
|
||||||
|
for (int i = 1; i < 4; ++i)
|
||||||
|
{
|
||||||
|
layout->PartitionEntry[i].RewritePartition = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD bytesReturned = 0;
|
||||||
|
if (!DeviceIoControl(hDisk, IOCTL_DISK_SET_DRIVE_LAYOUT_EX,
|
||||||
|
buf.data(), static_cast<DWORD>(bufSize),
|
||||||
|
nullptr, 0, &bytesReturned, nullptr))
|
||||||
|
{
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
|
||||||
|
"IOCTL_DISK_SET_DRIVE_LAYOUT_EX failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<void>::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> SdCardRecovery::rescanDisk(RawDiskHandle& disk)
|
||||||
|
{
|
||||||
|
HANDLE hDisk = disk.nativeHandle();
|
||||||
|
DWORD bytesReturned = 0;
|
||||||
|
|
||||||
|
// Tell Windows to re-read the partition table
|
||||||
|
if (!DeviceIoControl(hDisk, IOCTL_DISK_UPDATE_PROPERTIES,
|
||||||
|
nullptr, 0, nullptr, 0, &bytesReturned, nullptr))
|
||||||
|
{
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
|
||||||
|
"IOCTL_DISK_UPDATE_PROPERTIES failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<void>::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> SdCardRecovery::formatPartition(DiskId diskId, FilesystemType fs,
|
||||||
|
const std::wstring& label)
|
||||||
|
{
|
||||||
|
// Wait for Windows to assign a drive letter after partition creation
|
||||||
|
Sleep(2000);
|
||||||
|
|
||||||
|
// Find the drive letter for the new partition
|
||||||
|
auto snapshotResult = DiskEnumerator::getSystemSnapshot();
|
||||||
|
if (snapshotResult.isError())
|
||||||
|
return snapshotResult.error();
|
||||||
|
|
||||||
|
wchar_t driveLetter = L'\0';
|
||||||
|
for (const auto& part : snapshotResult.value().partitions)
|
||||||
|
{
|
||||||
|
if (part.diskId == diskId && part.driveLetter != L'\0')
|
||||||
|
{
|
||||||
|
driveLetter = part.driveLetter;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driveLetter == L'\0')
|
||||||
|
{
|
||||||
|
return ErrorInfo::fromCode(ErrorCode::DiskNotFound,
|
||||||
|
"Windows did not assign a drive letter. "
|
||||||
|
"Open Disk Management and assign a letter, then format manually.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build format command
|
||||||
|
std::wstring fsName;
|
||||||
|
switch (fs)
|
||||||
|
{
|
||||||
|
case FilesystemType::FAT32: fsName = L"FAT32"; break;
|
||||||
|
case FilesystemType::ExFAT: fsName = L"exFAT"; break;
|
||||||
|
case FilesystemType::NTFS: fsName = L"NTFS"; break;
|
||||||
|
default: fsName = L"FAT32"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format X: /FS:FAT32 /Q /V:label /Y
|
||||||
|
std::wstring cmd = L"format " + std::wstring(1, driveLetter) + L": /FS:" + fsName
|
||||||
|
+ L" /Q /V:" + label + L" /Y";
|
||||||
|
|
||||||
|
STARTUPINFOW si = {};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||||
|
si.wShowWindow = SW_HIDE;
|
||||||
|
|
||||||
|
PROCESS_INFORMATION pi = {};
|
||||||
|
|
||||||
|
if (!CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, FALSE,
|
||||||
|
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi))
|
||||||
|
{
|
||||||
|
return ErrorInfo::fromWin32(ErrorCode::FormatFailed, GetLastError(),
|
||||||
|
"Failed to launch format command");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait up to 60 seconds for format to complete
|
||||||
|
DWORD waitResult = WaitForSingleObject(pi.hProcess, 60000);
|
||||||
|
DWORD exitCode = 1;
|
||||||
|
GetExitCodeProcess(pi.hProcess, &exitCode);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
|
||||||
|
if (waitResult == WAIT_TIMEOUT)
|
||||||
|
return ErrorInfo::fromCode(ErrorCode::FormatFailed, "Format command timed out");
|
||||||
|
|
||||||
|
if (exitCode != 0)
|
||||||
|
return ErrorInfo::fromCode(ErrorCode::FormatFailed,
|
||||||
|
"Format command failed with exit code " + std::to_string(exitCode));
|
||||||
|
|
||||||
|
return Result<void>::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> SdCardRecovery::fixCard(DiskId diskId, const SdFixConfig& config,
|
||||||
|
SdProgressCallback progress)
|
||||||
|
{
|
||||||
|
auto report = [&progress](const std::string& stage, int pct) {
|
||||||
|
if (progress)
|
||||||
|
progress(stage, pct);
|
||||||
|
};
|
||||||
|
|
||||||
|
report("Opening disk...", 0);
|
||||||
|
|
||||||
|
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
|
||||||
|
if (diskResult.isError())
|
||||||
|
return diskResult.error();
|
||||||
|
|
||||||
|
auto& disk = diskResult.value();
|
||||||
|
|
||||||
|
// Get geometry for disk size
|
||||||
|
auto geomResult = disk.getGeometry();
|
||||||
|
if (geomResult.isError())
|
||||||
|
return geomResult.error();
|
||||||
|
|
||||||
|
uint64_t diskSize = geomResult.value().totalBytes;
|
||||||
|
uint32_t sectorSize = geomResult.value().bytesPerSector;
|
||||||
|
|
||||||
|
if (diskSize == 0)
|
||||||
|
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports zero size - no media?");
|
||||||
|
|
||||||
|
// Auto-select filesystem based on size if FAT32 requested on >32GB card
|
||||||
|
FilesystemType targetFs = config.targetFs;
|
||||||
|
if (targetFs == FilesystemType::FAT32 && diskSize > 34359738368ULL) // >32GB
|
||||||
|
targetFs = FilesystemType::ExFAT;
|
||||||
|
|
||||||
|
if (config.action == SdFixAction::CleanAndFormat ||
|
||||||
|
config.action == SdFixAction::ReinitPartitionOnly)
|
||||||
|
{
|
||||||
|
report("Cleaning disk (zeroing partition tables)...", 10);
|
||||||
|
auto cleanResult = cleanDisk(disk, diskSize);
|
||||||
|
if (cleanResult.isError())
|
||||||
|
return cleanResult;
|
||||||
|
|
||||||
|
report("Creating partition table...", 30);
|
||||||
|
auto partResult = createPartition(disk, diskSize, sectorSize, targetFs);
|
||||||
|
if (partResult.isError())
|
||||||
|
return partResult;
|
||||||
|
|
||||||
|
report("Updating disk properties...", 50);
|
||||||
|
auto rescanResult = rescanDisk(disk);
|
||||||
|
if (rescanResult.isError())
|
||||||
|
{
|
||||||
|
log::warn(("Rescan failed (non-fatal): " + rescanResult.error().message).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the disk handle before formatting so Windows can access it
|
||||||
|
disk.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.action == SdFixAction::CleanAndFormat ||
|
||||||
|
config.action == SdFixAction::FormatOnly)
|
||||||
|
{
|
||||||
|
report("Formatting partition...", 60);
|
||||||
|
auto fmtResult = formatPartition(diskId, targetFs, config.volumeLabel);
|
||||||
|
if (fmtResult.isError())
|
||||||
|
return fmtResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
report("Done!", 100);
|
||||||
|
return Result<void>::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace spw
|
||||||
106
src/core/maintenance/SdCardRecovery.h
Normal file
106
src/core/maintenance/SdCardRecovery.h
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// SdCardRecovery — Detects and repairs SD cards that Windows cannot see.
|
||||||
|
// Handles cards with corrupted partition tables (e.g., from interrupted format),
|
||||||
|
// RAW/uninitialized cards, and cards with no valid filesystem.
|
||||||
|
// Uses raw disk I/O via IOCTL_DISK_CREATE_DISK and IOCTL_DISK_SET_DRIVE_LAYOUT_EX
|
||||||
|
// to reinitialize partition tables without relying on Windows volume management.
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <winioctl.h>
|
||||||
|
|
||||||
|
#include "../common/Error.h"
|
||||||
|
#include "../common/Result.h"
|
||||||
|
#include "../common/Types.h"
|
||||||
|
#include "../disk/DiskEnumerator.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace spw
|
||||||
|
{
|
||||||
|
|
||||||
|
class RawDiskHandle;
|
||||||
|
|
||||||
|
// Status of an SD card as detected by the recovery tool
|
||||||
|
enum class SdCardStatus
|
||||||
|
{
|
||||||
|
Healthy, // Card is fine, has valid partition table and filesystem
|
||||||
|
NoPartitionTable, // No valid MBR or GPT found
|
||||||
|
CorruptPartition, // Partition table exists but is damaged
|
||||||
|
RawFilesystem, // Partition exists but filesystem is RAW/unrecognized
|
||||||
|
NoMedia, // Card reader detected but no card inserted
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
// Information about a detected SD card (may or may not be visible to Windows)
|
||||||
|
struct SdCardInfo
|
||||||
|
{
|
||||||
|
DiskId diskId = -1;
|
||||||
|
std::wstring model;
|
||||||
|
std::wstring serialNumber;
|
||||||
|
uint64_t sizeBytes = 0;
|
||||||
|
uint32_t sectorSize = 512;
|
||||||
|
SdCardStatus status = SdCardStatus::Unknown;
|
||||||
|
std::wstring statusDescription;
|
||||||
|
bool hasPartitions = false;
|
||||||
|
bool hasDriveLetter = false;
|
||||||
|
wchar_t driveLetter = L'\0';
|
||||||
|
DiskInterfaceType interfaceType = DiskInterfaceType::Unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What to do when fixing the card
|
||||||
|
enum class SdFixAction
|
||||||
|
{
|
||||||
|
CleanAndFormat, // Wipe partition table, create new MBR + single FAT32/exFAT partition
|
||||||
|
ReinitPartitionOnly, // Just rewrite the partition table, don't format
|
||||||
|
FormatOnly // Keep partition table, just format the existing partition
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SdFixConfig
|
||||||
|
{
|
||||||
|
SdFixAction action = SdFixAction::CleanAndFormat;
|
||||||
|
FilesystemType targetFs = FilesystemType::FAT32; // FAT32 for <= 32GB, exFAT for > 32GB
|
||||||
|
std::wstring volumeLabel = L"SD_CARD";
|
||||||
|
bool quickFormat = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
using SdProgressCallback = std::function<void(const std::string& stage, int percentComplete)>;
|
||||||
|
|
||||||
|
class SdCardRecovery
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Scan the system for SD/MMC card readers and cards, including those
|
||||||
|
// that Windows doesn't assign drive letters to.
|
||||||
|
// This uses SetupAPI directly, not just volume enumeration.
|
||||||
|
static Result<std::vector<SdCardInfo>> detectSdCards();
|
||||||
|
|
||||||
|
// Analyze a specific disk to determine its SD card status
|
||||||
|
static Result<SdCardInfo> analyzeDisk(DiskId diskId);
|
||||||
|
|
||||||
|
// Fix an SD card: clean partition table, create new partition, and format
|
||||||
|
static Result<void> fixCard(DiskId diskId, const SdFixConfig& config,
|
||||||
|
SdProgressCallback progress = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Clean the disk by creating a fresh MBR or GPT
|
||||||
|
static Result<void> cleanDisk(RawDiskHandle& disk, uint64_t diskSize);
|
||||||
|
|
||||||
|
// Create a single partition spanning the entire disk
|
||||||
|
static Result<void> createPartition(RawDiskHandle& disk, uint64_t diskSize,
|
||||||
|
uint32_t sectorSize, FilesystemType fs);
|
||||||
|
|
||||||
|
// Quick format the new partition
|
||||||
|
static Result<void> formatPartition(DiskId diskId, FilesystemType fs,
|
||||||
|
const std::wstring& label);
|
||||||
|
|
||||||
|
// Force Windows to rescan the disk for new partitions
|
||||||
|
static Result<void> rescanDisk(RawDiskHandle& disk);
|
||||||
|
|
||||||
|
// Check if a disk looks like an SD card based on bus type, removability, size
|
||||||
|
static bool looksLikeSdCard(const DiskInfo& disk);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace spw
|
||||||
@@ -233,8 +233,8 @@ void DiagnosticsTab::displaySmartData(const SmartData& data)
|
|||||||
{
|
{
|
||||||
case SmartStatus::OK:
|
case SmartStatus::OK:
|
||||||
healthText = tr("PASSED - Healthy");
|
healthText = tr("PASSED - Healthy");
|
||||||
healthColor = QColor(0, 180, 0);
|
healthColor = QColor(212, 165, 116);
|
||||||
m_healthIcon->setStyleSheet("background-color: #00b400; border-radius: 24px;");
|
m_healthIcon->setStyleSheet("background-color: #d4a574; border-radius: 24px;");
|
||||||
break;
|
break;
|
||||||
case SmartStatus::Warning:
|
case SmartStatus::Warning:
|
||||||
healthText = tr("WARNING - Issues Detected");
|
healthText = tr("WARNING - Issues Detected");
|
||||||
@@ -523,7 +523,7 @@ QColor DiagnosticsTab::smartStatusColor(SmartStatus status)
|
|||||||
{
|
{
|
||||||
switch (status)
|
switch (status)
|
||||||
{
|
{
|
||||||
case SmartStatus::OK: return QColor(0, 180, 0);
|
case SmartStatus::OK: return QColor(212, 165, 116);
|
||||||
case SmartStatus::Warning: return QColor(255, 180, 0);
|
case SmartStatus::Warning: return QColor(255, 180, 0);
|
||||||
case SmartStatus::Critical: return QColor(255, 0, 0);
|
case SmartStatus::Critical: return QColor(255, 0, 0);
|
||||||
default: return QColor(128, 128, 128);
|
default: return QColor(128, 128, 128);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "core/disk/DiskEnumerator.h"
|
#include "core/disk/DiskEnumerator.h"
|
||||||
#include "core/disk/RawDiskHandle.h"
|
#include "core/disk/RawDiskHandle.h"
|
||||||
#include "core/maintenance/SecureErase.h"
|
#include "core/maintenance/SecureErase.h"
|
||||||
|
#include "core/maintenance/SdCardRecovery.h"
|
||||||
#include "core/recovery/BootRepair.h"
|
#include "core/recovery/BootRepair.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
@@ -137,6 +138,60 @@ void MaintenanceTab::setupUi()
|
|||||||
bootLayout->addWidget(m_bootStatusLabel);
|
bootLayout->addWidget(m_bootStatusLabel);
|
||||||
|
|
||||||
layout->addWidget(bootGroup);
|
layout->addWidget(bootGroup);
|
||||||
|
|
||||||
|
// ===== SD Card Recovery Section =====
|
||||||
|
auto* sdGroup = new QGroupBox(tr("SD Card Recovery"));
|
||||||
|
auto* sdLayout = new QGridLayout(sdGroup);
|
||||||
|
|
||||||
|
auto* sdInfo = new QLabel(
|
||||||
|
tr("Detect and fix SD/microSD cards that Windows cannot see.\n"
|
||||||
|
"Repairs corrupted partition tables from interrupted formats, "
|
||||||
|
"RAW cards, and uninitialized media."));
|
||||||
|
sdInfo->setWordWrap(true);
|
||||||
|
sdLayout->addWidget(sdInfo, 0, 0, 1, 3);
|
||||||
|
|
||||||
|
m_sdScanBtn = new QPushButton(tr("Scan for SD Cards"));
|
||||||
|
m_sdScanBtn->setToolTip(tr("Scan all disk interfaces for SD/MMC cards, including invisible ones"));
|
||||||
|
connect(m_sdScanBtn, &QPushButton::clicked, this, &MaintenanceTab::onSdScan);
|
||||||
|
sdLayout->addWidget(m_sdScanBtn, 1, 0);
|
||||||
|
|
||||||
|
m_sdCardCombo = new QComboBox();
|
||||||
|
sdLayout->addWidget(m_sdCardCombo, 1, 1, 1, 2);
|
||||||
|
|
||||||
|
sdLayout->addWidget(new QLabel(tr("Format As:")), 2, 0);
|
||||||
|
m_sdFsCombo = new QComboBox();
|
||||||
|
m_sdFsCombo->addItems({
|
||||||
|
tr("FAT32 (recommended for <= 32 GB)"),
|
||||||
|
tr("exFAT (recommended for > 32 GB)"),
|
||||||
|
tr("NTFS")
|
||||||
|
});
|
||||||
|
sdLayout->addWidget(m_sdFsCombo, 2, 1, 1, 2);
|
||||||
|
|
||||||
|
sdLayout->addWidget(new QLabel(tr("Volume Label:")), 3, 0);
|
||||||
|
m_sdLabelEdit = new QLineEdit(QStringLiteral("SD_CARD"));
|
||||||
|
m_sdLabelEdit->setMaxLength(11);
|
||||||
|
sdLayout->addWidget(m_sdLabelEdit, 3, 1, 1, 2);
|
||||||
|
|
||||||
|
m_sdFixBtn = new QPushButton(tr("Fix SD Card"));
|
||||||
|
m_sdFixBtn->setMinimumHeight(40);
|
||||||
|
m_sdFixBtn->setStyleSheet(
|
||||||
|
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 14px; "
|
||||||
|
"font-weight: bold; border: 2px solid #b08050; border-radius: 6px; }"
|
||||||
|
"QPushButton:hover { background-color: #e0b584; }"
|
||||||
|
"QPushButton:pressed { background-color: #c49060; }");
|
||||||
|
m_sdFixBtn->setEnabled(false);
|
||||||
|
connect(m_sdFixBtn, &QPushButton::clicked, this, &MaintenanceTab::onSdFix);
|
||||||
|
sdLayout->addWidget(m_sdFixBtn, 4, 0, 1, 3);
|
||||||
|
|
||||||
|
m_sdProgress = new QProgressBar();
|
||||||
|
m_sdProgress->setVisible(false);
|
||||||
|
sdLayout->addWidget(m_sdProgress, 5, 0, 1, 3);
|
||||||
|
|
||||||
|
m_sdStatusLabel = new QLabel();
|
||||||
|
m_sdStatusLabel->setWordWrap(true);
|
||||||
|
sdLayout->addWidget(m_sdStatusLabel, 6, 0, 1, 3);
|
||||||
|
|
||||||
|
layout->addWidget(sdGroup);
|
||||||
layout->addStretch();
|
layout->addStretch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +520,149 @@ void MaintenanceTab::onReinstallBootloader()
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MaintenanceTab::onSdScan()
|
||||||
|
{
|
||||||
|
m_sdScanBtn->setEnabled(false);
|
||||||
|
m_sdStatusLabel->setText(tr("Scanning for SD cards..."));
|
||||||
|
m_sdCardCombo->clear();
|
||||||
|
m_detectedCards.clear();
|
||||||
|
m_sdFixBtn->setEnabled(false);
|
||||||
|
|
||||||
|
auto* thread = QThread::create([this]() {
|
||||||
|
auto result = SdCardRecovery::detectSdCards();
|
||||||
|
if (result.isError())
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(m_sdStatusLabel, "setText",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(QString, tr("Scan failed: %1")
|
||||||
|
.arg(QString::fromStdString(result.error().message))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_detectedCards = std::move(result.value());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, this, [this]() {
|
||||||
|
m_sdScanBtn->setEnabled(true);
|
||||||
|
|
||||||
|
if (m_detectedCards.empty())
|
||||||
|
{
|
||||||
|
m_sdStatusLabel->setText(
|
||||||
|
tr("No SD/MMC cards detected.\n"
|
||||||
|
"Make sure the card is inserted in a reader and the reader is connected."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& card : m_detectedCards)
|
||||||
|
{
|
||||||
|
QString statusStr;
|
||||||
|
switch (card.status)
|
||||||
|
{
|
||||||
|
case SdCardStatus::Healthy: statusStr = tr("Healthy"); break;
|
||||||
|
case SdCardStatus::NoPartitionTable: statusStr = tr("NO PARTITION TABLE"); break;
|
||||||
|
case SdCardStatus::CorruptPartition: statusStr = tr("CORRUPT"); break;
|
||||||
|
case SdCardStatus::RawFilesystem: statusStr = tr("RAW"); break;
|
||||||
|
case SdCardStatus::NoMedia: statusStr = tr("No Media"); break;
|
||||||
|
default: statusStr = tr("Unknown"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString label = QString("Disk %1: %2 (%3) [%4]")
|
||||||
|
.arg(card.diskId)
|
||||||
|
.arg(QString::fromStdWString(card.model))
|
||||||
|
.arg(formatSize(card.sizeBytes))
|
||||||
|
.arg(statusStr);
|
||||||
|
m_sdCardCombo->addItem(label, card.diskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sdFixBtn->setEnabled(true);
|
||||||
|
m_sdStatusLabel->setText(
|
||||||
|
tr("Found %1 SD card(s). Select one and click Fix to repair.")
|
||||||
|
.arg(m_detectedCards.size()));
|
||||||
|
|
||||||
|
emit statusMessage(tr("SD card scan complete — %1 card(s) found")
|
||||||
|
.arg(m_detectedCards.size()));
|
||||||
|
});
|
||||||
|
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaintenanceTab::onSdFix()
|
||||||
|
{
|
||||||
|
int diskId = m_sdCardCombo->currentData().toInt();
|
||||||
|
|
||||||
|
// Find the card info
|
||||||
|
const SdCardInfo* cardInfo = nullptr;
|
||||||
|
for (const auto& card : m_detectedCards)
|
||||||
|
{
|
||||||
|
if (card.diskId == diskId)
|
||||||
|
{
|
||||||
|
cardInfo = &card;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cardInfo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
|
auto reply = QMessageBox::warning(this, tr("Fix SD Card"),
|
||||||
|
tr("This will ERASE ALL DATA on:\n\n"
|
||||||
|
"Disk %1: %2 (%3)\n\n"
|
||||||
|
"The card will be cleaned, repartitioned, and formatted.\n\n"
|
||||||
|
"Continue?")
|
||||||
|
.arg(diskId)
|
||||||
|
.arg(QString::fromStdWString(cardInfo->model))
|
||||||
|
.arg(formatSize(cardInfo->sizeBytes)),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
if (reply != QMessageBox::Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Build config
|
||||||
|
SdFixConfig config;
|
||||||
|
config.action = SdFixAction::CleanAndFormat;
|
||||||
|
switch (m_sdFsCombo->currentIndex())
|
||||||
|
{
|
||||||
|
case 0: config.targetFs = FilesystemType::FAT32; break;
|
||||||
|
case 1: config.targetFs = FilesystemType::ExFAT; break;
|
||||||
|
case 2: config.targetFs = FilesystemType::NTFS; break;
|
||||||
|
}
|
||||||
|
config.volumeLabel = m_sdLabelEdit->text().toStdWString();
|
||||||
|
|
||||||
|
m_sdFixBtn->setEnabled(false);
|
||||||
|
m_sdScanBtn->setEnabled(false);
|
||||||
|
m_sdProgress->setVisible(true);
|
||||||
|
m_sdProgress->setValue(0);
|
||||||
|
m_sdStatusLabel->setText(tr("Fixing SD card..."));
|
||||||
|
|
||||||
|
auto* thread = QThread::create([this, diskId, config]() {
|
||||||
|
auto result = SdCardRecovery::fixCard(diskId, config,
|
||||||
|
[this](const std::string& stage, int pct) {
|
||||||
|
QMetaObject::invokeMethod(m_sdProgress, "setValue",
|
||||||
|
Qt::QueuedConnection, Q_ARG(int, pct));
|
||||||
|
QMetaObject::invokeMethod(m_sdStatusLabel, "setText",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(QString, QString::fromStdString(stage)));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isError())
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(m_sdStatusLabel, "setText",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(QString, tr("Fix failed: %1")
|
||||||
|
.arg(QString::fromStdString(result.error().message))));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||||
|
connect(thread, &QThread::finished, this, [this]() {
|
||||||
|
m_sdProgress->setVisible(false);
|
||||||
|
m_sdFixBtn->setEnabled(true);
|
||||||
|
m_sdScanBtn->setEnabled(true);
|
||||||
|
emit statusMessage(tr("SD card fix completed"));
|
||||||
|
});
|
||||||
|
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
QString MaintenanceTab::formatSize(uint64_t bytes)
|
QString MaintenanceTab::formatSize(uint64_t bytes)
|
||||||
{
|
{
|
||||||
if (bytes >= 1099511627776ULL)
|
if (bytes >= 1099511627776ULL)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "core/common/Types.h"
|
#include "core/common/Types.h"
|
||||||
#include "core/disk/DiskEnumerator.h"
|
#include "core/disk/DiskEnumerator.h"
|
||||||
#include "core/maintenance/SecureErase.h"
|
#include "core/maintenance/SecureErase.h"
|
||||||
|
#include "core/maintenance/SdCardRecovery.h"
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -40,6 +41,8 @@ private slots:
|
|||||||
void onRepairGpt();
|
void onRepairGpt();
|
||||||
void onRepairBcd();
|
void onRepairBcd();
|
||||||
void onReinstallBootloader();
|
void onReinstallBootloader();
|
||||||
|
void onSdScan();
|
||||||
|
void onSdFix();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupUi();
|
void setupUi();
|
||||||
@@ -65,6 +68,16 @@ private:
|
|||||||
QProgressBar* m_bootProgress = nullptr;
|
QProgressBar* m_bootProgress = nullptr;
|
||||||
QLabel* m_bootStatusLabel = nullptr;
|
QLabel* m_bootStatusLabel = nullptr;
|
||||||
|
|
||||||
|
// SD Card Recovery
|
||||||
|
QComboBox* m_sdCardCombo = nullptr;
|
||||||
|
QPushButton* m_sdScanBtn = nullptr;
|
||||||
|
QPushButton* m_sdFixBtn = nullptr;
|
||||||
|
QComboBox* m_sdFsCombo = nullptr;
|
||||||
|
QLineEdit* m_sdLabelEdit = nullptr;
|
||||||
|
QLabel* m_sdStatusLabel = nullptr;
|
||||||
|
QProgressBar* m_sdProgress = nullptr;
|
||||||
|
std::vector<SdCardInfo> m_detectedCards;
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
SystemDiskSnapshot m_snapshot;
|
SystemDiskSnapshot m_snapshot;
|
||||||
std::atomic<bool> m_cancelFlag{false};
|
std::atomic<bool> m_cancelFlag{false};
|
||||||
|
|||||||
@@ -291,10 +291,10 @@ QColor DiskMapWidget::colorForFilesystem(FilesystemType fs)
|
|||||||
switch (fs)
|
switch (fs)
|
||||||
{
|
{
|
||||||
case FilesystemType::NTFS: return QColor(52, 101, 164);
|
case FilesystemType::NTFS: return QColor(52, 101, 164);
|
||||||
case FilesystemType::FAT32: return QColor(78, 154, 6);
|
case FilesystemType::FAT32: return QColor(180, 145, 120);
|
||||||
case FilesystemType::FAT16: return QColor(78, 154, 6);
|
case FilesystemType::FAT16: return QColor(180, 145, 120);
|
||||||
case FilesystemType::FAT12: return QColor(78, 154, 6);
|
case FilesystemType::FAT12: return QColor(180, 145, 120);
|
||||||
case FilesystemType::ExFAT: return QColor(115, 210, 22);
|
case FilesystemType::ExFAT: return QColor(212, 165, 116);
|
||||||
case FilesystemType::ReFS: return QColor(32, 74, 135);
|
case FilesystemType::ReFS: return QColor(32, 74, 135);
|
||||||
case FilesystemType::Ext2: return QColor(204, 0, 0);
|
case FilesystemType::Ext2: return QColor(204, 0, 0);
|
||||||
case FilesystemType::Ext3: return QColor(204, 0, 0);
|
case FilesystemType::Ext3: return QColor(204, 0, 0);
|
||||||
@@ -307,6 +307,29 @@ QColor DiskMapWidget::colorForFilesystem(FilesystemType fs)
|
|||||||
case FilesystemType::ISO9660: return QColor(85, 87, 83);
|
case FilesystemType::ISO9660: return QColor(85, 87, 83);
|
||||||
case FilesystemType::UDF: return QColor(85, 87, 83);
|
case FilesystemType::UDF: return QColor(85, 87, 83);
|
||||||
case FilesystemType::Unallocated: return QColor(80, 80, 80);
|
case FilesystemType::Unallocated: return QColor(80, 80, 80);
|
||||||
|
// Flash-optimized
|
||||||
|
case FilesystemType::F2FS: return QColor(0, 150, 136); // Teal
|
||||||
|
case FilesystemType::JFFS2: return QColor(121, 85, 72); // Brown
|
||||||
|
case FilesystemType::NILFS2: return QColor(158, 157, 36); // Lime
|
||||||
|
// Console / gaming
|
||||||
|
case FilesystemType::FATX: return QColor(76, 175, 80); // Xbox green
|
||||||
|
case FilesystemType::STFS: return QColor(56, 142, 60); // Xbox dark green
|
||||||
|
case FilesystemType::GDFX: return QColor(46, 125, 50); // Xbox disc green
|
||||||
|
case FilesystemType::PS2MC: return QColor(33, 150, 243); // PS blue
|
||||||
|
// Virtual disk images
|
||||||
|
case FilesystemType::VHD: return QColor(0, 120, 215); // Windows blue
|
||||||
|
case FilesystemType::VHDX: return QColor(0, 99, 177);
|
||||||
|
case FilesystemType::VMDK: return QColor(120, 144, 156); // Slate
|
||||||
|
case FilesystemType::QCOW2: return QColor(255, 87, 34); // Deep orange
|
||||||
|
case FilesystemType::VDI: return QColor(63, 81, 181); // Indigo
|
||||||
|
// Disc images
|
||||||
|
case FilesystemType::RVZ: return QColor(156, 39, 176); // Purple
|
||||||
|
case FilesystemType::WUA: return QColor(103, 58, 183); // Deep purple
|
||||||
|
case FilesystemType::WBFs: return QColor(0, 188, 212); // Cyan
|
||||||
|
case FilesystemType::NRG: return QColor(96, 125, 139); // Blue grey
|
||||||
|
case FilesystemType::MDF: return QColor(96, 125, 139);
|
||||||
|
case FilesystemType::CDI: return QColor(96, 125, 139);
|
||||||
|
case FilesystemType::CDFS: return QColor(85, 87, 83);
|
||||||
default: return QColor(136, 138, 133);
|
default: return QColor(136, 138, 133);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +355,25 @@ QString DiskMapWidget::filesystemShortName(FilesystemType fs)
|
|||||||
case FilesystemType::SWAP_LINUX: return QStringLiteral("Swap");
|
case FilesystemType::SWAP_LINUX: return QStringLiteral("Swap");
|
||||||
case FilesystemType::ISO9660: return QStringLiteral("ISO9660");
|
case FilesystemType::ISO9660: return QStringLiteral("ISO9660");
|
||||||
case FilesystemType::UDF: return QStringLiteral("UDF");
|
case FilesystemType::UDF: return QStringLiteral("UDF");
|
||||||
|
case FilesystemType::F2FS: return QStringLiteral("F2FS");
|
||||||
|
case FilesystemType::JFFS2: return QStringLiteral("JFFS2");
|
||||||
|
case FilesystemType::NILFS2: return QStringLiteral("NILFS2");
|
||||||
|
case FilesystemType::FATX: return QStringLiteral("FATX");
|
||||||
|
case FilesystemType::STFS: return QStringLiteral("STFS");
|
||||||
|
case FilesystemType::GDFX: return QStringLiteral("GDFX");
|
||||||
|
case FilesystemType::PS2MC: return QStringLiteral("PS2MC");
|
||||||
|
case FilesystemType::VHD: return QStringLiteral("VHD");
|
||||||
|
case FilesystemType::VHDX: return QStringLiteral("VHDX");
|
||||||
|
case FilesystemType::VMDK: return QStringLiteral("VMDK");
|
||||||
|
case FilesystemType::QCOW2: return QStringLiteral("QCOW2");
|
||||||
|
case FilesystemType::VDI: return QStringLiteral("VDI");
|
||||||
|
case FilesystemType::RVZ: return QStringLiteral("RVZ");
|
||||||
|
case FilesystemType::WUA: return QStringLiteral("WUA");
|
||||||
|
case FilesystemType::WBFs: return QStringLiteral("WBFS");
|
||||||
|
case FilesystemType::NRG: return QStringLiteral("NRG");
|
||||||
|
case FilesystemType::MDF: return QStringLiteral("MDF");
|
||||||
|
case FilesystemType::CDI: return QStringLiteral("CDI");
|
||||||
|
case FilesystemType::CDFS: return QStringLiteral("CDFS");
|
||||||
case FilesystemType::Unallocated: return QStringLiteral("Free");
|
case FilesystemType::Unallocated: return QStringLiteral("Free");
|
||||||
case FilesystemType::Unknown: return QStringLiteral("Unknown");
|
case FilesystemType::Unknown: return QStringLiteral("Unknown");
|
||||||
case FilesystemType::Raw: return QStringLiteral("RAW");
|
case FilesystemType::Raw: return QStringLiteral("RAW");
|
||||||
|
|||||||
BIN
third_party/hwdiag/lib/spw_hwdiag.lib
vendored
BIN
third_party/hwdiag/lib/spw_hwdiag.lib
vendored
Binary file not shown.
BIN
virustotal.jpg
Normal file
BIN
virustotal.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Reference in New Issue
Block a user