diff --git a/.gitignore b/.gitignore index 34585ac..aed88b5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 7cc4320..d7968e5 100644 --- a/README.md +++ b/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. [![VirusTotal Scan](https://i.ibb.co/TdDKXWb/virustotal.jpg)](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. diff --git a/build_hwdiag_release.bat b/build_hwdiag_release.bat new file mode 100644 index 0000000..f0d7ea5 --- /dev/null +++ b/build_hwdiag_release.bat @@ -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 diff --git a/resources/styles/default.qss b/resources/styles/default.qss index 6d04d13..1aeb809 100644 --- a/resources/styles/default.qss +++ b/resources/styles/default.qss @@ -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; +} diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 007a3f6..1aaa7ca 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -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" + "$/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) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2cdb184..16fd490 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/common/Types.h b/src/core/common/Types.h index 8230678..c4b7627 100644 --- a/src/core/common/Types.h +++ b/src/core/common/Types.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, diff --git a/src/core/disk/FilesystemDetector.cpp b/src/core/disk/FilesystemDetector.cpp index 9b04422..a1f87a5 100644 --- a/src/core/disk/FilesystemDetector.cpp +++ b/src/core/disk/FilesystemDetector.cpp @@ -124,6 +124,29 @@ Result 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(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(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 diff --git a/src/core/disk/FilesystemDetector.h b/src/core/disk/FilesystemDetector.h index 3c36316..56bcff2 100644 --- a/src/core/disk/FilesystemDetector.h +++ b/src/core/disk/FilesystemDetector.h @@ -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 safeRead(const DiskReadCallback& readFunc, uint64_t offset, uint32_t size); }; diff --git a/src/core/maintenance/SdCardRecovery.cpp b/src/core/maintenance/SdCardRecovery.cpp new file mode 100644 index 0000000..e6e17eb --- /dev/null +++ b/src/core/maintenance/SdCardRecovery.cpp @@ -0,0 +1,500 @@ +#include "SdCardRecovery.h" + +#include "../disk/RawDiskHandle.h" +#include "../disk/DiskEnumerator.h" +#include "../common/Logging.h" + +#include +#include + +#include +#include +#include + +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(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 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> SdCardRecovery::detectSdCards() +{ + std::vector 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 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 zeros(static_cast(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(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(diskSize - kCleanSize); + if (SetFilePointerEx(hDisk, offset, nullptr, FILE_BEGIN)) + { + WriteFile(hDisk, zeros.data(), static_cast(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::ok(); +} + +Result 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 buf(bufSize, 0); + auto* layout = reinterpret_cast(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(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(partOffset); + part.PartitionLength.QuadPart = static_cast(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(bufSize), + nullptr, 0, &bytesReturned, nullptr)) + { + return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(), + "IOCTL_DISK_SET_DRIVE_LAYOUT_EX failed"); + } + + return Result::ok(); +} + +Result 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::ok(); +} + +Result 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::ok(); +} + +Result 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::ok(); +} + +} // namespace spw diff --git a/src/core/maintenance/SdCardRecovery.h b/src/core/maintenance/SdCardRecovery.h new file mode 100644 index 0000000..8be30ee --- /dev/null +++ b/src/core/maintenance/SdCardRecovery.h @@ -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 +#include + +#include "../common/Error.h" +#include "../common/Result.h" +#include "../common/Types.h" +#include "../disk/DiskEnumerator.h" + +#include +#include +#include +#include + +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; + +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> detectSdCards(); + + // Analyze a specific disk to determine its SD card status + static Result analyzeDisk(DiskId diskId); + + // Fix an SD card: clean partition table, create new partition, and format + static Result fixCard(DiskId diskId, const SdFixConfig& config, + SdProgressCallback progress = nullptr); + +private: + // Clean the disk by creating a fresh MBR or GPT + static Result cleanDisk(RawDiskHandle& disk, uint64_t diskSize); + + // Create a single partition spanning the entire disk + static Result createPartition(RawDiskHandle& disk, uint64_t diskSize, + uint32_t sectorSize, FilesystemType fs); + + // Quick format the new partition + static Result formatPartition(DiskId diskId, FilesystemType fs, + const std::wstring& label); + + // Force Windows to rescan the disk for new partitions + static Result 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 diff --git a/src/ui/tabs/DiagnosticsTab.cpp b/src/ui/tabs/DiagnosticsTab.cpp index bd647aa..6ab4a37 100644 --- a/src/ui/tabs/DiagnosticsTab.cpp +++ b/src/ui/tabs/DiagnosticsTab.cpp @@ -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); diff --git a/src/ui/tabs/MaintenanceTab.cpp b/src/ui/tabs/MaintenanceTab.cpp index cf9ac91..e659aa2 100644 --- a/src/ui/tabs/MaintenanceTab.cpp +++ b/src/ui/tabs/MaintenanceTab.cpp @@ -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 @@ -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) diff --git a/src/ui/tabs/MaintenanceTab.h b/src/ui/tabs/MaintenanceTab.h index b3f2b6a..32eae86 100644 --- a/src/ui/tabs/MaintenanceTab.h +++ b/src/ui/tabs/MaintenanceTab.h @@ -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 #include @@ -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 m_detectedCards; + // Data SystemDiskSnapshot m_snapshot; std::atomic m_cancelFlag{false}; diff --git a/src/ui/widgets/DiskMapWidget.cpp b/src/ui/widgets/DiskMapWidget.cpp index 41d0740..ea7a7be 100644 --- a/src/ui/widgets/DiskMapWidget.cpp +++ b/src/ui/widgets/DiskMapWidget.cpp @@ -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"); diff --git a/third_party/hwdiag/lib/spw_hwdiag.lib b/third_party/hwdiag/lib/spw_hwdiag.lib index ffdd46b..96ec5d5 100644 Binary files a/third_party/hwdiag/lib/spw_hwdiag.lib and b/third_party/hwdiag/lib/spw_hwdiag.lib differ diff --git a/virustotal.jpg b/virustotal.jpg new file mode 100644 index 0000000..e86af7a Binary files /dev/null and b/virustotal.jpg differ