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/build/
|
||||
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.**
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
### 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
|
||||
- **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
|
||||
- Random fill and custom byte patterns
|
||||
- **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
|
||||
|
||||
```bash
|
||||
# Debug build
|
||||
cmake --preset default
|
||||
cmake --build --preset default
|
||||
|
||||
# Release build
|
||||
cmake --preset release
|
||||
cmake --build --preset release
|
||||
```
|
||||
|
||||
### Build Requirements
|
||||
@@ -106,7 +133,7 @@ src/
|
||||
│ ├── recovery/ # Partition recovery, file carving, boot repair
|
||||
│ ├── diagnostics/ # Benchmarks, surface scan
|
||||
│ ├── imaging/ # Disk cloner, image creator/restorer, ISO flasher
|
||||
│ ├── maintenance/ # Secure erase
|
||||
│ ├── maintenance/ # Secure erase, SD card recovery
|
||||
│ └── security/ # Encrypted vaults, FIDO2, boot auth
|
||||
├── ui/ # spw_ui static library (Qt Widgets)
|
||||
│ ├── MainWindow # Tab container with visual disk map
|
||||
@@ -122,6 +149,7 @@ src/
|
||||
- **Operation queue** — changes are queued, previewed, then applied atomically
|
||||
- **Removable-only safety** — ISO flasher refuses to write to fixed disks
|
||||
- **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."*
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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 {
|
||||
background-color: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QTabWidget::pane {
|
||||
@@ -12,7 +12,7 @@ QTabWidget::pane {
|
||||
|
||||
QTabBar::tab {
|
||||
background-color: #313244;
|
||||
color: #bac2de;
|
||||
color: #ffffff;
|
||||
padding: 8px 20px;
|
||||
margin-right: 2px;
|
||||
border-top-left-radius: 4px;
|
||||
@@ -22,8 +22,8 @@ QTabBar::tab {
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background-color: #45475a;
|
||||
color: #cdd6f4;
|
||||
border-bottom: 2px solid #89b4fa;
|
||||
color: #ffffff;
|
||||
border-bottom: 2px solid #d4a574;
|
||||
}
|
||||
|
||||
QTabBar::tab:hover:!selected {
|
||||
@@ -32,10 +32,10 @@ QTabBar::tab:hover:!selected {
|
||||
|
||||
QTreeView, QTableView, QListView {
|
||||
background-color: #181825;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
border: 1px solid #45475a;
|
||||
selection-background-color: #45475a;
|
||||
selection-color: #cdd6f4;
|
||||
selection-color: #ffffff;
|
||||
alternate-background-color: #1e1e2e;
|
||||
}
|
||||
|
||||
@@ -45,14 +45,14 @@ QTreeView::item:hover, QTableView::item:hover {
|
||||
|
||||
QHeaderView::section {
|
||||
background-color: #313244;
|
||||
color: #bac2de;
|
||||
color: #ffffff;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #45475a;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
background-color: #45475a;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
border: 1px solid #585b70;
|
||||
border-radius: 4px;
|
||||
padding: 6px 16px;
|
||||
@@ -68,13 +68,13 @@ QPushButton:pressed {
|
||||
}
|
||||
|
||||
QPushButton#applyButton {
|
||||
background-color: #a6e3a1;
|
||||
background-color: #d4a574;
|
||||
color: #1e1e2e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QPushButton#applyButton:hover {
|
||||
background-color: #94e2d5;
|
||||
background-color: #c49060;
|
||||
}
|
||||
|
||||
QPushButton#cancelButton {
|
||||
@@ -87,17 +87,17 @@ QProgressBar {
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #89b4fa;
|
||||
background-color: #d4a574;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
QMenuBar {
|
||||
background-color: #181825;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
@@ -106,7 +106,7 @@ QMenuBar::item:selected {
|
||||
|
||||
QMenu {
|
||||
background-color: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
border: 1px solid #45475a;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ QToolBar {
|
||||
|
||||
QStatusBar {
|
||||
background-color: #181825;
|
||||
color: #a6adc8;
|
||||
color: #ffffff;
|
||||
border-top: 1px solid #45475a;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ QGroupBox {
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
@@ -147,19 +147,63 @@ QGroupBox::title {
|
||||
|
||||
QLineEdit, QSpinBox, QComboBox {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
border: 1px solid #45475a;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QLineEdit:focus, QSpinBox:focus, QComboBox:focus {
|
||||
border-color: #89b4fa;
|
||||
border-color: #d4a574;
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
color: #ffffff;
|
||||
border: 1px solid #45475a;
|
||||
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
|
||||
)
|
||||
|
||||
# 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
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/QtDeployHelper.cmake)
|
||||
spw_deploy_qt(SetecPartitionWizard)
|
||||
|
||||
@@ -38,6 +38,7 @@ set(CORE_SOURCES
|
||||
|
||||
# Maintenance
|
||||
maintenance/SecureErase.cpp
|
||||
maintenance/SdCardRecovery.cpp
|
||||
|
||||
# Security
|
||||
security/EncryptedVault.cpp
|
||||
@@ -76,6 +77,7 @@ set(CORE_HEADERS
|
||||
imaging/ImageRestorer.h
|
||||
imaging/IsoFlasher.h
|
||||
maintenance/SecureErase.h
|
||||
maintenance/SdCardRecovery.h
|
||||
security/EncryptedVault.h
|
||||
security/Fido2Manager.h
|
||||
security/BootAuthenticator.h
|
||||
|
||||
@@ -94,9 +94,39 @@ enum class FilesystemType
|
||||
VFAT, // Virtual FAT (long filename extension)
|
||||
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)
|
||||
NFS,
|
||||
SMB,
|
||||
HDFS, // Hadoop Distributed File System (marker only)
|
||||
SWAP_LINUX,
|
||||
SWAP_SOLARIS,
|
||||
|
||||
|
||||
@@ -124,6 +124,29 @@ Result<FilesystemDetection> FilesystemDetector::detect(
|
||||
if (detectQnx4(readFunc, detection)) 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
|
||||
if (detectFat(readFunc, detection)) return detection;
|
||||
|
||||
@@ -1208,10 +1231,483 @@ const char* FilesystemDetector::filesystemName(FilesystemType type)
|
||||
case FilesystemType::SMB: return "SMB";
|
||||
case FilesystemType::SWAP_LINUX: return "Linux 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::Unallocated: return "Unallocated";
|
||||
}
|
||||
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
|
||||
|
||||
@@ -85,6 +85,29 @@ private:
|
||||
static bool detectRomFs(const DiskReadCallback& readFunc, FilesystemDetection& out);
|
||||
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
|
||||
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:
|
||||
healthText = tr("PASSED - Healthy");
|
||||
healthColor = QColor(0, 180, 0);
|
||||
m_healthIcon->setStyleSheet("background-color: #00b400; border-radius: 24px;");
|
||||
healthColor = QColor(212, 165, 116);
|
||||
m_healthIcon->setStyleSheet("background-color: #d4a574; border-radius: 24px;");
|
||||
break;
|
||||
case SmartStatus::Warning:
|
||||
healthText = tr("WARNING - Issues Detected");
|
||||
@@ -523,7 +523,7 @@ QColor DiagnosticsTab::smartStatusColor(SmartStatus 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::Critical: return QColor(255, 0, 0);
|
||||
default: return QColor(128, 128, 128);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/disk/DiskEnumerator.h"
|
||||
#include "core/disk/RawDiskHandle.h"
|
||||
#include "core/maintenance/SecureErase.h"
|
||||
#include "core/maintenance/SdCardRecovery.h"
|
||||
#include "core/recovery/BootRepair.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
@@ -137,6 +138,60 @@ void MaintenanceTab::setupUi()
|
||||
bootLayout->addWidget(m_bootStatusLabel);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -465,6 +520,149 @@ void MaintenanceTab::onReinstallBootloader()
|
||||
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)
|
||||
{
|
||||
if (bytes >= 1099511627776ULL)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/common/Types.h"
|
||||
#include "core/disk/DiskEnumerator.h"
|
||||
#include "core/maintenance/SecureErase.h"
|
||||
#include "core/maintenance/SdCardRecovery.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <atomic>
|
||||
@@ -40,6 +41,8 @@ private slots:
|
||||
void onRepairGpt();
|
||||
void onRepairBcd();
|
||||
void onReinstallBootloader();
|
||||
void onSdScan();
|
||||
void onSdFix();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
@@ -65,6 +68,16 @@ private:
|
||||
QProgressBar* m_bootProgress = 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
|
||||
SystemDiskSnapshot m_snapshot;
|
||||
std::atomic<bool> m_cancelFlag{false};
|
||||
|
||||
@@ -291,10 +291,10 @@ 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::FAT32: return QColor(180, 145, 120);
|
||||
case FilesystemType::FAT16: return QColor(180, 145, 120);
|
||||
case FilesystemType::FAT12: return QColor(180, 145, 120);
|
||||
case FilesystemType::ExFAT: return QColor(212, 165, 116);
|
||||
case FilesystemType::ReFS: return QColor(32, 74, 135);
|
||||
case FilesystemType::Ext2: 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::UDF: return QColor(85, 87, 83);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -332,6 +355,25 @@ QString DiskMapWidget::filesystemShortName(FilesystemType fs)
|
||||
case FilesystemType::SWAP_LINUX: return QStringLiteral("Swap");
|
||||
case FilesystemType::ISO9660: return QStringLiteral("ISO9660");
|
||||
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::Unknown: return QStringLiteral("Unknown");
|
||||
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