v1.2.0 — Linux Flasher, Kali Creator, fixed flashing & counterfeit detection

New features:
- Linux Flasher tab: download+decompress+flash pipeline for RPi OS, Ubuntu,
  Debian, Fedora, Kali, DietPi, Alpine, Arch ARM with built-in image catalog
- Kali Creator tab: 4 sub-tabs for USB/SD, VM creation, Docker/Podman
  container pulls, and cloud image downloads
- DownloadManager: async downloads with resume support and speed tracking
- Decompressor: streaming .xz (xz-embedded), .gz (zlib), .zip decompression
- ImageCatalog: built-in catalog + remote fetch from rpi-imager JSON endpoint
- SevenZipExtractor: QProcess wrapper for 7z.exe with progress parsing
- Bundled xz-embedded third-party library for native XZ decompression

Bug fixes:
- Fixed VirtualDisk::flashToDisk() — added FSCTL_ALLOW_EXTENDED_DASD_IO,
  FSCTL_LOCK_VOLUME, FSCTL_DISMOUNT_VOLUME, 32MB aligned buffers,
  WriteFile retry logic (3 attempts), FlushFileBuffers before close
- Fixed VirtualDisk::captureFromDisk() with same improvements
- Fixed ImagingTab::onFlashIso() — now populates targetVolumeLetters from
  disk snapshot so IsoFlasher can properly lock/dismount volumes
- Fixed SD card counterfeit detection false positives:
  - Changed from write-one-read-one to write-all-then-read-all algorithm
    to properly detect NAND address wrapping on fake cards
  - Added volume lock/dismount before probing to prevent filesystem
    interference (journal writes, metadata updates)
  - Added FSCTL_ALLOW_EXTENDED_DASD_IO for probes near end of disk
  - Fixed overly aggressive vendor string check — USB card readers
    legitimately report "USB"/"Mass Storage", no longer flagged
  - Added handle re-open between write and verify phases to defeat
    USB reader hardware cache
- README: documented how to unlock the secret menu, added new feature docs
This commit is contained in:
DigiJ
2026-03-12 12:51:35 -07:00
parent 9e0af78932
commit e3cf246d8c
46 changed files with 11128 additions and 154 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
project(SetecPartitionWizard VERSION 1.0.0 LANGUAGES CXX) project(SetecPartitionWizard VERSION 1.0.0 LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -11,7 +11,7 @@ set(CMAKE_AUTOUIC ON)
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
# Find Qt6 # Find Qt6
find_package(Qt6 REQUIRED COMPONENTS Widgets Core) find_package(Qt6 REQUIRED COMPONENTS Widgets Core Network)
# CMake helpers # CMake helpers
include(cmake/CompilerWarnings.cmake) include(cmake/CompilerWarnings.cmake)

View File

@@ -59,6 +59,19 @@ RVZ/WIA (Dolphin Wii), WUA (Cemu Wii U), WBFS (Wii Backup), NRG (Nero), MDF (Alc
- **Disk cloning** — sector-by-sector or smart clone (skips unallocated space) - **Disk cloning** — sector-by-sector or smart clone (skips unallocated space)
- **Checksum verification** — SHA-256, MD5, and CRC32 on all imaging operations - **Checksum verification** — SHA-256, MD5, and CRC32 on all imaging operations
### Linux Flasher
- **Built-in image catalog** — Raspberry Pi OS, Ubuntu, Debian, Fedora, Kali, DietPi, Alpine, Arch Linux ARM
- **One-click download & flash** — download, decompress, and flash in a single pipeline
- **Decompression support** — .xz (streaming via xz-embedded), .gz (zlib), .zip, .7z (via 7-Zip)
- **Resume support** — resume interrupted downloads automatically
- **Custom images** — flash any local .img/.iso file or paste a download URL
### Kali Creator
- **USB/SD Card** — download and flash any Kali variant (Installer, Live, ARM) with optional persistence partition
- **Virtual Machine** — create QCOW2/VMDK/VDI/VHDX disks or download pre-built VMware/VirtualBox/Hyper-V VMs
- **Containers** — pull `kalilinux/kali-rolling` via Docker or Podman with live log output
- **Cloud Image** — download official Kali cloud images (Raw, QCOW2, OVA)
### S.M.A.R.T. & Diagnostics ### S.M.A.R.T. & Diagnostics
- **S.M.A.R.T. monitoring** for both ATA and NVMe drives — read all attributes, thresholds, and health status - **S.M.A.R.T. monitoring** for both ATA and NVMe drives — read all attributes, thresholds, and health status
- **Disk benchmarks** — sequential and random read/write, queue depth 1 and 32 - **Disk benchmarks** — sequential and random read/write, queue depth 1 and 32
@@ -153,12 +166,22 @@ src/
--- ---
## A Note for the Curious ## Unlocking the Secret Menu
There's a hidden pentesting/diagnostics tab behind a puzzle. Two ways to unlock it:
### The Easy Way
1. Go to **Tools → Unlock Features...**
2. Select the file `tools/unlock.key` (included in the repo)
### The Hard Way
1. Press **F5** while the app is running
2. Survive the AstroChicken calibration
3. Complete the Vohaul telemetry sequence
4. Pass the Arnoid sensor authentication gate
> *"Don't forget to look UP UP at space."* > *"Don't forget to look UP UP at space."*
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. A riddle. A dark void. And a very particular file that ships with every build — one that looks like garbage in a hex editor, but says something in a text editor. Find it. Read it. Then find the file that only *your* build can produce.
Those who grew up cleaning floors on the SCS Deepship 86 might feel right at home. The janitor always did have a knack for finding hidden things. Those who grew up cleaning floors on the SCS Deepship 86 might feel right at home. The janitor always did have a knack for finding hidden things.

View File

@@ -1,10 +1,12 @@
# GenerateKey.cmake — Build-time 1337-bit key generation # GenerateKey.cmake — Build-time 1337-bit key generation
# Compiles and runs the keygen tool to produce: # Compiles and runs the keygen tool to produce:
# 1. generated/EmbeddedKey.h (compiled into the app) # 1. generated/EmbeddedKey.h (compiled into the app)
# 2. resources/garbage.xtx (distributed read-only alongside the app) # 2. resources/garbage.xtx (distributed alongside the app)
#
# garbage.xtx is regenerated on EVERY build — it is unique to each build
# and must match the EmbeddedKey.h baked into that specific binary.
set(KEYGEN_SOURCE "${CMAKE_SOURCE_DIR}/tools/keygen.cpp") set(KEYGEN_SOURCE "${CMAKE_SOURCE_DIR}/tools/keygen.cpp")
set(KEYGEN_BINARY "${CMAKE_BINARY_DIR}/tools/keygen${CMAKE_EXECUTABLE_SUFFIX}")
set(GENERATED_DIR "${CMAKE_BINARY_DIR}/generated") set(GENERATED_DIR "${CMAKE_BINARY_DIR}/generated")
set(GENERATED_KEY_HEADER "${GENERATED_DIR}/EmbeddedKey.h") set(GENERATED_KEY_HEADER "${GENERATED_DIR}/EmbeddedKey.h")
set(GARBAGE_XTX "${CMAKE_SOURCE_DIR}/resources/garbage.xtx") set(GARBAGE_XTX "${CMAKE_SOURCE_DIR}/resources/garbage.xtx")
@@ -12,7 +14,7 @@ set(GARBAGE_XTX "${CMAKE_SOURCE_DIR}/resources/garbage.xtx")
file(MAKE_DIRECTORY ${GENERATED_DIR}) file(MAKE_DIRECTORY ${GENERATED_DIR})
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/tools") file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/tools")
# Step 1: Compile keygen tool (runs on host at configure/build time) # Step 1: Compile keygen tool
add_executable(spw_keygen EXCLUDE_FROM_ALL "${KEYGEN_SOURCE}") add_executable(spw_keygen EXCLUDE_FROM_ALL "${KEYGEN_SOURCE}")
if(WIN32) if(WIN32)
target_link_libraries(spw_keygen PRIVATE bcrypt) target_link_libraries(spw_keygen PRIVATE bcrypt)
@@ -21,13 +23,12 @@ set_target_properties(spw_keygen PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools"
) )
# Step 2: Run keygen to produce header + garbage.xtx # Step 2: Always-run target — regenerates both EmbeddedKey.h and garbage.xtx
add_custom_command( # every build so the key and .xtx file are always in sync with the binary.
OUTPUT "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}" # Using add_custom_target (not add_custom_command) so it runs unconditionally.
COMMAND spw_keygen "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}" add_custom_target(generate_key ALL
DEPENDS spw_keygen "${KEYGEN_SOURCE}" COMMAND $<TARGET_FILE:spw_keygen> "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}"
COMMENT "Generating 1337-bit cryptographic key and garbage.xtx..." DEPENDS spw_keygen
COMMENT "Generating build-specific 1337-bit key and garbage.xtx..."
VERBATIM VERBATIM
) )
add_custom_target(generate_key DEPENDS "${GENERATED_KEY_HEADER}" "${GARBAGE_XTX}")

View File

@@ -7,6 +7,7 @@
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QMessageBox> #include <QMessageBox>
#include <QPalette>
#include <QStandardPaths> #include <QStandardPaths>
#include <QString> #include <QString>
@@ -52,6 +53,49 @@ static bool relaunchAsAdmin()
#endif #endif
} }
static void applyDarkPalette(QApplication& app)
{
// Set the application palette so every widget — including QProgressDialog,
// QMessageBox, QInputDialog, and other system-rendered popups — gets the
// correct dark background and white text regardless of stylesheets.
QPalette p;
// Background roles
p.setColor(QPalette::Window, QColor(0x1e, 0x1e, 0x2e));
p.setColor(QPalette::WindowText, QColor(0xff, 0xff, 0xff));
p.setColor(QPalette::Base, QColor(0x18, 0x18, 0x25));
p.setColor(QPalette::AlternateBase, QColor(0x1e, 0x1e, 0x2e));
p.setColor(QPalette::ToolTipBase, QColor(0x31, 0x32, 0x44));
p.setColor(QPalette::ToolTipText, QColor(0xff, 0xff, 0xff));
p.setColor(QPalette::PlaceholderText, QColor(0x6e, 0x70, 0x80));
// Text roles
p.setColor(QPalette::Text, QColor(0xff, 0xff, 0xff));
p.setColor(QPalette::BrightText, QColor(0xff, 0xff, 0xff));
// Button roles
p.setColor(QPalette::Button, QColor(0x45, 0x47, 0x5a));
p.setColor(QPalette::ButtonText, QColor(0xff, 0xff, 0xff));
// Selection
p.setColor(QPalette::Highlight, QColor(0x45, 0x47, 0x5a));
p.setColor(QPalette::HighlightedText, QColor(0xff, 0xff, 0xff));
// Links
p.setColor(QPalette::Link, QColor(0xd4, 0xa5, 0x74));
p.setColor(QPalette::LinkVisited, QColor(0xb0, 0x80, 0x50));
// Disabled state — dimmed text, same dark backgrounds
p.setColor(QPalette::Disabled, QPalette::WindowText, QColor(0x6e, 0x70, 0x80));
p.setColor(QPalette::Disabled, QPalette::Text, QColor(0x6e, 0x70, 0x80));
p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(0x6e, 0x70, 0x80));
p.setColor(QPalette::Disabled, QPalette::Base, QColor(0x2a, 0x2a, 0x3a));
p.setColor(QPalette::Disabled, QPalette::Button, QColor(0x2a, 0x2a, 0x3a));
p.setColor(QPalette::Disabled, QPalette::Highlight, QColor(0x31, 0x32, 0x44));
app.setPalette(p);
}
static void applyStylesheet(QApplication& app) static void applyStylesheet(QApplication& app)
{ {
QFile styleFile(":/styles/default.qss"); QFile styleFile(":/styles/default.qss");
@@ -108,7 +152,10 @@ int main(int argc, char* argv[])
spw::log::warn("Continuing without admin privileges — write operations will fail"); spw::log::warn("Continuing without admin privileges — write operations will fail");
} }
// Apply stylesheet // Apply dark palette first (catches system-rendered widgets like QProgressDialog,
// QMessageBox internals that ignore stylesheets on Windows), then apply
// the stylesheet on top for fine-grained visual control.
applyDarkPalette(app);
applyStylesheet(app); applyStylesheet(app);
// Show main window // Show main window

View File

@@ -35,10 +35,18 @@ set(CORE_SOURCES
imaging/ImageCreator.cpp imaging/ImageCreator.cpp
imaging/ImageRestorer.cpp imaging/ImageRestorer.cpp
imaging/IsoFlasher.cpp imaging/IsoFlasher.cpp
imaging/VirtualDisk.cpp
imaging/Decompressor.cpp
imaging/ImageCatalog.cpp
imaging/SevenZipExtractor.cpp
# Networking
net/DownloadManager.cpp
# Maintenance # Maintenance
maintenance/SecureErase.cpp maintenance/SecureErase.cpp
maintenance/SdCardRecovery.cpp maintenance/SdCardRecovery.cpp
maintenance/SdCardAnalyzer.cpp
# Security # Security
security/EncryptedVault.cpp security/EncryptedVault.cpp
@@ -76,6 +84,11 @@ set(CORE_HEADERS
imaging/ImageCreator.h imaging/ImageCreator.h
imaging/ImageRestorer.h imaging/ImageRestorer.h
imaging/IsoFlasher.h imaging/IsoFlasher.h
imaging/VirtualDisk.h
imaging/Decompressor.h
imaging/ImageCatalog.h
imaging/SevenZipExtractor.h
net/DownloadManager.h
maintenance/SecureErase.h maintenance/SecureErase.h
maintenance/SdCardRecovery.h maintenance/SdCardRecovery.h
security/EncryptedVault.h security/EncryptedVault.h
@@ -83,7 +96,16 @@ set(CORE_HEADERS
security/BootAuthenticator.h security/BootAuthenticator.h
) )
add_library(spw_core STATIC ${CORE_SOURCES} ${CORE_HEADERS}) # xz-embedded (streaming XZ decompression)
set(XZ_EMBEDDED_DIR "${CMAKE_SOURCE_DIR}/third_party/xz-embedded")
set(XZ_EMBEDDED_SOURCES
${XZ_EMBEDDED_DIR}/xz_crc32.c
${XZ_EMBEDDED_DIR}/xz_crc64.c
${XZ_EMBEDDED_DIR}/xz_dec_stream.c
${XZ_EMBEDDED_DIR}/xz_dec_lzma2.c
)
add_library(spw_core STATIC ${CORE_SOURCES} ${CORE_HEADERS} ${XZ_EMBEDDED_SOURCES})
# Depend on build-time key generation # Depend on build-time key generation
add_dependencies(spw_core generate_key) add_dependencies(spw_core generate_key)
@@ -93,8 +115,13 @@ target_include_directories(spw_core PUBLIC
${CMAKE_BINARY_DIR}/generated ${CMAKE_BINARY_DIR}/generated
) )
target_include_directories(spw_core PRIVATE
${XZ_EMBEDDED_DIR}
)
target_link_libraries(spw_core PUBLIC target_link_libraries(spw_core PUBLIC
Qt6::Core Qt6::Core
Qt6::Network
) )
# Windows system libraries # Windows system libraries

View File

@@ -57,6 +57,7 @@ enum class ErrorCode
ImageWriteError, ImageWriteError,
IsoParseError, IsoParseError,
InsufficientDiskSpace, InsufficientDiskSpace,
DecompressionFailed,
// Security // Security
Fido2DeviceNotFound, Fido2DeviceNotFound,

View File

@@ -0,0 +1,524 @@
#include "Decompressor.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QtZlib/zlib.h>
// xz-embedded header — provided at third_party/xz-embedded/xz.h
#include "xz.h"
#include <cstring>
#include <vector>
namespace spw
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static constexpr int kChunkSize = 4 * 1024 * 1024; // 4 MB processing chunks
static constexpr int kZipLocalFileHeaderSig = 0x04034b50;
// ---------------------------------------------------------------------------
// decompressXz — xz-embedded streaming decompression
// ---------------------------------------------------------------------------
Result<void> Decompressor::decompressXz(const QString& inputPath,
const QString& outputPath,
std::function<void(qint64, qint64)> progress)
{
QFile inFile(inputPath);
if (!inFile.open(QIODevice::ReadOnly)) {
return ErrorInfo::fromCode(ErrorCode::FileNotFound,
"Cannot open input file: " + inputPath.toStdString());
}
QFile outFile(outputPath);
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create output file: " + outputPath.toStdString());
}
const qint64 totalSize = inFile.size();
// Initialize CRC tables required by xz-embedded
xz_crc32_init();
// Allocate decoder — XZ_DYNALLOC lets xz-embedded malloc as needed,
// dictionary limit set to 64 MB (1 << 26).
struct xz_dec* decoder = xz_dec_init(XZ_DYNALLOC, 1 << 26);
if (!decoder) {
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"Failed to initialize xz decoder");
}
std::vector<uint8_t> inBuf(kChunkSize);
std::vector<uint8_t> outBuf(kChunkSize);
struct xz_buf buf;
std::memset(&buf, 0, sizeof(buf));
buf.in = inBuf.data();
buf.out = outBuf.data();
buf.out_size = outBuf.size();
qint64 totalRead = 0;
enum xz_ret ret = XZ_OK;
while (ret == XZ_OK) {
// Refill input buffer if exhausted
if (buf.in_pos == buf.in_size) {
qint64 bytesRead = inFile.read(reinterpret_cast<char*>(inBuf.data()),
static_cast<qint64>(inBuf.size()));
if (bytesRead < 0) {
xz_dec_end(decoder);
return ErrorInfo::fromCode(ErrorCode::ImageReadError,
"Read error on input file: " + inputPath.toStdString());
}
buf.in_size = static_cast<size_t>(bytesRead);
buf.in_pos = 0;
totalRead += bytesRead;
}
buf.out_pos = 0;
ret = xz_dec_run(decoder, &buf);
// Write whatever output was produced
if (buf.out_pos > 0) {
qint64 written = outFile.write(reinterpret_cast<const char*>(outBuf.data()),
static_cast<qint64>(buf.out_pos));
if (written != static_cast<qint64>(buf.out_pos)) {
xz_dec_end(decoder);
return ErrorInfo::fromCode(ErrorCode::ImageWriteError,
"Write error on output file: " + outputPath.toStdString());
}
}
if (progress && totalSize > 0) {
progress(totalRead, totalSize);
}
}
xz_dec_end(decoder);
if (ret != XZ_STREAM_END) {
outFile.remove();
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"XZ decompression failed (error code " + std::to_string(static_cast<int>(ret)) + ")");
}
outFile.flush();
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// decompressGz — zlib inflate with gzip wrapper
// ---------------------------------------------------------------------------
Result<void> Decompressor::decompressGz(const QString& inputPath,
const QString& outputPath,
std::function<void(qint64, qint64)> progress)
{
QFile inFile(inputPath);
if (!inFile.open(QIODevice::ReadOnly)) {
return ErrorInfo::fromCode(ErrorCode::FileNotFound,
"Cannot open input file: " + inputPath.toStdString());
}
QFile outFile(outputPath);
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create output file: " + outputPath.toStdString());
}
const qint64 totalSize = inFile.size();
z_stream strm;
std::memset(&strm, 0, sizeof(strm));
// 15 + 16 = enable gzip decoding via zlib
int zret = inflateInit2(&strm, 15 + 16);
if (zret != Z_OK) {
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"inflateInit2 failed: " + std::string(strm.msg ? strm.msg : "unknown error"));
}
std::vector<Bytef> inBuf(kChunkSize);
std::vector<Bytef> outBuf(kChunkSize);
qint64 totalRead = 0;
while (true) {
qint64 bytesRead = inFile.read(reinterpret_cast<char*>(inBuf.data()),
static_cast<qint64>(inBuf.size()));
if (bytesRead < 0) {
inflateEnd(&strm);
return ErrorInfo::fromCode(ErrorCode::ImageReadError,
"Read error on input file: " + inputPath.toStdString());
}
if (bytesRead == 0) {
break; // EOF
}
totalRead += bytesRead;
strm.avail_in = static_cast<uInt>(bytesRead);
strm.next_in = inBuf.data();
// Inflate all available input
do {
strm.avail_out = static_cast<uInt>(outBuf.size());
strm.next_out = outBuf.data();
zret = inflate(&strm, Z_NO_FLUSH);
if (zret == Z_STREAM_ERROR || zret == Z_DATA_ERROR ||
zret == Z_MEM_ERROR || zret == Z_NEED_DICT) {
std::string errMsg = strm.msg ? strm.msg : "inflate error";
inflateEnd(&strm);
outFile.remove();
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"Gzip decompression failed: " + errMsg);
}
uInt have = static_cast<uInt>(outBuf.size()) - strm.avail_out;
if (have > 0) {
qint64 written = outFile.write(reinterpret_cast<const char*>(outBuf.data()),
static_cast<qint64>(have));
if (written != static_cast<qint64>(have)) {
inflateEnd(&strm);
return ErrorInfo::fromCode(ErrorCode::ImageWriteError,
"Write error on output file: " + outputPath.toStdString());
}
}
} while (strm.avail_out == 0);
if (progress && totalSize > 0) {
progress(totalRead, totalSize);
}
if (zret == Z_STREAM_END) {
break;
}
}
inflateEnd(&strm);
outFile.flush();
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// decompressZip — basic zip extraction using zlib raw inflate
//
// Parses local file headers directly (PK\x03\x04) and inflates each stored
// or deflated entry. This avoids a minizip dependency while handling the
// vast majority of zip files encountered in disk-imaging workflows.
// ---------------------------------------------------------------------------
namespace
{
struct ZipLocalHeader
{
uint16_t versionNeeded;
uint16_t flags;
uint16_t method;
uint16_t modTime;
uint16_t modDate;
uint32_t crc32;
uint32_t compressedSize;
uint32_t uncompressedSize;
uint16_t nameLen;
uint16_t extraLen;
};
template <typename T>
T readLE(const uint8_t* p)
{
T val = 0;
for (size_t i = 0; i < sizeof(T); ++i)
val |= static_cast<T>(p[i]) << (8 * i);
return val;
}
bool parseLocalHeader(const uint8_t* data, ZipLocalHeader& hdr)
{
// Skip past 4-byte signature already verified by caller
hdr.versionNeeded = readLE<uint16_t>(data + 4);
hdr.flags = readLE<uint16_t>(data + 6);
hdr.method = readLE<uint16_t>(data + 8);
hdr.modTime = readLE<uint16_t>(data + 10);
hdr.modDate = readLE<uint16_t>(data + 12);
hdr.crc32 = readLE<uint32_t>(data + 14);
hdr.compressedSize = readLE<uint32_t>(data + 18);
hdr.uncompressedSize = readLE<uint32_t>(data + 22);
hdr.nameLen = readLE<uint16_t>(data + 26);
hdr.extraLen = readLE<uint16_t>(data + 28);
return true;
}
} // anonymous namespace
Result<void> Decompressor::decompressZip(const QString& inputPath,
const QString& outputDir,
std::function<void(qint64, qint64)> progress)
{
QFile inFile(inputPath);
if (!inFile.open(QIODevice::ReadOnly)) {
return ErrorInfo::fromCode(ErrorCode::FileNotFound,
"Cannot open zip file: " + inputPath.toStdString());
}
const qint64 totalSize = inFile.size();
QDir outDirObj(outputDir);
if (!outDirObj.mkpath(".")) {
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create output directory: " + outputDir.toStdString());
}
qint64 bytesProcessed = 0;
while (bytesProcessed < totalSize) {
// Read local file header (30 bytes minimum)
uint8_t headerBuf[30];
inFile.seek(bytesProcessed);
qint64 hdrRead = inFile.read(reinterpret_cast<char*>(headerBuf), 30);
if (hdrRead < 30) {
break; // No more entries
}
uint32_t sig = readLE<uint32_t>(headerBuf);
if (sig != static_cast<uint32_t>(kZipLocalFileHeaderSig)) {
// Reached central directory or end-of-central-dir — we are done
break;
}
ZipLocalHeader hdr;
parseLocalHeader(headerBuf, hdr);
// Read filename
QByteArray nameBuf(hdr.nameLen, '\0');
inFile.read(nameBuf.data(), hdr.nameLen);
QString entryName = QString::fromUtf8(nameBuf);
// Skip extra field
inFile.skip(hdr.extraLen);
qint64 dataOffset = bytesProcessed + 30 + hdr.nameLen + hdr.extraLen;
// Determine compressed size — handle data descriptor (bit 3 of flags)
uint32_t compSize = hdr.compressedSize;
uint32_t uncompSize = hdr.uncompressedSize;
// If the entry name ends with '/', it is a directory
if (entryName.endsWith('/') || entryName.endsWith('\\')) {
outDirObj.mkpath(entryName);
bytesProcessed = dataOffset + compSize;
continue;
}
// Build output path, ensuring parent directories exist
QString outPath = outDirObj.filePath(entryName);
QFileInfo outInfo(outPath);
outInfo.dir().mkpath(".");
QFile outFile(outPath);
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create file: " + outPath.toStdString());
}
inFile.seek(dataOffset);
if (hdr.method == 0) {
// Stored (no compression) — just copy bytes
qint64 remaining = compSize;
std::vector<char> buf(kChunkSize);
while (remaining > 0) {
qint64 toRead = std::min(remaining, static_cast<qint64>(buf.size()));
qint64 got = inFile.read(buf.data(), toRead);
if (got <= 0) {
return ErrorInfo::fromCode(ErrorCode::ImageReadError,
"Read error extracting stored entry: " + entryName.toStdString());
}
if (outFile.write(buf.data(), got) != got) {
return ErrorInfo::fromCode(ErrorCode::ImageWriteError,
"Write error extracting: " + entryName.toStdString());
}
remaining -= got;
}
} else if (hdr.method == 8) {
// Deflated — use zlib raw inflate (windowBits = -15 for raw deflate)
z_stream strm;
std::memset(&strm, 0, sizeof(strm));
int zret = inflateInit2(&strm, -15); // raw deflate
if (zret != Z_OK) {
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"inflateInit2 failed for zip entry: " + entryName.toStdString());
}
std::vector<Bytef> inBuf(kChunkSize);
std::vector<Bytef> outBuf(kChunkSize);
qint64 compRemaining = compSize;
while (compRemaining > 0) {
qint64 toRead = std::min(compRemaining, static_cast<qint64>(inBuf.size()));
qint64 got = inFile.read(reinterpret_cast<char*>(inBuf.data()), toRead);
if (got <= 0) {
inflateEnd(&strm);
return ErrorInfo::fromCode(ErrorCode::ImageReadError,
"Read error inflating zip entry: " + entryName.toStdString());
}
compRemaining -= got;
strm.avail_in = static_cast<uInt>(got);
strm.next_in = inBuf.data();
do {
strm.avail_out = static_cast<uInt>(outBuf.size());
strm.next_out = outBuf.data();
zret = inflate(&strm, Z_NO_FLUSH);
if (zret == Z_DATA_ERROR || zret == Z_MEM_ERROR) {
std::string errMsg = strm.msg ? strm.msg : "inflate error";
inflateEnd(&strm);
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"Deflate error in zip entry '" + entryName.toStdString() + "': " + errMsg);
}
uInt have = static_cast<uInt>(outBuf.size()) - strm.avail_out;
if (have > 0) {
if (outFile.write(reinterpret_cast<const char*>(outBuf.data()),
static_cast<qint64>(have)) != static_cast<qint64>(have)) {
inflateEnd(&strm);
return ErrorInfo::fromCode(ErrorCode::ImageWriteError,
"Write error inflating zip entry: " + entryName.toStdString());
}
}
} while (strm.avail_out == 0);
if (zret == Z_STREAM_END) {
break;
}
}
inflateEnd(&strm);
} else {
// Unsupported compression method
outFile.close();
outFile.remove();
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"Unsupported zip compression method " + std::to_string(hdr.method)
+ " for entry: " + entryName.toStdString());
}
outFile.flush();
outFile.close();
// Handle data descriptor if bit 3 is set
qint64 nextOffset = dataOffset + compSize;
if (hdr.flags & 0x08) {
// Data descriptor follows: optionally 4-byte sig + crc32 + compSize + uncompSize
inFile.seek(nextOffset);
uint8_t ddBuf[16];
qint64 ddRead = inFile.read(reinterpret_cast<char*>(ddBuf), 16);
if (ddRead >= 12) {
uint32_t ddSig = readLE<uint32_t>(ddBuf);
if (ddSig == 0x08074b50) {
// Signature present — descriptor is 16 bytes
nextOffset += 16;
} else {
// No signature — descriptor is 12 bytes
nextOffset += 12;
}
}
}
bytesProcessed = nextOffset;
if (progress && totalSize > 0) {
progress(bytesProcessed, totalSize);
}
}
return Result<void>::ok();
}
// ---------------------------------------------------------------------------
// decompressAuto — detect format by extension and dispatch
// ---------------------------------------------------------------------------
Result<QString> Decompressor::decompressAuto(const QString& inputPath,
const QString& outputDir,
std::function<void(qint64, qint64)> progress)
{
QFileInfo info(inputPath);
QString ext = info.suffix().toLower();
QDir dir(outputDir);
if (!dir.mkpath(".")) {
return ErrorInfo::fromCode(ErrorCode::FileCreateFailed,
"Cannot create output directory: " + outputDir.toStdString());
}
if (ext == "xz") {
QString outName = decompressedName(info.fileName());
QString outPath = dir.filePath(outName);
auto result = decompressXz(inputPath, outPath, progress);
if (result.isError())
return result.error();
return Result<QString>(outPath);
}
if (ext == "gz") {
QString outName = decompressedName(info.fileName());
QString outPath = dir.filePath(outName);
auto result = decompressGz(inputPath, outPath, progress);
if (result.isError())
return result.error();
return Result<QString>(outPath);
}
if (ext == "zip") {
auto result = decompressZip(inputPath, outputDir, progress);
if (result.isError())
return result.error();
return Result<QString>(outputDir);
}
return ErrorInfo::fromCode(ErrorCode::DecompressionFailed,
"Unsupported compression format: ." + ext.toStdString());
}
// ---------------------------------------------------------------------------
// isCompressed — check extension
// ---------------------------------------------------------------------------
bool Decompressor::isCompressed(const QString& path)
{
QString ext = QFileInfo(path).suffix().toLower();
return (ext == "xz" || ext == "gz" || ext == "zip" || ext == "7z");
}
// ---------------------------------------------------------------------------
// decompressedName — strip compression extension
// ---------------------------------------------------------------------------
QString Decompressor::decompressedName(const QString& path)
{
QFileInfo info(path);
QString name = info.fileName();
QString ext = info.suffix().toLower();
if (ext == "xz" || ext == "gz" || ext == "zip" || ext == "7z") {
// Remove the last extension: "file.img.xz" -> "file.img"
int lastDot = name.lastIndexOf('.');
if (lastDot > 0) {
return name.left(lastDot);
}
}
return name;
}
} // namespace spw

View File

@@ -0,0 +1,45 @@
#pragma once
#include "../common/Result.h"
#include "../common/Error.h"
#include <QString>
#include <functional>
namespace spw
{
/// Streaming decompression utility for .xz, .gz, and .zip files.
class Decompressor
{
public:
Decompressor() = delete;
/// Decompress an .xz file using xz-embedded C API.
static Result<void> decompressXz(const QString& inputPath,
const QString& outputPath,
std::function<void(qint64, qint64)> progress = nullptr);
/// Decompress a .gz file using zlib inflate.
static Result<void> decompressGz(const QString& inputPath,
const QString& outputPath,
std::function<void(qint64, qint64)> progress = nullptr);
/// Extract a .zip archive using zlib (basic single-stream extraction).
static Result<void> decompressZip(const QString& inputPath,
const QString& outputDir,
std::function<void(qint64, qint64)> progress = nullptr);
/// Auto-detect format by extension and decompress. Returns output path on success.
static Result<QString> decompressAuto(const QString& inputPath,
const QString& outputDir,
std::function<void(qint64, qint64)> progress = nullptr);
/// Check if a file has a recognized compression extension (.xz, .gz, .zip, .7z).
static bool isCompressed(const QString& path);
/// Strip compression extension (e.g. "file.img.xz" -> "file.img").
static QString decompressedName(const QString& path);
};
} // namespace spw

View File

@@ -0,0 +1,338 @@
#include "ImageCatalog.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QSet>
#include <algorithm>
namespace spw {
ImageCatalog::ImageCatalog(QObject* parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
{
}
QList<ImageEntry> ImageCatalog::builtinImages() const
{
QList<ImageEntry> images;
// --- Raspberry Pi ---
images.append({
QStringLiteral("Raspberry Pi OS Lite (64-bit)"),
QStringLiteral("A port of Debian Bookworm with no desktop environment, 64-bit kernel and userspace"),
QStringLiteral("Raspberry Pi"),
QStringLiteral("2024-11-19"),
QUrl(QStringLiteral("https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz")),
QStringLiteral("3d844d09a803524b15a3c1b06ee7882d6f8e57a95b7c1bc32773edf44b0b0e7f"),
static_cast<qint64>(503'316'480), // ~480 MB compressed
static_cast<qint64>(2'684'354'560), // ~2.5 GB extracted
true,
QStringLiteral(".xz")
});
images.append({
QStringLiteral("Raspberry Pi OS Lite (32-bit)"),
QStringLiteral("A port of Debian Bookworm with no desktop environment, 32-bit kernel and userspace"),
QStringLiteral("Raspberry Pi"),
QStringLiteral("2024-11-19"),
QUrl(QStringLiteral("https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-11-19/2024-11-19-raspios-bookworm-armhf-lite.img.xz")),
QStringLiteral(""),
static_cast<qint64>(471'859'200), // ~450 MB compressed
static_cast<qint64>(2'147'483'648), // ~2 GB extracted
true,
QStringLiteral(".xz")
});
// --- Ubuntu ---
images.append({
QStringLiteral("Ubuntu Server 24.04 LTS (RPi)"),
QStringLiteral("Ubuntu Server 24.04 LTS Noble Numbat for Raspberry Pi (arm64)"),
QStringLiteral("Ubuntu"),
QStringLiteral("24.04.1"),
QUrl(QStringLiteral("https://cdimage.ubuntu.com/releases/24.04.1/release/ubuntu-24.04.1-preinstalled-server-arm64+raspi.img.xz")),
QStringLiteral(""),
static_cast<qint64>(1'153'433'600), // ~1.1 GB compressed
static_cast<qint64>(3'758'096'384), // ~3.5 GB extracted
true,
QStringLiteral(".xz")
});
// --- Debian ---
images.append({
QStringLiteral("Debian 12 Netinst (amd64)"),
QStringLiteral("Debian 12 Bookworm network installer for 64-bit PC (amd64)"),
QStringLiteral("Debian"),
QStringLiteral("12.8.0"),
QUrl(QStringLiteral("https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.8.0-amd64-netinst.iso")),
QStringLiteral(""),
static_cast<qint64>(659'554'304), // ~629 MB
static_cast<qint64>(659'554'304), // ISO, no decompression
false,
QString()
});
// --- Fedora ---
images.append({
QStringLiteral("Fedora Server 40 (aarch64)"),
QStringLiteral("Fedora Server 40 raw disk image for ARM 64-bit systems"),
QStringLiteral("Fedora"),
QStringLiteral("40-1.14"),
QUrl(QStringLiteral("https://download.fedoraproject.org/pub/fedora/linux/releases/40/Server/aarch64/images/Fedora-Server-40-1.14.aarch64.raw.xz")),
QStringLiteral(""),
static_cast<qint64>(838'860'800), // ~800 MB compressed
static_cast<qint64>(7'516'192'768), // ~7 GB extracted
true,
QStringLiteral(".xz")
});
// --- Kali Linux ---
images.append({
QStringLiteral("Kali Linux Installer (amd64)"),
QStringLiteral("Kali Linux full installer ISO for 64-bit PC"),
QStringLiteral("Kali"),
QStringLiteral("2024.3"),
QUrl(QStringLiteral("https://cdimage.kali.org/kali-2024.3/kali-linux-2024.3-installer-amd64.iso")),
QStringLiteral(""),
static_cast<qint64>(4'089'446'400), // ~3.8 GB
static_cast<qint64>(4'089'446'400), // ISO, no decompression
false,
QString()
});
images.append({
QStringLiteral("Kali Linux (RPi ARM64)"),
QStringLiteral("Kali Linux image for Raspberry Pi (64-bit ARM)"),
QStringLiteral("Kali"),
QStringLiteral("2024.3"),
QUrl(QStringLiteral("https://kali.download/arm-images/kali-2024.3/kali-linux-2024.3-raspberry-pi-arm64.img.xz")),
QStringLiteral(""),
static_cast<qint64>(1'610'612'736), // ~1.5 GB compressed
static_cast<qint64>(7'516'192'768), // ~7 GB extracted
true,
QStringLiteral(".xz")
});
// --- DietPi ---
images.append({
QStringLiteral("DietPi (RPi ARMv8 64-bit)"),
QStringLiteral("Highly optimized minimal Debian-based OS for Raspberry Pi 2/3/4/5 (64-bit)"),
QStringLiteral("DietPi"),
QStringLiteral("9.8"),
QUrl(QStringLiteral("https://dietpi.com/downloads/images/DietPi_RPi-ARMv8-Bookworm.img.xz")),
QStringLiteral(""),
static_cast<qint64>(209'715'200), // ~200 MB compressed
static_cast<qint64>(1'073'741'824), // ~1 GB extracted
true,
QStringLiteral(".xz")
});
// --- Alpine Linux ---
images.append({
QStringLiteral("Alpine Linux Extended (RPi aarch64)"),
QStringLiteral("Alpine Linux extended image for Raspberry Pi (aarch64), includes common packages"),
QStringLiteral("Other"),
QStringLiteral("3.20.3"),
QUrl(QStringLiteral("https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/aarch64/alpine-rpi-3.20.3-aarch64.img.gz")),
QStringLiteral(""),
static_cast<qint64>(209'715'200), // ~200 MB compressed
static_cast<qint64>(524'288'000), // ~500 MB extracted
true,
QStringLiteral(".gz")
});
// --- Arch Linux ARM ---
images.append({
QStringLiteral("Arch Linux ARM (RPi 4/5 aarch64)"),
QStringLiteral("Arch Linux ARM root filesystem tarball for Raspberry Pi 4 and 5"),
QStringLiteral("Other"),
QStringLiteral("latest"),
QUrl(QStringLiteral("http://os.archlinuxarm.org/os/ArchLinuxARM-rpi-aarch64-latest.tar.gz")),
QStringLiteral(""),
static_cast<qint64>(419'430'400), // ~400 MB compressed
static_cast<qint64>(2'147'483'648), // ~2 GB extracted
true,
QStringLiteral(".gz")
});
return images;
}
QList<ImageEntry> ImageCatalog::allImages() const
{
QList<ImageEntry> all = builtinImages();
all.append(m_remoteImages);
return all;
}
QStringList ImageCatalog::categories() const
{
QSet<QString> cats;
const auto all = allImages();
for (const auto& img : all) {
cats.insert(img.category);
}
QStringList sorted = cats.values();
sorted.sort(Qt::CaseInsensitive);
return sorted;
}
QList<ImageEntry> ImageCatalog::imagesByCategory(const QString& category) const
{
QList<ImageEntry> result;
const auto all = allImages();
for (const auto& img : all) {
if (img.category.compare(category, Qt::CaseInsensitive) == 0) {
result.append(img);
}
}
return result;
}
void ImageCatalog::fetchRemoteCatalog()
{
QNetworkRequest request(QUrl(QStringLiteral(
"https://downloads.raspberrypi.com/os_list_imagingutility_v4.json")));
request.setHeader(QNetworkRequest::UserAgentHeader,
QStringLiteral("SetecPartitionWizard/1.0"));
QNetworkReply* reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit fetchError(QStringLiteral("Failed to fetch remote catalog: %1")
.arg(reply->errorString()));
return;
}
const QByteArray data = reply->readAll();
if (data.isEmpty()) {
emit fetchError(QStringLiteral("Remote catalog response was empty"));
return;
}
parseRpiImagerJson(data);
emit catalogUpdated();
});
}
void ImageCatalog::parseRpiImagerJson(const QByteArray& data)
{
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
if (doc.isNull()) {
emit fetchError(QStringLiteral("JSON parse error: %1 at offset %2")
.arg(parseError.errorString())
.arg(parseError.offset));
return;
}
m_remoteImages.clear();
const QJsonObject root = doc.object();
const QJsonArray osList = root.value(QStringLiteral("os_list")).toArray();
// Recursively process the os_list; entries can contain nested "subitems"
std::function<void(const QJsonArray&, const QString&)> processArray;
processArray = [&](const QJsonArray& array, const QString& parentCategory) {
for (const QJsonValue& val : array) {
if (!val.isObject())
continue;
const QJsonObject obj = val.toObject();
const QString name = obj.value(QStringLiteral("name")).toString().trimmed();
// If this entry has subitems, recurse into them using this entry's
// name as the category hint
const QJsonArray subitems = obj.value(QStringLiteral("subitems")).toArray();
if (!subitems.isEmpty()) {
processArray(subitems, name);
continue;
}
// Skip entries without a download URL
const QString url = obj.value(QStringLiteral("url")).toString().trimmed();
if (url.isEmpty())
continue;
// Determine category from parent or name
QString category = parentCategory;
if (category.isEmpty()) {
if (name.contains(QStringLiteral("Raspberry Pi"), Qt::CaseInsensitive) ||
name.contains(QStringLiteral("Raspbian"), Qt::CaseInsensitive)) {
category = QStringLiteral("Raspberry Pi");
} else if (name.contains(QStringLiteral("Ubuntu"), Qt::CaseInsensitive)) {
category = QStringLiteral("Ubuntu");
} else if (name.contains(QStringLiteral("Debian"), Qt::CaseInsensitive)) {
category = QStringLiteral("Debian");
} else if (name.contains(QStringLiteral("Fedora"), Qt::CaseInsensitive)) {
category = QStringLiteral("Fedora");
} else if (name.contains(QStringLiteral("Kali"), Qt::CaseInsensitive)) {
category = QStringLiteral("Kali");
} else {
category = QStringLiteral("Other");
}
}
// Determine compression from URL
bool isCompressed = false;
QString compressedExt;
if (url.endsWith(QStringLiteral(".xz"))) {
isCompressed = true;
compressedExt = QStringLiteral(".xz");
} else if (url.endsWith(QStringLiteral(".gz"))) {
isCompressed = true;
compressedExt = QStringLiteral(".gz");
} else if (url.endsWith(QStringLiteral(".zip"))) {
isCompressed = true;
compressedExt = QStringLiteral(".zip");
} else if (url.endsWith(QStringLiteral(".7z"))) {
isCompressed = true;
compressedExt = QStringLiteral(".7z");
}
ImageEntry entry;
entry.name = name;
entry.description = obj.value(QStringLiteral("description")).toString().trimmed();
entry.category = category;
entry.version = obj.value(QStringLiteral("release_date")).toString().trimmed();
entry.downloadUrl = QUrl(url);
entry.sha256 = obj.value(QStringLiteral("extract_sha256")).toString().trimmed();
entry.downloadSize = static_cast<qint64>(
obj.value(QStringLiteral("image_download_size")).toDouble(0.0));
entry.extractedSize = static_cast<qint64>(
obj.value(QStringLiteral("extract_size")).toDouble(0.0));
entry.isCompressed = isCompressed;
entry.compressedExt = compressedExt;
// Skip duplicate entries already in the builtin list
bool duplicate = false;
const auto builtins = builtinImages();
for (const auto& b : builtins) {
if (b.downloadUrl == entry.downloadUrl) {
duplicate = true;
break;
}
}
if (!duplicate)
m_remoteImages.append(entry);
}
};
processArray(osList, QString());
}
} // namespace spw

View File

@@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QList>
#include <QUrl>
#include <QString>
#include <QStringList>
class QNetworkAccessManager;
namespace spw {
struct ImageEntry {
QString name; // e.g. "Raspberry Pi OS (64-bit)"
QString description; // Short description
QString category; // "Raspberry Pi", "Ubuntu", "Debian", "Fedora", "Kali", "DietPi", "Other"
QString version; // e.g. "2024-11-15"
QUrl downloadUrl; // Direct download URL
QString sha256; // Expected hash (empty if unknown)
qint64 downloadSize; // Approximate download size in bytes (0 if unknown)
qint64 extractedSize; // Approximate extracted size (0 if unknown)
bool isCompressed; // Whether the download needs decompression
QString compressedExt; // ".xz", ".gz", ".zip", ".7z"
};
class ImageCatalog : public QObject
{
Q_OBJECT
public:
explicit ImageCatalog(QObject* parent = nullptr);
QList<ImageEntry> builtinImages() const;
QList<ImageEntry> allImages() const;
QStringList categories() const;
QList<ImageEntry> imagesByCategory(const QString& category) const;
void fetchRemoteCatalog();
signals:
void catalogUpdated();
void fetchError(const QString& error);
private:
void parseRpiImagerJson(const QByteArray& data);
QNetworkAccessManager* m_networkManager = nullptr;
QList<ImageEntry> m_remoteImages;
};
} // namespace spw

View File

@@ -0,0 +1,185 @@
#include "SevenZipExtractor.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStandardPaths>
namespace spw {
SevenZipExtractor::SevenZipExtractor(QObject* parent)
: QObject(parent)
{
}
SevenZipExtractor::~SevenZipExtractor()
{
cancel();
}
bool SevenZipExtractor::isAvailable()
{
return !findSevenZip().isEmpty();
}
QString SevenZipExtractor::findSevenZip()
{
// 1. Check PATH via QStandardPaths
QString path = QStandardPaths::findExecutable(QStringLiteral("7z"));
if (!path.isEmpty())
return path;
// 2. Check common installation directories
const QStringList commonPaths = {
QStringLiteral("C:/Program Files/7-Zip/7z.exe"),
QStringLiteral("C:/Program Files (x86)/7-Zip/7z.exe"),
};
for (const QString& candidate : commonPaths) {
if (QFileInfo::exists(candidate))
return candidate;
}
// 3. Check application directory
const QString appDirCandidate = QCoreApplication::applicationDirPath()
+ QStringLiteral("/7z.exe");
if (QFileInfo::exists(appDirCandidate))
return appDirCandidate;
return {};
}
void SevenZipExtractor::extract(const QString& archivePath, const QString& outputDir)
{
if (m_process) {
emit extractionError(tr("An extraction is already in progress."));
return;
}
const QString exe = findSevenZip();
if (exe.isEmpty()) {
emit extractionError(tr("7z.exe not found. Please install 7-Zip or add it to PATH."));
return;
}
if (!QFileInfo::exists(archivePath)) {
emit extractionError(tr("Archive not found: %1").arg(archivePath));
return;
}
// Ensure output directory exists
QDir().mkpath(outputDir);
m_process = new QProcess(this);
connect(m_process, &QProcess::readyReadStandardOutput,
this, &SevenZipExtractor::parseOutput);
connect(m_process, &QProcess::finished, this,
[this, outputDir](int exitCode, QProcess::ExitStatus exitStatus) {
QProcess* proc = m_process;
m_process = nullptr;
if (exitStatus == QProcess::CrashExit) {
emit extractionError(tr("7z process crashed."));
} else if (exitCode != 0) {
const QString errText = QString::fromLocal8Bit(proc->readAllStandardError()).trimmed();
const QString msg = errText.isEmpty()
? tr("7z exited with code %1.").arg(exitCode)
: tr("7z exited with code %1: %2").arg(exitCode).arg(errText);
emit extractionError(msg);
} else {
emit progressChanged(100);
emit extractionComplete(outputDir);
}
proc->deleteLater();
});
connect(m_process, &QProcess::errorOccurred, this,
[this](QProcess::ProcessError error) {
if (!m_process)
return;
QString msg;
switch (error) {
case QProcess::FailedToStart:
msg = tr("Failed to start 7z process.");
break;
case QProcess::Timedout:
msg = tr("7z process timed out.");
break;
default:
msg = tr("7z process error (%1).").arg(static_cast<int>(error));
break;
}
QProcess* proc = m_process;
m_process = nullptr;
proc->deleteLater();
emit extractionError(msg);
});
// Build arguments: x = extract with full paths, -o = output dir, -y = yes to all, -bsp1 = progress to stdout
const QStringList args = {
QStringLiteral("x"),
archivePath,
QStringLiteral("-o%1").arg(outputDir),
QStringLiteral("-y"),
QStringLiteral("-bsp1"),
};
m_process->start(exe, args);
}
void SevenZipExtractor::cancel()
{
if (!m_process)
return;
m_process->kill();
m_process->waitForFinished(3000);
QProcess* proc = m_process;
m_process = nullptr;
proc->deleteLater();
}
bool SevenZipExtractor::isRunning() const
{
return m_process && m_process->state() != QProcess::NotRunning;
}
void SevenZipExtractor::parseOutput()
{
if (!m_process)
return;
// 7z with -bsp1 outputs progress lines like:
// " 42% 12 - somefile.txt"
// " 100%"
// We look for a leading percentage value.
static const QRegularExpression percentRe(QStringLiteral(R"(^\s*(\d{1,3})%)"),
QRegularExpression::MultilineOption);
const QByteArray data = m_process->readAllStandardOutput();
const QString text = QString::fromLocal8Bit(data);
// Process all percentage matches and take the last (most recent) one
int lastPercent = -1;
QRegularExpressionMatchIterator it = percentRe.globalMatch(text);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
bool ok = false;
const int pct = match.captured(1).toInt(&ok);
if (ok && pct >= 0 && pct <= 100)
lastPercent = pct;
}
if (lastPercent >= 0)
emit progressChanged(lastPercent);
}
} // namespace spw

View File

@@ -0,0 +1,35 @@
#pragma once
#include <QObject>
#include <QProcess>
#include <QString>
namespace spw {
class SevenZipExtractor : public QObject
{
Q_OBJECT
public:
explicit SevenZipExtractor(QObject* parent = nullptr);
~SevenZipExtractor() override;
static bool isAvailable();
static QString findSevenZip();
void extract(const QString& archivePath, const QString& outputDir);
void cancel();
bool isRunning() const;
signals:
void progressChanged(int percent);
void extractionComplete(const QString& outputDir);
void extractionError(const QString& error);
private:
void parseOutput();
QProcess* m_process = nullptr;
};
} // namespace spw

View File

@@ -0,0 +1,709 @@
#include "VirtualDisk.h"
#include "../disk/DiskEnumerator.h"
#include "../common/Logging.h"
#include <windows.h>
#include <virtdisk.h>
#include <winioctl.h>
// Define the GUID manually — avoids initguid.h ordering issues in a static lib.
// Value from Windows SDK virtdisk.h DEFINE_GUID(VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT, ...)
static const GUID kVendorMicrosoft =
{ 0xec984aec, 0xa0f9, 0x47e9, { 0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b } };
#include <algorithm>
#include <chrono>
#include <cstring>
#include <sstream>
#include <vector>
// Link against virtdisk.lib — already in core CMakeLists
#pragma comment(lib, "virtdisk.lib")
namespace spw
{
// ============================================================================
// Helpers
// ============================================================================
VirtualDiskFormat VirtualDisk::detectFormat(const std::wstring& filePath)
{
auto ext = filePath.substr(filePath.rfind(L'.') + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
if (ext == L"vhdx") return VirtualDiskFormat::VHDX;
if (ext == L"vhd") return VirtualDiskFormat::VHD;
if (ext == L"vmdk") return VirtualDiskFormat::VMDK;
if (ext == L"qcow2" || ext == L"qcow") return VirtualDiskFormat::QCOW2;
return VirtualDiskFormat::RAW;
}
const char* VirtualDisk::formatName(VirtualDiskFormat fmt)
{
switch (fmt)
{
case VirtualDiskFormat::VHD: return "VHD";
case VirtualDiskFormat::VHDX: return "VHDX";
case VirtualDiskFormat::VMDK: return "VMDK";
case VirtualDiskFormat::QCOW2: return "QCOW2";
case VirtualDiskFormat::RAW: return "RAW";
}
return "Unknown";
}
bool VirtualDisk::qemuImgAvailable()
{
// Check PATH and app directory
DWORD r = SearchPathW(nullptr, L"qemu-img.exe", nullptr, 0, nullptr, nullptr);
return (r > 0);
}
Result<HANDLE> VirtualDisk::openVirtDiskHandle(const std::wstring& filePath,
VIRTUAL_DISK_ACCESS_MASK access,
OPEN_VIRTUAL_DISK_FLAG flags)
{
VirtualDiskFormat fmt = detectFormat(filePath);
VIRTUAL_STORAGE_TYPE storageType = {};
storageType.DeviceId = (fmt == VirtualDiskFormat::VHD)
? VIRTUAL_STORAGE_TYPE_DEVICE_VHD
: VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
storageType.VendorId = kVendorMicrosoft;
OPEN_VIRTUAL_DISK_PARAMETERS params = {};
params.Version = OPEN_VIRTUAL_DISK_VERSION_1;
params.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;
HANDLE hVdisk = INVALID_HANDLE_VALUE;
DWORD err = OpenVirtualDisk(&storageType, filePath.c_str(),
access, flags, &params, &hVdisk);
if (err != ERROR_SUCCESS)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, err,
"OpenVirtualDisk failed");
return hVdisk;
}
// ============================================================================
// Mount
// ============================================================================
Result<VirtualDiskInfo> VirtualDisk::mount(const std::wstring& filePath, bool readOnly)
{
VirtualDiskFormat fmt = detectFormat(filePath);
if (fmt == VirtualDiskFormat::VMDK || fmt == VirtualDiskFormat::QCOW2 || fmt == VirtualDiskFormat::RAW)
{
// These formats require conversion to VHD/VHDX first, or use ImDisk/Arsenal Image Mounter
return ErrorInfo::fromCode(ErrorCode::FilesystemNotSupported,
std::string(formatName(fmt)) + " cannot be mounted natively on Windows. "
"Convert to VHDX first (use the Convert function), or install Arsenal Image Mounter.");
}
auto mask = readOnly ? VIRTUAL_DISK_ACCESS_ATTACH_RO : VIRTUAL_DISK_ACCESS_ALL;
auto handleResult = openVirtDiskHandle(filePath, mask, OPEN_VIRTUAL_DISK_FLAG_NONE);
if (handleResult.isError())
return handleResult.error();
HANDLE hVdisk = handleResult.value();
// Attach the disk (makes it visible to the OS as a physical disk)
ATTACH_VIRTUAL_DISK_PARAMETERS attachParams = {};
attachParams.Version = ATTACH_VIRTUAL_DISK_VERSION_1;
ATTACH_VIRTUAL_DISK_FLAG attachFlags = ATTACH_VIRTUAL_DISK_FLAG_NONE;
if (readOnly)
attachFlags = static_cast<ATTACH_VIRTUAL_DISK_FLAG>(
attachFlags | ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY);
DWORD err = AttachVirtualDisk(hVdisk, nullptr, attachFlags,
0, &attachParams, nullptr);
if (err != ERROR_SUCCESS)
{
CloseHandle(hVdisk);
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, err, "AttachVirtualDisk failed");
}
// Get the physical path (e.g. \\.\PhysicalDrive3)
wchar_t physPath[512] = {};
DWORD pathSize = sizeof(physPath);
GetVirtualDiskPhysicalPath(hVdisk, &pathSize, physPath);
CloseHandle(hVdisk);
VirtualDiskInfo info;
info.filePath = filePath;
info.format = fmt;
info.isMounted = true;
info.physicalDrivePath = physPath;
return info;
}
// ============================================================================
// Unmount
// ============================================================================
Result<void> VirtualDisk::unmount(const std::wstring& filePath)
{
VirtualDiskFormat fmt = detectFormat(filePath);
auto mask = VIRTUAL_DISK_ACCESS_ALL;
auto handleResult = openVirtDiskHandle(filePath, mask, OPEN_VIRTUAL_DISK_FLAG_NONE);
if (handleResult.isError())
return handleResult.error();
HANDLE hVdisk = handleResult.value();
DWORD err = DetachVirtualDisk(hVdisk, DETACH_VIRTUAL_DISK_FLAG_NONE, 0);
CloseHandle(hVdisk);
if (err != ERROR_SUCCESS && err != ERROR_NOT_FOUND)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, err, "DetachVirtualDisk failed");
return Result<void>::ok();
}
void VirtualDisk::unmountAll()
{
// No convenient Windows API to enumerate all attached VDs
// This is a best-effort placeholder
}
// ============================================================================
// Query info
// ============================================================================
Result<VirtualDiskInfo> VirtualDisk::queryInfo(const std::wstring& filePath)
{
VirtualDiskFormat fmt = detectFormat(filePath);
if (fmt == VirtualDiskFormat::VMDK || fmt == VirtualDiskFormat::QCOW2 || fmt == VirtualDiskFormat::RAW)
{
// For non-VirtDisk formats, just stat the file
VirtualDiskInfo info;
info.filePath = filePath;
info.format = fmt;
WIN32_FILE_ATTRIBUTE_DATA attr{};
if (GetFileAttributesExW(filePath.c_str(), GetFileExInfoStandard, &attr))
{
info.physicalSizeBytes = (static_cast<uint64_t>(attr.nFileSizeHigh) << 32) | attr.nFileSizeLow;
info.virtualSizeBytes = info.physicalSizeBytes; // Best estimate without parsing
}
return info;
}
VIRTUAL_STORAGE_TYPE storageType = {};
storageType.DeviceId = (fmt == VirtualDiskFormat::VHD)
? VIRTUAL_STORAGE_TYPE_DEVICE_VHD
: VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
storageType.VendorId = kVendorMicrosoft;
OPEN_VIRTUAL_DISK_PARAMETERS openParams = {};
openParams.Version = OPEN_VIRTUAL_DISK_VERSION_2;
openParams.Version2.GetInfoOnly = TRUE;
HANDLE hVdisk = INVALID_HANDLE_VALUE;
DWORD err = OpenVirtualDisk(&storageType, filePath.c_str(),
VIRTUAL_DISK_ACCESS_GET_INFO,
OPEN_VIRTUAL_DISK_FLAG_NONE, &openParams, &hVdisk);
if (err != ERROR_SUCCESS)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, err, "OpenVirtualDisk (info) failed");
// Query size
GET_VIRTUAL_DISK_INFO sizeInfo = {};
sizeInfo.Version = GET_VIRTUAL_DISK_INFO_SIZE;
DWORD sizeInfoSize = sizeof(sizeInfo);
DWORD err2 = GetVirtualDiskInformation(hVdisk, &sizeInfoSize, &sizeInfo, nullptr);
VirtualDiskInfo info;
info.filePath = filePath;
info.format = fmt;
if (err2 == ERROR_SUCCESS)
{
info.virtualSizeBytes = sizeInfo.Size.VirtualSize;
info.physicalSizeBytes = sizeInfo.Size.PhysicalSize;
}
// Query provider subtype (fixed vs dynamic)
GET_VIRTUAL_DISK_INFO typeInfo = {};
typeInfo.Version = GET_VIRTUAL_DISK_INFO_PROVIDER_SUBTYPE;
DWORD typeInfoSize = sizeof(typeInfo);
if (GetVirtualDiskInformation(hVdisk, &typeInfoSize, &typeInfo, nullptr) == ERROR_SUCCESS)
info.isDynamic = (typeInfo.ProviderSubtype != 2); // 2 = fixed
CloseHandle(hVdisk);
return info;
}
// ============================================================================
// Create
// ============================================================================
Result<void> VirtualDisk::create(const VirtualDiskCreateParams& params, VDiskProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
if (params.format == VirtualDiskFormat::VMDK || params.format == VirtualDiskFormat::QCOW2)
{
// Use qemu-img for VMDK/QCOW2
if (!qemuImgAvailable())
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"qemu-img not found. Install QEMU and ensure qemu-img.exe is on PATH.");
report("Creating " + std::string(formatName(params.format)) + " image via qemu-img...", 10);
std::wstring fmtStr = (params.format == VirtualDiskFormat::VMDK) ? L"vmdk" : L"qcow2";
std::wstring sizeStr = std::to_wstring(params.sizeBytes);
// qemu-img create -f vmdk output.vmdk <size>
std::wstring cmd = L"qemu-img create -f " + fmtStr + L" \"" +
params.filePath + L"\" " + sizeStr;
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 qemu-img");
WaitForSingleObject(pi.hProcess, 60000);
DWORD exitCode = 1;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
if (exitCode != 0)
return ErrorInfo::fromCode(ErrorCode::FormatFailed, "qemu-img create failed");
report("Done.", 100);
return Result<void>::ok();
}
// VHD or VHDX via VirtDisk API
report("Creating " + std::string(formatName(params.format)) + "...", 5);
VIRTUAL_STORAGE_TYPE storageType = {};
storageType.VendorId = kVendorMicrosoft;
storageType.DeviceId = (params.format == VirtualDiskFormat::VHD)
? VIRTUAL_STORAGE_TYPE_DEVICE_VHD
: VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
CREATE_VIRTUAL_DISK_PARAMETERS createParams = {};
createParams.Version = CREATE_VIRTUAL_DISK_VERSION_2;
createParams.Version2.MaximumSize = params.sizeBytes;
createParams.Version2.BlockSizeInBytes = params.blockSizeBytes;
createParams.Version2.SectorSizeInBytes = params.sectorSize;
CREATE_VIRTUAL_DISK_FLAG createFlags = params.dynamic
? CREATE_VIRTUAL_DISK_FLAG_NONE
: CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION;
HANDLE hVdisk = INVALID_HANDLE_VALUE;
DWORD err = CreateVirtualDisk(&storageType,
params.filePath.c_str(),
VIRTUAL_DISK_ACCESS_NONE,
nullptr,
createFlags,
0,
&createParams,
nullptr,
&hVdisk);
if (err != ERROR_SUCCESS)
return ErrorInfo::fromWin32(ErrorCode::FormatFailed, err, "CreateVirtualDisk failed");
CloseHandle(hVdisk);
report("Done.", 100);
return Result<void>::ok();
}
// ============================================================================
// Capture (physical disk → image)
// ============================================================================
Result<void> VirtualDisk::captureFromDisk(DiskId sourceDiskId,
const std::wstring& outputPath,
VirtualDiskFormat format,
VDiskProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
auto diskInfoResult = DiskEnumerator::getDiskInfo(sourceDiskId);
if (diskInfoResult.isError()) return diskInfoResult.error();
const auto& di = diskInfoResult.value();
uint64_t diskSize = di.sizeBytes;
if (format == VirtualDiskFormat::VHD || format == VirtualDiskFormat::VHDX)
{
// Step 1: create a raw image first, then convert if needed
// For simplicity: capture as RAW, then if VHDX is requested, create VHDX and raw-copy into it
report("Creating virtual disk container...", 5);
if (format == VirtualDiskFormat::VHDX || format == VirtualDiskFormat::VHD)
{
VirtualDiskCreateParams cp;
cp.filePath = outputPath;
cp.format = format;
cp.sizeBytes = diskSize;
cp.dynamic = false; // fixed = exact size for capture
cp.sectorSize = di.sectorSize > 0 ? di.sectorSize : 512;
auto createResult = create(cp, nullptr);
if (createResult.isError()) return createResult;
// Mount the new VHDX
report("Mounting virtual disk for writing...", 10);
auto mountResult = mount(outputPath, false);
if (mountResult.isError()) return mountResult.error();
std::wstring vdiskPhysPath = mountResult.value().physicalDrivePath;
// Open source disk
std::wstring srcPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(sourceDiskId);
HANDLE hSrc = CreateFileW(srcPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hSrc == INVALID_HANDLE_VALUE)
{
unmount(outputPath);
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(), "Cannot open source disk");
}
// Allow reading past mounted volume boundaries on the source disk
DWORD ioBytes = 0;
DeviceIoControl(hSrc, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
// Open target (mounted vdisk)
HANDLE hDst = CreateFileW(vdiskPhysPath.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, nullptr);
if (hDst == INVALID_HANDLE_VALUE)
{
CloseHandle(hSrc);
unmount(outputPath);
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(), "Cannot open virtual disk for writing");
}
// Prepare the target: allow extended I/O, lock and dismount
DeviceIoControl(hDst, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
DeviceIoControl(hDst, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
DeviceIoControl(hDst, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
constexpr uint32_t kChunk = 32 * 1024 * 1024; // 32 MB
auto* buf = static_cast<uint8_t*>(VirtualAlloc(nullptr, kChunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (!buf)
{
CloseHandle(hSrc);
CloseHandle(hDst);
unmount(outputPath);
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to allocate 32 MB I/O buffer");
}
uint64_t totalCopied = 0;
DWORD n = 0;
bool writeError = false;
report("Capturing disk to virtual image...", 15);
while (totalCopied < diskSize)
{
DWORD toRead = static_cast<DWORD>(std::min<uint64_t>(kChunk, diskSize - totalCopied));
if (!ReadFile(hSrc, buf, toRead, &n, nullptr) || n == 0) break;
// Write with retry — re-seek and retry up to 3 times on failure
DWORD written = 0;
bool ok = false;
for (int attempt = 0; attempt < 3; ++attempt)
{
if (WriteFile(hDst, buf, n, &written, nullptr) && written == n)
{
ok = true;
break;
}
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(totalCopied);
SetFilePointerEx(hDst, seekPos, nullptr, FILE_BEGIN);
}
if (!ok) { writeError = true; break; }
totalCopied += n;
int pct = 15 + static_cast<int>((totalCopied * 80) / diskSize);
report("Copying " + std::to_string(totalCopied / (1024*1024)) + " MB / " +
std::to_string(diskSize / (1024*1024)) + " MB...", pct);
}
FlushFileBuffers(hDst);
CloseHandle(hSrc);
CloseHandle(hDst);
VirtualFree(buf, 0, MEM_RELEASE);
report("Unmounting virtual disk...", 97);
unmount(outputPath);
if (writeError)
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Capture failed — write error after 3 retry attempts");
if (totalCopied < diskSize)
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Capture incomplete — read error on source disk");
report("Done.", 100);
return Result<void>::ok();
}
}
// RAW format: straight sector copy
report("Capturing disk as raw image...", 5);
std::wstring srcPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(sourceDiskId);
HANDLE hSrc = CreateFileW(srcPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hSrc == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(), "Cannot open source disk");
// Allow reading past mounted volume boundaries on the source disk
DWORD ioBytes = 0;
DeviceIoControl(hSrc, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
HANDLE hOut = CreateFileW(outputPath.c_str(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hOut == INVALID_HANDLE_VALUE)
{
CloseHandle(hSrc);
return ErrorInfo::fromWin32(ErrorCode::FileCreateFailed, GetLastError(), "Cannot create output file");
}
constexpr uint32_t kChunk = 32 * 1024 * 1024; // 32 MB
auto* buf = static_cast<uint8_t*>(VirtualAlloc(nullptr, kChunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (!buf)
{
CloseHandle(hSrc);
CloseHandle(hOut);
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to allocate 32 MB I/O buffer");
}
uint64_t totalCopied = 0;
DWORD n = 0;
bool writeError = false;
while (totalCopied < diskSize)
{
DWORD toRead = static_cast<DWORD>(std::min<uint64_t>(kChunk, diskSize - totalCopied));
if (!ReadFile(hSrc, buf, toRead, &n, nullptr) || n == 0) break;
// Write with retry — re-seek and retry up to 3 times on failure
DWORD written = 0;
bool ok = false;
for (int attempt = 0; attempt < 3; ++attempt)
{
if (WriteFile(hOut, buf, n, &written, nullptr) && written == n)
{
ok = true;
break;
}
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(totalCopied);
SetFilePointerEx(hOut, seekPos, nullptr, FILE_BEGIN);
}
if (!ok) { writeError = true; break; }
totalCopied += n;
int pct = 5 + static_cast<int>((totalCopied * 90) / diskSize);
report("Copying " + std::to_string(totalCopied / (1024*1024)) + " MB / " +
std::to_string(diskSize / (1024*1024)) + " MB...", pct);
}
FlushFileBuffers(hOut);
CloseHandle(hSrc);
CloseHandle(hOut);
VirtualFree(buf, 0, MEM_RELEASE);
if (writeError)
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Capture failed — write error after 3 retry attempts");
report("Done.", 100);
return Result<void>::ok();
}
// ============================================================================
// Flash to disk
// ============================================================================
Result<void> VirtualDisk::flashToDisk(const std::wstring& imagePath,
DiskId targetDiskId,
VDiskProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
VirtualDiskFormat fmt = detectFormat(imagePath);
bool mounted = false;
std::wstring sourcePath;
if (fmt == VirtualDiskFormat::VHD || fmt == VirtualDiskFormat::VHDX)
{
report("Mounting virtual disk...", 2);
auto mountResult = mount(imagePath, true);
if (mountResult.isError()) return mountResult.error();
sourcePath = mountResult.value().physicalDrivePath;
mounted = true;
}
else
{
// RAW / VMDK / QCOW2: treat as raw file
sourcePath = imagePath;
}
report("Opening source...", 5);
HANDLE hSrc = CreateFileW(sourcePath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hSrc == INVALID_HANDLE_VALUE)
{
if (mounted) unmount(imagePath);
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(), "Cannot open image for reading");
}
// Get source size
LARGE_INTEGER fileSize{};
GetFileSizeEx(hSrc, &fileSize);
uint64_t totalBytes = static_cast<uint64_t>(fileSize.QuadPart);
// For mounted VHD, use IOCTL
if (totalBytes == 0)
{
GET_LENGTH_INFORMATION lenInfo{};
DWORD ret = 0;
DeviceIoControl(hSrc, IOCTL_DISK_GET_LENGTH_INFO, nullptr, 0,
&lenInfo, sizeof(lenInfo), &ret, nullptr);
totalBytes = static_cast<uint64_t>(lenInfo.Length.QuadPart);
}
std::wstring dstPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(targetDiskId);
HANDLE hDst = CreateFileW(dstPath.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, nullptr);
if (hDst == INVALID_HANDLE_VALUE)
{
CloseHandle(hSrc);
if (mounted) unmount(imagePath);
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(), "Cannot open target disk");
}
// Prepare the target disk: allow writes past mounted volume boundaries,
// lock and dismount so Windows doesn't interfere during the flash.
DWORD ioBytes = 0;
DeviceIoControl(hDst, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
DeviceIoControl(hDst, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
DeviceIoControl(hDst, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &ioBytes, nullptr);
constexpr uint32_t kChunk = 32 * 1024 * 1024; // 32 MB
auto* buf = static_cast<uint8_t*>(VirtualAlloc(nullptr, kChunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (!buf)
{
CloseHandle(hSrc);
CloseHandle(hDst);
if (mounted) unmount(imagePath);
return ErrorInfo::fromCode(ErrorCode::DiskReadError, "Failed to allocate 32 MB I/O buffer");
}
uint64_t totalWritten = 0;
DWORD n = 0;
bool writeError = false;
report("Flashing...", 10);
while (true)
{
DWORD toRead = static_cast<DWORD>(
totalBytes > 0 ? std::min<uint64_t>(kChunk, totalBytes - totalWritten) : kChunk);
if (toRead == 0) break;
if (!ReadFile(hSrc, buf, toRead, &n, nullptr) || n == 0) break;
// Write with retry logic — re-seek and retry up to 3 times on failure
DWORD written = 0;
bool ok = false;
for (int attempt = 0; attempt < 3; ++attempt)
{
if (WriteFile(hDst, buf, n, &written, nullptr) && written == n)
{
ok = true;
break;
}
// Re-seek to the same position for retry
LARGE_INTEGER seekPos;
seekPos.QuadPart = static_cast<LONGLONG>(totalWritten);
SetFilePointerEx(hDst, seekPos, nullptr, FILE_BEGIN);
}
if (!ok)
{
writeError = true;
break;
}
totalWritten += n;
int pct = totalBytes > 0
? 10 + static_cast<int>((totalWritten * 85) / totalBytes)
: 50;
report("Writing " + std::to_string(totalWritten / (1024*1024)) + " MB...", pct);
}
FlushFileBuffers(hDst);
CloseHandle(hSrc);
CloseHandle(hDst);
VirtualFree(buf, 0, MEM_RELEASE);
if (mounted) unmount(imagePath);
if (writeError)
return ErrorInfo::fromCode(ErrorCode::DiskWriteError, "Write failed after 3 retry attempts");
report("Done.", 100);
return Result<void>::ok();
}
// ============================================================================
// Convert
// ============================================================================
Result<void> VirtualDisk::convert(const std::wstring& inputPath,
const std::wstring& outputPath,
VirtualDiskFormat targetFormat,
VDiskProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
if (!qemuImgAvailable())
return ErrorInfo::fromCode(ErrorCode::NotImplemented,
"qemu-img not found. Install QEMU and ensure qemu-img.exe is on PATH "
"to convert between VMDK, QCOW2, VHD, and VHDX.");
const wchar_t* fmtStr = L"raw";
switch (targetFormat)
{
case VirtualDiskFormat::VHD: fmtStr = L"vpc"; break; // qemu-img uses "vpc" for VHD
case VirtualDiskFormat::VHDX: fmtStr = L"vhdx"; break;
case VirtualDiskFormat::VMDK: fmtStr = L"vmdk"; break;
case VirtualDiskFormat::QCOW2: fmtStr = L"qcow2"; break;
case VirtualDiskFormat::RAW: fmtStr = L"raw"; break;
}
report("Converting to " + std::string(formatName(targetFormat)) + "...", 5);
std::wstring cmd = L"qemu-img convert -p -O " + std::wstring(fmtStr) +
L" \"" + inputPath + L"\" \"" + outputPath + L"\"";
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 qemu-img");
// Wait up to 60 minutes
WaitForSingleObject(pi.hProcess, 3600000);
DWORD exitCode = 1;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
if (exitCode != 0)
return ErrorInfo::fromCode(ErrorCode::FormatFailed,
"qemu-img convert failed (exit " + std::to_string(exitCode) + ")");
report("Conversion complete.", 100);
return Result<void>::ok();
}
} // namespace spw

View File

@@ -0,0 +1,117 @@
#pragma once
// VirtualDisk — Create, mount, unmount, and convert virtual disk images.
// Supports VHD and VHDX natively via the Windows VirtDisk API.
// VMDK support uses qemu-img as an external converter.
#include <windows.h>
#include <virtdisk.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include <functional>
#include <string>
#include <vector>
namespace spw
{
enum class VirtualDiskFormat
{
VHD, // Fixed or dynamic .vhd (Hyper-V, VirtualBox legacy)
VHDX, // .vhdx — larger, more resilient, Hyper-V preferred
VMDK, // VMware virtual disk (qemu-img required for creation/conversion)
QCOW2, // QEMU native format (qemu-img required)
RAW, // Flat raw image (.img) — sector-for-sector copy
};
struct VirtualDiskInfo
{
std::wstring filePath;
VirtualDiskFormat format = VirtualDiskFormat::VHD;
uint64_t virtualSizeBytes = 0; // Logical size the VM sees
uint64_t physicalSizeBytes = 0; // Actual file size on disk
bool isDynamic = true; // Dynamic (grows) vs fixed (pre-allocated)
bool isMounted = false;
wchar_t mountedDriveLetter = L'\0';
std::wstring physicalDrivePath; // e.g. \\.\PhysicalDrive3 when mounted
};
struct VirtualDiskCreateParams
{
std::wstring filePath;
VirtualDiskFormat format = VirtualDiskFormat::VHDX;
uint64_t sizeBytes = 128ULL * 1024 * 1024 * 1024; // 128 GB default
bool dynamic = true; // dynamic = grows as needed; fixed = pre-allocate all
uint32_t blockSizeBytes = 0; // 0 = use default (2MB for VHDX, 512KB for VHD)
uint32_t sectorSize = 512;
};
using VDiskProgress = std::function<void(const std::string& stage, int pct)>;
class VirtualDisk
{
public:
// ---- Mount / Unmount ----
// Attach a VHD/VHDX file and return the physical drive path (e.g. \\.\PhysicalDrive3)
// readOnly = true prevents writes to the image file
static Result<VirtualDiskInfo> mount(const std::wstring& filePath, bool readOnly = false);
// Detach a mounted virtual disk by its file path
static Result<void> unmount(const std::wstring& filePath);
// Unmount all attached virtual disks (cleanup on exit)
static void unmountAll();
// Get info about a VHD/VHDX without mounting it
static Result<VirtualDiskInfo> queryInfo(const std::wstring& filePath);
// ---- Create ----
// Create a new VHD or VHDX file
static Result<void> create(const VirtualDiskCreateParams& params,
VDiskProgress progress = nullptr);
// ---- Capture (disk → image) ----
// Read a physical disk and write it as a raw .img or VHD/VHDX
static Result<void> captureFromDisk(DiskId sourceDiskId,
const std::wstring& outputPath,
VirtualDiskFormat format,
VDiskProgress progress = nullptr);
// ---- Flash (image → disk) ----
// Write a virtual disk image to a physical disk (SD card, USB, etc.)
// Mounts the VHD first if needed, then copies sector-by-sector
static Result<void> flashToDisk(const std::wstring& imagePath,
DiskId targetDiskId,
VDiskProgress progress = nullptr);
// ---- Conversion ----
// Convert between formats using qemu-img (must be on PATH or next to exe)
static Result<void> convert(const std::wstring& inputPath,
const std::wstring& outputPath,
VirtualDiskFormat targetFormat,
VDiskProgress progress = nullptr);
// Check if qemu-img is available
static bool qemuImgAvailable();
// Detect format from file extension
static VirtualDiskFormat detectFormat(const std::wstring& filePath);
// Format name string
static const char* formatName(VirtualDiskFormat fmt);
private:
static Result<HANDLE> openVirtDiskHandle(const std::wstring& filePath,
VIRTUAL_DISK_ACCESS_MASK access,
OPEN_VIRTUAL_DISK_FLAG flags);
};
} // namespace spw

View File

@@ -0,0 +1,797 @@
#include "SdCardAnalyzer.h"
#include "../disk/RawDiskHandle.h"
#include "../disk/DiskEnumerator.h"
#include "../common/Logging.h"
#include <windows.h>
#include <winioctl.h>
#include <setupapi.h>
#include <devguid.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstring>
#include <random>
#include <string>
namespace spw
{
// ============================================================================
// Known SD/MMC manufacturer IDs (CID MID byte)
// Source: SD Physical Layer Simplified Spec + community databases
// ============================================================================
static const KnownManufacturer kManufacturers[] = {
{ 0x01, "Panasonic" },
{ 0x02, "Toshiba / Kingston" },
{ 0x03, "SanDisk" },
{ 0x06, "Ritek" },
{ 0x09, "ATP Electronics" },
{ 0x11, "Verbatim" },
{ 0x12, "Dana Technology" },
{ 0x13, "Apricorn" },
{ 0x1B, "Samsung" },
{ 0x1D, "AData" },
{ 0x27, "Phison" },
{ 0x28, "Lexar Media" },
{ 0x30, "Silicon Power" },
{ 0x31, "Silicon Power" },
{ 0x33, "STMicroelectronics" },
{ 0x37, "Kingston" },
{ 0x38, "ISSI" },
{ 0x39, "Intenso" },
{ 0x3E, "Netlist" },
{ 0x41, "Kingston" },
{ 0x43, "Micron/Crucial" },
{ 0x45, "Team Group" },
{ 0x46, "Sony" },
{ 0x48, "Hynix" },
{ 0x49, "Lexar" },
{ 0x4E, "Transcend" },
{ 0x51, "Qimonda" },
{ 0x52, "Hynix" },
{ 0x56, "Unknown (likely clone)" },
{ 0x64, "Transcend" },
{ 0x65, "Kingston" },
{ 0x6F, "GreenHouse" },
{ 0x70, "Pny Technologies" },
{ 0x73, "GS Nanotech" },
{ 0x74, "Transcend" },
{ 0x76, "Patriot" },
{ 0x7F, "Unknown (likely clone)" },
{ 0x82, "Sony" },
{ 0x89, "Unknown (likely clone)" },
{ 0xAD, "Hynix" },
{ 0xCE, "Samsung" },
{ 0x00, nullptr }
};
const char* SdCardAnalyzer::manufacturerName(uint8_t mid)
{
for (int i = 0; kManufacturers[i].name != nullptr; ++i)
{
if (kManufacturers[i].mid == mid)
return kManufacturers[i].name;
}
return "Unknown";
}
// ============================================================================
// Query device identity
// ============================================================================
Result<SdCardIdentity> SdCardAnalyzer::queryIdentity(DiskId diskId)
{
SdCardIdentity id;
// Open the device
std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(diskId);
HANDLE hDev = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hDev == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(),
"Cannot open disk for identity query");
// STORAGE_DEVICE_DESCRIPTOR query
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageDeviceProperty;
query.QueryType = PropertyStandardQuery;
constexpr DWORD kBufSize = 4096;
std::vector<uint8_t> buf(kBufSize, 0);
DWORD returned = 0;
if (DeviceIoControl(hDev, IOCTL_STORAGE_QUERY_PROPERTY,
&query, sizeof(query),
buf.data(), kBufSize, &returned, nullptr))
{
auto* desc = reinterpret_cast<STORAGE_DEVICE_DESCRIPTOR*>(buf.data());
auto readStr = [&](DWORD offset) -> std::wstring {
if (offset == 0 || offset >= returned) return {};
const char* p = reinterpret_cast<const char*>(buf.data()) + offset;
std::wstring out;
while (*p && p < reinterpret_cast<const char*>(buf.data()) + returned)
out += static_cast<wchar_t>(*p++);
// Trim trailing spaces
while (!out.empty() && out.back() == L' ') out.pop_back();
return out;
};
id.vendorId = readStr(desc->VendorIdOffset);
id.productId = readStr(desc->ProductIdOffset);
id.productRevision = readStr(desc->ProductRevisionOffset);
id.serialNumberStr = readStr(desc->SerialNumberOffset);
// Bus type string
switch (desc->BusType)
{
case BusTypeSd: id.busType = L"SD"; break;
case BusTypeMmc: id.busType = L"MMC"; break;
case BusTypeUsb: id.busType = L"USB"; break;
case BusTypeSata: id.busType = L"SATA"; break;
case BusTypeNvme: id.busType = L"NVMe"; break;
default: id.busType = L"Unknown"; break;
}
}
// Suspicious vendor detection
// Generic, empty, or known-fake vendor strings
auto vendorLow = id.vendorId;
std::transform(vendorLow.begin(), vendorLow.end(), vendorLow.begin(), ::towlower);
CloseHandle(hDev);
return id;
}
// ============================================================================
// Signature generation — deterministic per (offset, diskId)
// ============================================================================
void SdCardAnalyzer::makeSignature(uint8_t* buf, uint32_t sectorSize,
uint64_t offsetBytes, uint64_t diskSerial)
{
// 8-byte magic header: "SPWSDCK!" + "FAKEDEAД"
buf[0] = 0x53; buf[1] = 0x50; buf[2] = 0x57; buf[3] = 0x53; // "SPWS"
buf[4] = 0xFA; buf[5] = 0x4B; buf[6] = 0xDE; buf[7] = 0xAD; // fake-DEAD
// Encode offset in bytes 8..15 (little-endian)
for (int i = 0; i < 8; ++i)
buf[8 + i] = static_cast<uint8_t>((offsetBytes >> (i * 8)) & 0xFF);
// Encode disk serial in bytes 16..23
for (int i = 0; i < 8; ++i)
buf[16 + i] = static_cast<uint8_t>((diskSerial >> (i * 8)) & 0xFF);
// Fill remaining bytes with a pseudo-random pattern seeded from offset+serial
uint64_t seed = offsetBytes ^ (diskSerial << 13) ^ 0xDEADBEEFCAFEBABEULL;
for (uint32_t i = 24; i < sectorSize; ++i)
{
seed = seed * 6364136223846793005ULL + 1442695040888963407ULL;
buf[i] = static_cast<uint8_t>(seed >> 56);
}
}
// ============================================================================
// Probe a single offset — write phase (save original, write signature)
// ============================================================================
bool SdCardAnalyzer::probeOffset(HANDLE hDisk, uint64_t offsetBytes,
uint32_t sectorSize, uint64_t diskSerial)
{
// This is now used only as a verify-read for a previously written signature.
// The write-all-then-read-all approach is in checkCounterfeit().
offsetBytes = (offsetBytes / sectorSize) * sectorSize;
std::vector<uint8_t> expectedBuf(sectorSize);
std::vector<uint8_t> readBuf(sectorSize);
makeSignature(expectedBuf.data(), sectorSize, offsetBytes, diskSerial);
LARGE_INTEGER li;
li.QuadPart = static_cast<LONGLONG>(offsetBytes);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD nRead = 0;
if (!ReadFile(hDisk, readBuf.data(), sectorSize, &nRead, nullptr) || nRead != sectorSize)
return false;
return (std::memcmp(expectedBuf.data(), readBuf.data(), sectorSize) == 0);
}
// ============================================================================
// Lock and dismount all volumes on a physical disk
// ============================================================================
static void lockAndDismountVolumes(DiskId diskId, std::vector<HANDLE>& lockedHandles)
{
// Find volume letters for this disk by checking each drive letter
DWORD drives = GetLogicalDrives();
for (int i = 0; i < 26; ++i)
{
if (!(drives & (1 << i))) continue;
wchar_t letter = static_cast<wchar_t>(L'A' + i);
// Check if this volume belongs to our disk
wchar_t volPath[] = { L'\\', L'\\', L'.', L'\\', letter, L':', L'\0' };
HANDLE hVol = CreateFileW(volPath, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hVol == INVALID_HANDLE_VALUE)
continue;
VOLUME_DISK_EXTENTS extents{};
DWORD ret = 0;
if (DeviceIoControl(hVol, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
nullptr, 0, &extents, sizeof(extents), &ret, nullptr))
{
bool onOurDisk = false;
for (DWORD e = 0; e < extents.NumberOfDiskExtents; ++e)
{
if (extents.Extents[e].DiskNumber == static_cast<DWORD>(diskId))
{
onOurDisk = true;
break;
}
}
if (onOurDisk)
{
DWORD dummy = 0;
DeviceIoControl(hVol, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &dummy, nullptr);
DeviceIoControl(hVol, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &dummy, nullptr);
lockedHandles.push_back(hVol);
continue; // keep handle open (locked)
}
}
CloseHandle(hVol);
}
}
static void unlockVolumes(std::vector<HANDLE>& lockedHandles)
{
for (HANDLE h : lockedHandles)
{
DWORD dummy = 0;
DeviceIoControl(h, FSCTL_UNLOCK_VOLUME, nullptr, 0, nullptr, 0, &dummy, nullptr);
CloseHandle(h);
}
lockedHandles.clear();
}
// ============================================================================
// Counterfeit check — write-all-then-read-all algorithm
// ============================================================================
Result<CounterfeitResult> SdCardAnalyzer::checkCounterfeit(
DiskId diskId, SdAnalysisProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
CounterfeitResult result;
report("Opening disk for counterfeit check...", 0);
// Get disk size
auto diskInfoResult = DiskEnumerator::getDiskInfo(diskId);
if (diskInfoResult.isError()) return diskInfoResult.error();
const auto& di = diskInfoResult.value();
result.reportedCapacityBytes = di.sizeBytes;
if (result.reportedCapacityBytes == 0)
{
result.verdict = CounterfeitVerdict::TestFailed;
result.summaryMessage = "Disk reports zero capacity — no media?";
return result;
}
// Get identity for manufacturer check
auto idResult = queryIdentity(diskId);
if (idResult.isOk())
{
const auto& id = idResult.value();
result.manufacturerName = manufacturerName(id.manufacturerId);
result.unknownManufacturer = (id.manufacturerId == 0 || !id.cidValid
|| std::string(result.manufacturerName) == "Unknown");
// Only flag vendor string if it's truly suspicious (empty or literally "Generic").
// USB card readers legitimately report "USB" or "Mass Storage" as the bus interface,
// not the card manufacturer — that's normal and not suspicious.
auto vendorLow = id.vendorId;
std::transform(vendorLow.begin(), vendorLow.end(), vendorLow.begin(), ::towlower);
result.suspiciousVendorString =
(vendorLow == L"generic" || vendorLow == L"storage device" || vendorLow == L"sd");
}
// Check write protection before locking volumes
if (isWriteProtected(diskId))
{
result.verdict = CounterfeitVerdict::TestFailed;
result.summaryMessage = "Card is write-protected — cannot probe capacity";
return result;
}
report("Locking volumes on disk...", 3);
// Lock and dismount all volumes on this disk to prevent filesystem
// interference (journal writes, metadata updates, etc.) during probing
std::vector<HANDLE> lockedVolumes;
lockAndDismountVolumes(diskId, lockedVolumes);
report("Opening disk for write probing...", 5);
// Open for read-write with extended DASD I/O (allows access past last partition)
std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(diskId);
HANDLE hDisk = CreateFileW(devPath.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH,
nullptr);
if (hDisk == INVALID_HANDLE_VALUE)
{
unlockVolumes(lockedVolumes);
result.verdict = CounterfeitVerdict::TestFailed;
result.summaryMessage = "Cannot open disk for writing — check admin privileges";
return result;
}
// Enable extended DASD I/O — allows read/write past the last partition boundary,
// which is critical for probing near the end of the disk
DWORD dummy = 0;
DeviceIoControl(hDisk, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &dummy, nullptr);
uint32_t sectorSize = di.sectorSize > 0 ? di.sectorSize : 512;
uint64_t totalSectors = result.reportedCapacityBytes / sectorSize;
// Build probe offsets — geometric distribution across the disk.
// Fake cards typically wrap at their real capacity, so probes past the
// real capacity will silently alias to lower addresses.
// We use write-all-then-read-all: if any write clobbered a previous one
// (due to address aliasing), we'll detect it.
std::vector<uint64_t> probeOffsets;
// Near-end probes (most likely to fail on fake cards)
static const double nearEndFracs[] = { 0.99, 0.98, 0.97, 0.95, 0.93, 0.90, 0.85, 0.80 };
for (auto frac : nearEndFracs)
{
uint64_t off = static_cast<uint64_t>(totalSectors * frac) * sectorSize;
off = (off / sectorSize) * sectorSize; // align
if (off > 0 && off + sectorSize <= result.reportedCapacityBytes)
probeOffsets.push_back(off);
}
// Mid-range probes
static const double midFracs[] = { 0.75, 0.50, 0.625, 0.875, 0.25, 0.125 };
for (auto frac : midFracs)
{
uint64_t off = static_cast<uint64_t>(totalSectors * frac) * sectorSize;
off = (off / sectorSize) * sectorSize;
if (off > 0 && off + sectorSize <= result.reportedCapacityBytes)
probeOffsets.push_back(off);
}
// Remove duplicates (can happen on very small disks)
std::sort(probeOffsets.begin(), probeOffsets.end());
probeOffsets.erase(std::unique(probeOffsets.begin(), probeOffsets.end()), probeOffsets.end());
result.probeCount = static_cast<int>(probeOffsets.size());
result.verifiedCapacityBytes = result.reportedCapacityBytes;
uint64_t diskSerial = static_cast<uint64_t>(diskId) ^ 0xABCD1234EF567890ULL;
// ---- Phase 1: Save original data and write all signatures ----
report("Writing probe signatures...", 8);
std::vector<std::vector<uint8_t>> originalData(probeOffsets.size());
std::vector<bool> writeOk(probeOffsets.size(), false);
for (int i = 0; i < static_cast<int>(probeOffsets.size()); ++i)
{
int pct = 8 + static_cast<int>((static_cast<double>(i) / probeOffsets.size()) * 30.0);
report("Writing signature at " + std::to_string(probeOffsets[i] / (1024 * 1024)) + " MB...", pct);
// Save original sector
originalData[i].resize(sectorSize, 0);
LARGE_INTEGER li;
li.QuadPart = static_cast<LONGLONG>(probeOffsets[i]);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD nRead = 0;
ReadFile(hDisk, originalData[i].data(), sectorSize, &nRead, nullptr);
// Write our unique signature
std::vector<uint8_t> sigBuf(sectorSize);
makeSignature(sigBuf.data(), sectorSize, probeOffsets[i], diskSerial);
li.QuadPart = static_cast<LONGLONG>(probeOffsets[i]);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD nWritten = 0;
writeOk[i] = (WriteFile(hDisk, sigBuf.data(), sectorSize, &nWritten, nullptr)
&& nWritten == sectorSize);
}
// Flush all writes to ensure they hit the physical media
FlushFileBuffers(hDisk);
// ---- Phase 2: Close and re-open to defeat any driver-level caching ----
// Some USB-to-SD readers cache writes internally; re-opening the handle
// forces a fresh read from the actual NAND.
CloseHandle(hDisk);
Sleep(200); // Brief pause for USB controller to settle
hDisk = CreateFileW(devPath.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH,
nullptr);
if (hDisk == INVALID_HANDLE_VALUE)
{
// Can't re-open — restore what we can and bail
unlockVolumes(lockedVolumes);
result.verdict = CounterfeitVerdict::TestFailed;
result.summaryMessage = "Lost access to disk during probe — test inconclusive";
return result;
}
DeviceIoControl(hDisk, FSCTL_ALLOW_EXTENDED_DASD_IO, nullptr, 0, nullptr, 0, &dummy, nullptr);
// ---- Phase 3: Read back ALL signatures and verify ----
report("Verifying probe signatures...", 42);
for (int i = 0; i < static_cast<int>(probeOffsets.size()); ++i)
{
int pct = 42 + static_cast<int>((static_cast<double>(i) / probeOffsets.size()) * 45.0);
report("Verifying at " + std::to_string(probeOffsets[i] / (1024 * 1024)) + " MB...", pct);
if (!writeOk[i])
{
// Write itself failed — count as failure
result.failCount++;
if (result.firstBadOffsetBytes == 0)
result.firstBadOffsetBytes = probeOffsets[i];
if (probeOffsets[i] < result.verifiedCapacityBytes)
result.verifiedCapacityBytes = probeOffsets[i];
continue;
}
bool ok = probeOffset(hDisk, probeOffsets[i], sectorSize, diskSerial);
if (!ok)
{
result.failCount++;
if (result.firstBadOffsetBytes == 0)
result.firstBadOffsetBytes = probeOffsets[i];
if (probeOffsets[i] < result.verifiedCapacityBytes)
result.verifiedCapacityBytes = probeOffsets[i];
}
}
// ---- Phase 4: Restore original data ----
report("Restoring original data...", 90);
for (int i = 0; i < static_cast<int>(probeOffsets.size()); ++i)
{
LARGE_INTEGER li;
li.QuadPart = static_cast<LONGLONG>(probeOffsets[i]);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD nWritten = 0;
WriteFile(hDisk, originalData[i].data(), sectorSize, &nWritten, nullptr);
}
FlushFileBuffers(hDisk);
CloseHandle(hDisk);
// Unlock volumes
unlockVolumes(lockedVolumes);
result.failPercent = result.probeCount > 0
? (static_cast<double>(result.failCount) / result.probeCount) * 100.0 : 0.0;
report("Analyzing results...", 95);
// ---- Verdict ----
if (result.failCount == 0)
{
// All probes passed — card is genuine regardless of vendor string.
// USB card readers legitimately report generic vendor info; that alone
// is not evidence of counterfeiting.
result.verdict = CounterfeitVerdict::Genuine;
result.summaryMessage =
"All " + std::to_string(result.probeCount) + " capacity probes passed. "
"No evidence of capacity spoofing detected.";
if (result.unknownManufacturer)
result.summaryMessage += " (Note: manufacturer ID could not be read, which is "
"normal for cards accessed via USB readers.)";
}
else if (result.failPercent >= 25.0)
{
result.verdict = CounterfeitVerdict::LikelySpoofed;
double realGB = static_cast<double>(result.verifiedCapacityBytes) / (1024.0 * 1024.0 * 1024.0);
double claimedGB = static_cast<double>(result.reportedCapacityBytes) / (1024.0 * 1024.0 * 1024.0);
char buf[512];
snprintf(buf, sizeof(buf),
"COUNTERFEIT DETECTED: Card claims %.1f GB but real capacity appears to be ~%.1f GB. "
"%d of %d probes failed (%.0f%%). "
"First failure at offset %.1f GB.",
claimedGB, realGB,
result.failCount, result.probeCount, result.failPercent,
static_cast<double>(result.firstBadOffsetBytes) / (1024.0 * 1024.0 * 1024.0));
result.summaryMessage = buf;
}
else
{
// Low failure rate — could be marginal NAND or I/O glitch, not necessarily fake
result.verdict = CounterfeitVerdict::Suspicious;
result.summaryMessage =
std::to_string(result.failCount) + " of " + std::to_string(result.probeCount) +
" probes failed. This may indicate marginal NAND cells or a partially "
"counterfeit card. Recommend running the test again — if failures are "
"consistent at the same offsets, the card is likely fake.";
}
report("Done.", 100);
return result;
}
// ============================================================================
// Speed benchmark
// ============================================================================
Result<SdSpeedResult> SdCardAnalyzer::benchmarkSpeed(
DiskId diskId, uint64_t testSizeBytes, SdAnalysisProgress progress)
{
auto report = [&](const std::string& s, int p) { if (progress) progress(s, p); };
SdSpeedResult result;
auto diskInfoResult = DiskEnumerator::getDiskInfo(diskId);
if (diskInfoResult.isError()) return diskInfoResult.error();
uint32_t sectorSize = diskInfoResult.value().sectorSize > 0
? diskInfoResult.value().sectorSize : 512;
if (isWriteProtected(diskId))
result.writeProtected = true;
std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(diskId);
// ---- Sequential Read ----
report("Sequential read benchmark...", 5);
{
HANDLE hDisk = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hDisk != INVALID_HANDLE_VALUE)
{
constexpr uint32_t kChunkSize = 1024 * 1024; // 1 MB chunks
std::vector<uint8_t> buf(kChunkSize);
uint64_t totalRead = 0;
LARGE_INTEGER li{}; SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
auto t0 = std::chrono::steady_clock::now();
while (totalRead < testSizeBytes)
{
DWORD n = 0;
if (!ReadFile(hDisk, buf.data(), kChunkSize, &n, nullptr) || n == 0) break;
totalRead += n;
int pct = 5 + static_cast<int>((static_cast<double>(totalRead) / testSizeBytes) * 25.0);
report("Sequential read: " + std::to_string(totalRead / (1024*1024)) + " MB...", pct);
}
auto t1 = std::chrono::steady_clock::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
if (secs > 0 && totalRead > 0)
result.seqReadMBps = (static_cast<double>(totalRead) / (1024.0 * 1024.0)) / secs;
CloseHandle(hDisk);
}
}
if (result.writeProtected)
{
result.seqWriteMBps = 0;
result.notes = "Write-protected — write benchmarks skipped";
report("Done (write-protected).", 100);
return result;
}
// ---- Sequential Write ----
report("Sequential write benchmark...", 32);
{
HANDLE hDisk = CreateFileW(devPath.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH, nullptr);
if (hDisk != INVALID_HANDLE_VALUE)
{
constexpr uint32_t kChunkSize = 1024 * 1024;
std::vector<uint8_t> buf(kChunkSize, 0xA5); // pattern
uint64_t totalWritten = 0;
LARGE_INTEGER li{}; SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
auto t0 = std::chrono::steady_clock::now();
while (totalWritten < testSizeBytes)
{
DWORD n = 0;
DWORD toWrite = static_cast<DWORD>(
std::min<uint64_t>(kChunkSize, testSizeBytes - totalWritten));
if (!WriteFile(hDisk, buf.data(), toWrite, &n, nullptr) || n == 0) break;
totalWritten += n;
int pct = 32 + static_cast<int>((static_cast<double>(totalWritten) / testSizeBytes) * 28.0);
report("Sequential write: " + std::to_string(totalWritten / (1024*1024)) + " MB...", pct);
}
auto t1 = std::chrono::steady_clock::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
if (secs > 0 && totalWritten > 0)
result.seqWriteMBps = (static_cast<double>(totalWritten) / (1024.0 * 1024.0)) / secs;
CloseHandle(hDisk);
}
}
// ---- Random 4K Read IOPS ----
report("Random 4K read IOPS...", 62);
{
HANDLE hDisk = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_RANDOM_ACCESS, nullptr);
if (hDisk != INVALID_HANDLE_VALUE)
{
constexpr uint32_t kBlockSize = 4096;
constexpr int kIterations = 256;
std::vector<uint8_t> buf(kBlockSize);
uint64_t diskSectors = diskInfoResult.value().sizeBytes / sectorSize;
std::mt19937_64 rng(42);
std::uniform_int_distribution<uint64_t> dist(0, diskSectors - kBlockSize / sectorSize - 1);
auto t0 = std::chrono::steady_clock::now();
int completed = 0;
for (int i = 0; i < kIterations; ++i)
{
uint64_t off = (dist(rng) * sectorSize) & ~(uint64_t)(kBlockSize - 1);
LARGE_INTEGER li; li.QuadPart = static_cast<LONGLONG>(off);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD n = 0;
if (ReadFile(hDisk, buf.data(), kBlockSize, &n, nullptr) && n == kBlockSize)
++completed;
}
auto t1 = std::chrono::steady_clock::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
if (secs > 0)
result.randRead4kIOPS = completed / secs;
CloseHandle(hDisk);
}
}
// ---- Random 4K Write IOPS ----
report("Random 4K write IOPS...", 82);
{
HANDLE hDisk = CreateFileW(devPath.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_RANDOM_ACCESS, nullptr);
if (hDisk != INVALID_HANDLE_VALUE)
{
constexpr uint32_t kBlockSize = 4096;
constexpr int kIterations = 128;
std::vector<uint8_t> buf(kBlockSize, 0x5A);
uint64_t diskSectors = diskInfoResult.value().sizeBytes / sectorSize;
uint64_t safeEnd = diskSectors / 2; // Only write to first half to preserve data
std::mt19937_64 rng(99);
std::uniform_int_distribution<uint64_t> dist(1, safeEnd - kBlockSize / sectorSize - 1);
auto t0 = std::chrono::steady_clock::now();
int completed = 0;
for (int i = 0; i < kIterations; ++i)
{
uint64_t off = (dist(rng) * sectorSize) & ~(uint64_t)(kBlockSize - 1);
LARGE_INTEGER li; li.QuadPart = static_cast<LONGLONG>(off);
SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
DWORD n = 0;
if (WriteFile(hDisk, buf.data(), kBlockSize, &n, nullptr) && n == kBlockSize)
++completed;
}
auto t1 = std::chrono::steady_clock::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
if (secs > 0)
result.randWrite4kIOPS = completed / secs;
CloseHandle(hDisk);
}
}
report("Done.", 100);
return result;
}
// ============================================================================
// Surface scan
// ============================================================================
Result<SdHealthResult> SdCardAnalyzer::surfaceScan(
DiskId diskId, std::atomic<bool>* cancelFlag, SdSectorProgress progress)
{
SdHealthResult result;
auto diskInfoResult = DiskEnumerator::getDiskInfo(diskId);
if (diskInfoResult.isError()) return diskInfoResult.error();
uint32_t sectorSize = diskInfoResult.value().sectorSize > 0
? diskInfoResult.value().sectorSize : 512;
result.totalSectors = diskInfoResult.value().sizeBytes / sectorSize;
std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(diskId);
HANDLE hDisk = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING, nullptr);
if (hDisk == INVALID_HANDLE_VALUE)
return ErrorInfo::fromWin32(ErrorCode::DiskAccessDenied, GetLastError(),
"Cannot open disk for surface scan");
constexpr uint32_t kSectorsPerRead = 64; // 32 KB per read
std::vector<uint8_t> buf(kSectorsPerRead * 512);
LARGE_INTEGER li{}; SetFilePointerEx(hDisk, li, nullptr, FILE_BEGIN);
for (uint64_t sec = 0; sec < result.totalSectors; sec += kSectorsPerRead)
{
if (cancelFlag && cancelFlag->load()) break;
uint32_t toRead = static_cast<uint32_t>(
std::min<uint64_t>(kSectorsPerRead, result.totalSectors - sec));
uint32_t readBytes = toRead * sectorSize;
auto t0 = std::chrono::steady_clock::now();
DWORD n = 0;
bool ok = ReadFile(hDisk, buf.data(), readBytes, &n, nullptr) && n == readBytes;
auto t1 = std::chrono::steady_clock::now();
double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
if (!ok)
result.badSectors += toRead;
else if (ms > 500.0)
result.slowSectors += toRead;
result.sectorsScanned += toRead;
if (progress)
{
int pct = static_cast<int>((result.sectorsScanned * 100) / result.totalSectors);
progress(sec, result.totalSectors, result.badSectors, pct);
}
}
CloseHandle(hDisk);
result.complete = (cancelFlag == nullptr || !cancelFlag->load());
char summary[256];
snprintf(summary, sizeof(summary),
"Scanned %llu sectors. Bad: %llu. Slow (>500ms): %llu.",
(unsigned long long)result.sectorsScanned,
(unsigned long long)result.badSectors,
(unsigned long long)result.slowSectors);
result.summary = summary;
return result;
}
// ============================================================================
// Write-protection check
// ============================================================================
bool SdCardAnalyzer::isWriteProtected(DiskId diskId)
{
std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(diskId);
HANDLE h = CreateFileW(devPath.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (h == INVALID_HANDLE_VALUE)
return true; // Can't open for write — treat as write-protected
// Try a test write of zero bytes via DeviceIoControl
DWORD returned = 0;
BOOL ok = DeviceIoControl(h, IOCTL_DISK_IS_WRITABLE, nullptr, 0, nullptr, 0, &returned, nullptr);
DWORD err = GetLastError();
CloseHandle(h);
// IOCTL_DISK_IS_WRITABLE returns FALSE with ERROR_WRITE_PROTECT if write-protected
return (!ok && err == ERROR_WRITE_PROTECT);
}
} // namespace spw

View File

@@ -0,0 +1,169 @@
#pragma once
// SdCardAnalyzer — Deep analysis, counterfeit detection, speed testing,
// and health checking for SD/MMC cards on Windows.
//
// Counterfeit detection works by probing actual writable capacity — fake cards
// (capacity-spoofed NAND) report large sizes but silently wrap writes back to
// the beginning of the real NAND. We write unique signatures at geometrically
// distributed offsets, then read them back and count mismatches.
//
// DISCLAIMER: This code is for authorized disk utility software only.
#include <windows.h>
#include <winioctl.h>
#include "../common/Error.h"
#include "../common/Result.h"
#include "../common/Types.h"
#include "../disk/DiskEnumerator.h"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace spw
{
// ============================================================================
// Card identification info pulled from device descriptors
// ============================================================================
struct SdCardIdentity
{
// From STORAGE_DEVICE_DESCRIPTOR (via DeviceIoControl)
std::wstring vendorId; // e.g. L"SanDisk"
std::wstring productId; // e.g. L"SD Card" or L"Storage Device"
std::wstring productRevision;
std::wstring serialNumberStr;
// SD-specific CID fields (when available via IOCTL_SFFDISK_QUERY_DEVICE_PROTOCOL)
uint8_t manufacturerId = 0; // CID[127:120] — MID byte
uint16_t oemId = 0; // CID[119:104] — OEM/Application ID
std::string productName; // CID[103:64] — Product name (5 ASCII chars)
uint8_t productRevision8 = 0;
uint32_t serialNumber32 = 0; // CID[55:24]
bool cidValid = false; // True if CID was readable
// Reported speed / class (from STORAGE_DEVICE_DESCRIPTOR extended, if available)
std::wstring busType; // e.g. L"SD", L"MMC", L"USB"
};
// Known legitimate manufacturer IDs (CID MID byte)
// https://www.cameramemoryspeed.com/sd-memory-card-faq/reading-sd-card-cid-serial-psn-internal-information/
struct KnownManufacturer
{
uint8_t mid;
const char* name;
};
// ============================================================================
// Counterfeit check result
// ============================================================================
enum class CounterfeitVerdict
{
Genuine, // Passed all checks
LikelySpoofed, // Capacity mismatch found — almost certainly fake
Suspicious, // Some anomalies but not conclusive
TestFailed, // Could not complete test (write-protected, no access)
Untested
};
struct CounterfeitResult
{
CounterfeitVerdict verdict = CounterfeitVerdict::Untested;
uint64_t reportedCapacityBytes = 0; // What the card claims
uint64_t verifiedCapacityBytes = 0; // What we could actually write and read back
uint64_t firstBadOffsetBytes = 0; // Where the first mismatch was found (0 = none)
int probeCount = 0; // How many offsets were probed
int failCount = 0; // How many probes failed
double failPercent = 0.0;
std::string manufacturerName; // From known MID table (or "Unknown")
bool unknownManufacturer = false;
bool suspiciousVendorString = false; // Generic vendor like "Generic" or "Storage"
std::string summaryMessage; // Human-readable verdict
};
// ============================================================================
// Speed benchmark result
// ============================================================================
struct SdSpeedResult
{
double seqReadMBps = 0.0;
double seqWriteMBps = 0.0;
double randRead4kIOPS = 0.0;
double randWrite4kIOPS = 0.0;
bool writeProtected = false;
std::string notes;
};
// ============================================================================
// Health / surface scan result
// ============================================================================
struct SdHealthResult
{
uint64_t totalSectors = 0;
uint64_t sectorsScanned = 0;
uint64_t badSectors = 0;
uint64_t slowSectors = 0; // Readable but unusually slow (>500ms per sector)
bool complete = false;
std::string summary;
};
// ============================================================================
// Progress callbacks
// ============================================================================
using SdAnalysisProgress = std::function<void(const std::string& stage, int pct)>;
using SdSectorProgress = std::function<void(uint64_t currentSector, uint64_t totalSectors,
uint64_t badFound, int pct)>;
// ============================================================================
// SdCardAnalyzer
// ============================================================================
class SdCardAnalyzer
{
public:
// Read device identity / manufacturer info from descriptor
static Result<SdCardIdentity> queryIdentity(DiskId diskId);
// Counterfeit detection — writes unique signatures at probe offsets and
// verifies them. Minimally invasive: restores original data if possible.
// WARNING: overwrites probe sectors. Only use on cards you own.
static Result<CounterfeitResult> checkCounterfeit(
DiskId diskId,
SdAnalysisProgress progress = nullptr);
// Sequential + random speed benchmark
static Result<SdSpeedResult> benchmarkSpeed(
DiskId diskId,
uint64_t testSizeBytes = 64 * 1024 * 1024, // 64 MB default
SdAnalysisProgress progress = nullptr);
// Surface scan — read every sector and flag errors/slow sectors
static Result<SdHealthResult> surfaceScan(
DiskId diskId,
std::atomic<bool>* cancelFlag = nullptr,
SdSectorProgress progress = nullptr);
// Check write-protection status
static bool isWriteProtected(DiskId diskId);
// Look up manufacturer name from MID byte
static const char* manufacturerName(uint8_t mid);
private:
// Write a unique 512-byte signature block to a sector and read it back
static bool probeOffset(HANDLE hDisk, uint64_t offsetBytes,
uint32_t sectorSize, uint64_t diskId);
// Generate a deterministic signature for a given offset (so we can verify)
static void makeSignature(uint8_t* buf, uint32_t sectorSize,
uint64_t offsetBytes, uint64_t diskId);
};
} // namespace spw

View File

@@ -20,8 +20,8 @@ bool SdCardRecovery::looksLikeSdCard(const DiskInfo& disk)
if (disk.interfaceType == DiskInterfaceType::MMC) if (disk.interfaceType == DiskInterfaceType::MMC)
return true; return true;
// Removable + small size (up to 2TB covers SDXC) // Removable + size up to 2TB (zero size allowed — card may be corrupted)
if (disk.isRemovable && disk.sizeBytes > 0 && disk.sizeBytes <= 2199023255552ULL) if (disk.isRemovable && disk.sizeBytes <= 2199023255552ULL)
{ {
// Check model string for SD/MMC keywords // Check model string for SD/MMC keywords
std::wstring modelLower = disk.model; std::wstring modelLower = disk.model;
@@ -65,8 +65,19 @@ Result<SdCardInfo> SdCardRecovery::analyzeDisk(DiskId diskId)
if (info.sizeBytes == 0) if (info.sizeBytes == 0)
{ {
info.status = SdCardStatus::NoMedia; // Zero size can mean: no card inserted, OR card is so corrupted
info.statusDescription = L"Card reader detected but no media inserted"; // that the controller can't report geometry. Try opening it anyway.
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadOnly);
if (diskResult.isError())
{
info.status = SdCardStatus::NoMedia;
info.statusDescription = L"Card reader found but cannot access media — insert card or try again";
}
else
{
info.status = SdCardStatus::NoPartitionTable;
info.statusDescription = L"Card found but reports zero size — likely corrupted by interrupted format. Use Fix to repair.";
}
return info; return info;
} }
@@ -170,51 +181,97 @@ Result<std::vector<SdCardInfo>> SdCardRecovery::detectSdCards()
cards.push_back(std::move(analysisResult.value())); cards.push_back(std::move(analysisResult.value()));
} }
// Also try to find disks that SetupAPI detects but enumerateDisks might // Aggressive brute-force: scan PhysicalDrive0..127 to catch any device
// miss due to having zero partitions — scan PhysicalDrive0..31 directly // that SetupAPI missed — especially cards with corrupted partition tables
for (int i = 0; i < 32; ++i) // that report zero size or broken geometry after a failed format.
for (int i = 0; i < 128; ++i)
{ {
// Skip if we already found this disk // Skip if already found via normal enumeration
bool found = false; bool found = false;
for (const auto& card : cards) for (const auto& card : cards)
{ {
if (card.diskId == i) if (card.diskId == i) { found = true; break; }
{
found = true;
break;
}
} }
if (found) if (found)
continue; continue;
// Try to open the disk directly // Try to open — any device that opens is a candidate
auto diskResult = RawDiskHandle::open(i, DiskAccessMode::ReadOnly); std::wstring devPath = L"\\\\.\\PhysicalDrive" + std::to_wstring(i);
if (diskResult.isError()) HANDLE h = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (h == INVALID_HANDLE_VALUE)
continue; continue;
auto& disk = diskResult.value(); // Query STORAGE_DEVICE_DESCRIPTOR to determine if it's removable
auto geomResult = disk.getGeometry(); STORAGE_PROPERTY_QUERY spq = {};
if (geomResult.isError()) spq.PropertyId = StorageDeviceProperty;
continue; spq.QueryType = PropertyStandardQuery;
// Check if it's removable media uint8_t buf[1024] = {};
if (geomResult.value().mediaType == RemovableMedia || DWORD ret = 0;
geomResult.value().mediaType == FixedMedia) bool isRemovable = false;
uint64_t diskSize = 0;
if (DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY,
&spq, sizeof(spq), buf, sizeof(buf), &ret, nullptr))
{ {
// Only include small removable disks (likely SD cards) auto* desc = reinterpret_cast<STORAGE_DEVICE_DESCRIPTOR*>(buf);
auto totalBytes = geomResult.value().totalBytes; isRemovable = (desc->RemovableMedia != FALSE);
if (totalBytes > 0 && totalBytes <= 2199023255552ULL)
// BusTypeSd = 14, BusTypeMmc = 15
bool isSdBus = (desc->BusType == BusTypeSd || desc->BusType == BusTypeMmc);
bool isUsb = (desc->BusType == BusTypeUsb);
if (!isSdBus && !isRemovable && !isUsb)
{ {
auto analysisResult = analyzeDisk(i); CloseHandle(h);
if (analysisResult.isOk()) continue; // Not a removable / SD type device
{
auto& info = analysisResult.value();
if (info.model.empty())
info.model = L"Unknown Removable Disk";
cards.push_back(std::move(info));
}
} }
} }
// Get disk size — zero is OK (mid-format crash scenario)
GET_LENGTH_INFORMATION lenInfo = {};
DWORD lenRet = 0;
DeviceIoControl(h, IOCTL_DISK_GET_LENGTH_INFO, nullptr, 0,
&lenInfo, sizeof(lenInfo), &lenRet, nullptr);
diskSize = static_cast<uint64_t>(lenInfo.Length.QuadPart);
CloseHandle(h);
// Skip huge disks that are clearly not SD cards (> 2 TB)
if (diskSize > 2199023255552ULL && diskSize != 0)
continue;
// Include this device — it's removable and small (or zero-size due to corruption)
SdCardInfo info;
info.diskId = i;
info.sizeBytes = diskSize;
info.sectorSize = 512;
if (diskSize == 0)
{
info.status = SdCardStatus::NoPartitionTable;
info.statusDescription = L"Device found — reports zero size (likely corrupted by interrupted format)";
info.model = L"Removable Disk " + std::to_wstring(i) + L" (size unknown — use Fix to repair)";
}
else
{
auto analysisResult = analyzeDisk(i);
if (analysisResult.isOk())
{
info = analysisResult.value();
}
else
{
info.status = SdCardStatus::NoPartitionTable;
info.statusDescription = L"Cannot read partition table — may be corrupted";
}
if (info.model.empty())
info.model = L"Removable Disk " + std::to_wstring(i);
}
cards.push_back(std::move(info));
} }
return cards; return cards;
@@ -355,77 +412,189 @@ Result<void> SdCardRecovery::rescanDisk(RawDiskHandle& disk)
return Result<void>::ok(); return Result<void>::ok();
} }
// Helper: run a command and wait for it, return stdout+stderr and exit code
static std::pair<std::wstring, DWORD> runHidden(std::wstring cmd, DWORD timeoutMs = 120000)
{
STARTUPINFOW si = {};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
// Pipe stdout/stderr so we can capture error messages
HANDLE hReadPipe = nullptr, hWritePipe = nullptr;
SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
CreatePipe(&hReadPipe, &hWritePipe, &sa, 0);
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
si.hStdOutput = hWritePipe;
si.hStdError = hWritePipe;
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, TRUE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi))
{
CloseHandle(hReadPipe); CloseHandle(hWritePipe);
return { L"CreateProcess failed", (DWORD)-1 };
}
CloseHandle(hWritePipe);
WaitForSingleObject(pi.hProcess, timeoutMs);
// Read output
char buf[4096] = {};
DWORD nRead = 0;
std::string output;
while (ReadFile(hReadPipe, buf, sizeof(buf) - 1, &nRead, nullptr) && nRead > 0)
{
buf[nRead] = '\0';
output += buf;
}
CloseHandle(hReadPipe);
DWORD exitCode = 1;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
std::wstring wout(output.begin(), output.end());
return { wout, exitCode };
}
Result<void> SdCardRecovery::formatPartition(DiskId diskId, FilesystemType fs, Result<void> SdCardRecovery::formatPartition(DiskId diskId, FilesystemType fs,
const std::wstring& label) 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; std::wstring fsName;
switch (fs) switch (fs)
{ {
case FilesystemType::FAT32: fsName = L"FAT32"; break; case FilesystemType::FAT32: fsName = L"FAT32"; break;
case FilesystemType::ExFAT: fsName = L"exFAT"; break; case FilesystemType::ExFAT: fsName = L"exFAT"; break;
case FilesystemType::NTFS: fsName = L"NTFS"; break; case FilesystemType::NTFS: fsName = L"NTFS"; break;
default: fsName = L"FAT32"; 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 // Strategy 1: PowerShell Format-Volume by disk number.
+ L" /Q /V:" + label + L" /Y"; // Works without a drive letter — uses the disk index directly.
// This is the most reliable path for freshly-partitioned cards.
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(), std::wstring labelEscaped = label;
"Failed to launch format command"); // Escape single quotes in the label
size_t pos = 0;
while ((pos = labelEscaped.find(L"'", pos)) != std::wstring::npos)
{
labelEscaped.replace(pos, 1, L"''");
pos += 2;
}
std::wstring psCmd =
L"Get-Disk -Number " + std::to_wstring(diskId) +
L" | Get-Partition | Where-Object { $_.Type -ne 'Reserved' } "
L"| Format-Volume -FileSystem " + fsName +
L" -NewFileSystemLabel '" + labelEscaped +
L"' -Force -Confirm:$false";
std::wstring fullCmd = L"powershell.exe -NonInteractive -NoProfile -Command \"" + psCmd + L"\"";
auto [out, code] = runHidden(fullCmd, 120000);
if (code == 0)
return Result<void>::ok();
log::warn(("PowerShell Format-Volume failed (exit " + std::to_string(code) + "): " +
std::string(out.begin(), out.end())).c_str());
} }
// Wait up to 60 seconds for format to complete // ----------------------------------------------------------------
DWORD waitResult = WaitForSingleObject(pi.hProcess, 60000); // Strategy 2: diskpart to assign a drive letter, then format.com
DWORD exitCode = 1; // ----------------------------------------------------------------
GetExitCodeProcess(pi.hProcess, &exitCode); {
CloseHandle(pi.hProcess); // Wait a moment for Windows to recognise the partition
CloseHandle(pi.hThread); Sleep(3000);
if (waitResult == WAIT_TIMEOUT) // Find a free drive letter (start from Z: down to avoid conflicts)
return ErrorInfo::fromCode(ErrorCode::FormatFailed, "Format command timed out"); wchar_t freeLetter = L'\0';
DWORD driveMask = GetLogicalDrives();
for (int i = 25; i >= 4; --i) // Z..E
{
if (!(driveMask & (1 << i)))
{
freeLetter = wchar_t(L'A' + i);
break;
}
}
if (exitCode != 0) if (freeLetter != L'\0')
return ErrorInfo::fromCode(ErrorCode::FormatFailed, {
"Format command failed with exit code " + std::to_string(exitCode)); // Build a diskpart script to: select disk, select first partition, assign letter
std::wstring script =
L"select disk " + std::to_wstring(diskId) + L"\r\n"
L"select partition 1\r\n"
L"assign letter=" + std::wstring(1, freeLetter) + L"\r\n"
L"exit\r\n";
return Result<void>::ok(); // Write diskpart script to a temp file
wchar_t tempDir[MAX_PATH], scriptPath[MAX_PATH];
GetTempPathW(MAX_PATH, tempDir);
GetTempFileNameW(tempDir, L"spw", 0, scriptPath);
// Add .txt extension
std::wstring scriptFile = std::wstring(scriptPath) + L".txt";
MoveFileW(scriptPath, scriptFile.c_str());
HANDLE hFile = CreateFileW(scriptFile.c_str(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile != INVALID_HANDLE_VALUE)
{
DWORD written = 0;
std::string scriptA(script.begin(), script.end());
WriteFile(hFile, scriptA.c_str(), (DWORD)scriptA.size(), &written, nullptr);
CloseHandle(hFile);
std::wstring diskpartCmd = L"diskpart /s \"" + scriptFile + L"\"";
auto [dpOut, dpCode] = runHidden(diskpartCmd, 30000);
DeleteFileW(scriptFile.c_str());
if (dpCode == 0)
{
Sleep(1500);
std::wstring fmtCmd = L"format " + std::wstring(1, freeLetter) +
L": /FS:" + fsName + L" /Q /V:" + label + L" /Y";
auto [fmtOut, fmtCode] = runHidden(fmtCmd, 120000);
if (fmtCode == 0)
return Result<void>::ok();
log::warn(("format.com failed (exit " + std::to_string(fmtCode) + ")").c_str());
}
}
}
}
// ----------------------------------------------------------------
// Strategy 3: Already has a drive letter from a previous attempt
// ----------------------------------------------------------------
{
Sleep(2000);
auto snapshotResult = DiskEnumerator::getSystemSnapshot();
if (snapshotResult.isOk())
{
for (const auto& part : snapshotResult.value().partitions)
{
if (part.diskId == diskId && part.driveLetter != L'\0')
{
std::wstring fmtCmd =
L"format " + std::wstring(1, part.driveLetter) +
L": /FS:" + fsName + L" /Q /V:" + label + L" /Y";
auto [out, code] = runHidden(fmtCmd, 120000);
if (code == 0)
return Result<void>::ok();
}
}
}
}
return ErrorInfo::fromCode(ErrorCode::FormatFailed,
"All format strategies failed. The partition table was reinitialized successfully, "
"but Windows could not format the partition automatically. "
"Open Disk Management (diskmgmt.msc), right-click the new partition on your SD card, "
"and choose 'Format' to complete the process manually.");
} }
Result<void> SdCardRecovery::fixCard(DiskId diskId, const SdFixConfig& config, Result<void> SdCardRecovery::fixCard(DiskId diskId, const SdFixConfig& config,
@@ -444,16 +613,69 @@ Result<void> SdCardRecovery::fixCard(DiskId diskId, const SdFixConfig& config,
auto& disk = diskResult.value(); auto& disk = diskResult.value();
// Get geometry for disk size // Get geometry for disk size — corrupted cards sometimes report zero
auto geomResult = disk.getGeometry(); uint64_t diskSize = 0;
if (geomResult.isError()) uint32_t sectorSize = 512;
return geomResult.error();
uint64_t diskSize = geomResult.value().totalBytes; auto geomResult = disk.getGeometry();
uint32_t sectorSize = geomResult.value().bytesPerSector; if (geomResult.isOk())
{
diskSize = geomResult.value().totalBytes;
sectorSize = geomResult.value().bytesPerSector > 0
? geomResult.value().bytesPerSector : 512;
}
if (diskSize == 0) if (diskSize == 0)
return ErrorInfo::fromCode(ErrorCode::InvalidArgument, "Disk reports zero size - no media?"); {
// Try IOCTL_DISK_GET_LENGTH_INFO as fallback
HANDLE hDisk = disk.nativeHandle();
GET_LENGTH_INFORMATION lenInfo = {};
DWORD ret = 0;
if (DeviceIoControl(hDisk, IOCTL_DISK_GET_LENGTH_INFO, nullptr, 0,
&lenInfo, sizeof(lenInfo), &ret, nullptr))
{
diskSize = static_cast<uint64_t>(lenInfo.Length.QuadPart);
}
}
if (diskSize == 0)
{
// Card is accessible but reports zero size — this is the mid-format crash scenario.
// The controller still accepts writes even though geometry is broken.
// Use a conservative 32 GB estimate for the IOCTL_DISK_CREATE_DISK call;
// the card's real controller will handle the actual boundaries.
log::warn("Card reports zero size — proceeding with IOCTL-only repair (no raw zero write)");
// Skip cleanDisk (which writes to sectors) since we don't know the size.
// Just reinitialize the partition table via IOCTL, then let Windows rescan.
report("Card reports zero size — attempting IOCTL partition table reset...", 20);
HANDLE hDisk = disk.nativeHandle();
CREATE_DISK createDisk = {};
createDisk.PartitionStyle = PARTITION_STYLE_MBR;
createDisk.Mbr.Signature = GetTickCount();
DWORD returned = 0;
if (!DeviceIoControl(hDisk, IOCTL_DISK_CREATE_DISK,
&createDisk, sizeof(createDisk),
nullptr, 0, &returned, nullptr))
{
return ErrorInfo::fromWin32(ErrorCode::DiskWriteError, GetLastError(),
"Cannot reinitialize card — it may need to be physically reseated or the reader replaced");
}
DeviceIoControl(hDisk, IOCTL_DISK_UPDATE_PROPERTIES,
nullptr, 0, nullptr, 0, &returned, nullptr);
disk.close();
report("Partition table reset. Waiting for Windows to rescan...", 60);
Sleep(3000);
auto fmtResult = formatPartition(diskId, config.targetFs, config.volumeLabel);
if (fmtResult.isError())
return fmtResult;
report("Done!", 100);
return Result<void>::ok();
}
// Auto-select filesystem based on size if FAT32 requested on >32GB card // Auto-select filesystem based on size if FAT32 requested on >32GB card
FilesystemType targetFs = config.targetFs; FilesystemType targetFs = config.targetFs;

View File

@@ -0,0 +1,213 @@
#include "DownloadManager.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QFile>
#include <QFileInfo>
#include <QElapsedTimer>
#include <QSslError>
#include <QEventLoop>
#include <QDebug>
namespace spw {
DownloadManager::DownloadManager(QObject* parent)
: QObject(parent)
, m_manager(new QNetworkAccessManager(this))
, m_speedTimer(new QElapsedTimer)
{
}
DownloadManager::~DownloadManager()
{
cancelDownload();
delete m_speedTimer;
}
void DownloadManager::startDownload(const QUrl& url, const QString& outputPath)
{
if (m_downloading) {
emit downloadError(QStringLiteral("A download is already in progress."));
return;
}
m_outputPath = outputPath;
m_resumeOffset = 0;
m_bytesReceivedSinceLastSpeed = 0;
m_downloading = true;
// Check if we can resume a partial download
QFileInfo fileInfo(outputPath);
QIODevice::OpenMode openMode = QIODevice::WriteOnly;
if (fileInfo.exists() && fileInfo.size() > 0) {
m_resumeOffset = fileInfo.size();
openMode = QIODevice::Append;
}
m_file = new QFile(outputPath, this);
if (!m_file->open(openMode)) {
emit downloadError(QStringLiteral("Failed to open file for writing: %1").arg(m_file->errorString()));
cleanup();
return;
}
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
// Set range header for resume
if (m_resumeOffset > 0) {
QByteArray rangeHeader = "bytes=" + QByteArray::number(m_resumeOffset) + "-";
request.setRawHeader("Range", rangeHeader);
qDebug() << "[DownloadManager] Resuming download from byte" << m_resumeOffset;
}
m_reply = m_manager->get(request);
connect(m_reply, &QNetworkReply::readyRead,
this, &DownloadManager::onReadyRead);
connect(m_reply, &QNetworkReply::downloadProgress,
this, &DownloadManager::onDownloadProgress);
connect(m_reply, &QNetworkReply::finished,
this, &DownloadManager::onFinished);
connect(m_reply, &QNetworkReply::sslErrors,
this, [this](const QList<QSslError>& errors) {
for (const QSslError& err : errors)
qWarning() << "[DownloadManager] SSL error (ignored):" << err.errorString();
if (m_reply)
m_reply->ignoreSslErrors();
});
m_speedTimer->start();
qDebug() << "[DownloadManager] Starting download:" << url.toString();
}
void DownloadManager::cancelDownload()
{
if (!m_downloading)
return;
if (m_reply) {
m_reply->abort();
}
qDebug() << "[DownloadManager] Download cancelled.";
cleanup();
}
bool DownloadManager::isDownloading() const
{
return m_downloading;
}
bool DownloadManager::supportsResume(const QUrl& url)
{
QNetworkAccessManager tempManager;
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply* reply = tempManager.head(request);
// Block until the HEAD request finishes
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
bool resumable = false;
if (reply->error() == QNetworkReply::NoError) {
QByteArray acceptRanges = reply->rawHeader("Accept-Ranges");
resumable = acceptRanges.toLower().contains("bytes");
}
reply->deleteLater();
return resumable;
}
void DownloadManager::onReadyRead()
{
if (!m_file || !m_reply)
return;
// Write received data directly to disk without buffering
QByteArray data = m_reply->readAll();
qint64 written = m_file->write(data);
if (written == -1) {
emit downloadError(QStringLiteral("Failed to write to disk: %1").arg(m_file->errorString()));
cancelDownload();
return;
}
m_bytesReceivedSinceLastSpeed += data.size();
// Calculate speed every 500ms
qint64 elapsed = m_speedTimer->elapsed();
if (elapsed >= 500) {
double seconds = elapsed / 1000.0;
double bytesPerSec = m_bytesReceivedSinceLastSpeed / seconds;
emit speedUpdate(bytesPerSec);
m_bytesReceivedSinceLastSpeed = 0;
m_speedTimer->restart();
}
}
void DownloadManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
// Adjust for resume offset so the caller sees total file progress
qint64 totalReceived = bytesReceived + m_resumeOffset;
qint64 totalSize = (bytesTotal > 0) ? (bytesTotal + m_resumeOffset) : -1;
emit progressChanged(totalReceived, totalSize);
}
void DownloadManager::onFinished()
{
if (!m_reply)
return;
// Flush any remaining buffered data
QByteArray remaining = m_reply->readAll();
if (!remaining.isEmpty() && m_file) {
m_file->write(remaining);
}
if (m_reply->error() == QNetworkReply::NoError) {
QString path = m_outputPath;
cleanup();
qDebug() << "[DownloadManager] Download complete:" << path;
emit downloadComplete(path);
} else if (m_reply->error() == QNetworkReply::OperationCanceledError) {
// Already handled by cancelDownload, just clean up
cleanup();
} else {
QString errorMsg = m_reply->errorString();
cleanup();
qDebug() << "[DownloadManager] Download failed:" << errorMsg;
emit downloadError(errorMsg);
}
}
void DownloadManager::cleanup()
{
m_downloading = false;
if (m_reply) {
m_reply->disconnect(this);
m_reply->deleteLater();
m_reply = nullptr;
}
if (m_file) {
if (m_file->isOpen())
m_file->close();
m_file->deleteLater();
m_file = nullptr;
}
m_resumeOffset = 0;
m_bytesReceivedSinceLastSpeed = 0;
}
} // namespace spw

View File

@@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QUrl>
#include <QString>
class QNetworkAccessManager;
class QNetworkReply;
class QFile;
class QElapsedTimer;
class QSslError;
namespace spw {
class DownloadManager : public QObject
{
Q_OBJECT
public:
explicit DownloadManager(QObject* parent = nullptr);
~DownloadManager() override;
void startDownload(const QUrl& url, const QString& outputPath);
void cancelDownload();
bool isDownloading() const;
static bool supportsResume(const QUrl& url);
signals:
void progressChanged(qint64 bytesReceived, qint64 bytesTotal);
void downloadComplete(const QString& filePath);
void downloadError(const QString& error);
void speedUpdate(double bytesPerSec);
private slots:
void onReadyRead();
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onFinished();
private:
void cleanup();
QNetworkAccessManager* m_manager = nullptr;
QNetworkReply* m_reply = nullptr;
QFile* m_file = nullptr;
QElapsedTimer* m_speedTimer = nullptr;
QString m_outputPath;
qint64 m_resumeOffset = 0;
qint64 m_bytesReceivedSinceLastSpeed = 0;
bool m_downloading = false;
};
} // namespace spw

View File

@@ -6,6 +6,11 @@ set(UI_SOURCES
tabs/DiagnosticsTab.cpp tabs/DiagnosticsTab.cpp
tabs/SecurityTab.cpp tabs/SecurityTab.cpp
tabs/MaintenanceTab.cpp tabs/MaintenanceTab.cpp
tabs/SdCardTab.cpp
tabs/VirtualDiskTab.cpp
tabs/NonWindowsFsTab.cpp
tabs/LinuxFlasherTab.cpp
tabs/KaliCreatorTab.cpp
widgets/DiskMapWidget.cpp widgets/DiskMapWidget.cpp
) )
@@ -17,6 +22,11 @@ set(UI_HEADERS
tabs/DiagnosticsTab.h tabs/DiagnosticsTab.h
tabs/SecurityTab.h tabs/SecurityTab.h
tabs/MaintenanceTab.h tabs/MaintenanceTab.h
tabs/SdCardTab.h
tabs/VirtualDiskTab.h
tabs/NonWindowsFsTab.h
tabs/LinuxFlasherTab.h
tabs/KaliCreatorTab.h
widgets/DiskMapWidget.h widgets/DiskMapWidget.h
) )

View File

@@ -5,6 +5,11 @@
#include "tabs/DiagnosticsTab.h" #include "tabs/DiagnosticsTab.h"
#include "tabs/SecurityTab.h" #include "tabs/SecurityTab.h"
#include "tabs/MaintenanceTab.h" #include "tabs/MaintenanceTab.h"
#include "tabs/SdCardTab.h"
#include "tabs/VirtualDiskTab.h"
#include "tabs/NonWindowsFsTab.h"
#include "tabs/LinuxFlasherTab.h"
#include "tabs/KaliCreatorTab.h"
#include "core/common/Version.h" #include "core/common/Version.h"
#include "core/disk/DiskEnumerator.h" #include "core/disk/DiskEnumerator.h"
@@ -13,7 +18,9 @@
#include <QAction> #include <QAction>
#include <QApplication> #include <QApplication>
#include <QCryptographicHash>
#include <QDir> #include <QDir>
#include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include <QIcon> #include <QIcon>
#include <QKeyEvent> #include <QKeyEvent>
@@ -165,6 +172,8 @@ void MainWindow::setupMenuBar()
toolsMenu->addAction(tr("S&urface Scan...")); toolsMenu->addAction(tr("S&urface Scan..."));
toolsMenu->addSeparator(); toolsMenu->addSeparator();
toolsMenu->addAction(tr("&Boot Repair...")); toolsMenu->addAction(tr("&Boot Repair..."));
toolsMenu->addSeparator();
toolsMenu->addAction(tr("&Unlock Features..."), this, &MainWindow::onUnlockFeatures);
auto* helpMenu = menuBar()->addMenu(tr("&Help")); auto* helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(tr("&About..."), this, &MainWindow::onAbout); helpMenu->addAction(tr("&About..."), this, &MainWindow::onAbout);
@@ -215,6 +224,11 @@ void MainWindow::setupTabs()
m_diagnosticsTab = new DiagnosticsTab(this); m_diagnosticsTab = new DiagnosticsTab(this);
m_securityTab = new SecurityTab(this); m_securityTab = new SecurityTab(this);
m_maintenanceTab = new MaintenanceTab(this); m_maintenanceTab = new MaintenanceTab(this);
m_sdCardTab = new SdCardTab(this);
m_virtualDiskTab = new VirtualDiskTab(this);
m_nonWinFsTab = new NonWindowsFsTab(this);
m_linuxFlasherTab = new LinuxFlasherTab(this);
m_kaliCreatorTab = new KaliCreatorTab(this);
m_tabWidget->addTab(m_diskPartitionTab, tr("Disks && Partitions")); m_tabWidget->addTab(m_diskPartitionTab, tr("Disks && Partitions"));
m_tabWidget->addTab(m_recoveryTab, tr("Recovery")); m_tabWidget->addTab(m_recoveryTab, tr("Recovery"));
@@ -222,6 +236,11 @@ void MainWindow::setupTabs()
m_tabWidget->addTab(m_diagnosticsTab, tr("Diagnostics")); m_tabWidget->addTab(m_diagnosticsTab, tr("Diagnostics"));
m_tabWidget->addTab(m_securityTab, tr("Security Keys")); m_tabWidget->addTab(m_securityTab, tr("Security Keys"));
m_tabWidget->addTab(m_maintenanceTab, tr("Maintenance")); m_tabWidget->addTab(m_maintenanceTab, tr("Maintenance"));
m_tabWidget->addTab(m_sdCardTab, tr("SD Cards"));
m_tabWidget->addTab(m_virtualDiskTab, tr("Virtual Disks"));
m_tabWidget->addTab(m_nonWinFsTab, tr("Linux Filesystems"));
m_tabWidget->addTab(m_linuxFlasherTab, tr("Linux Flasher"));
m_tabWidget->addTab(m_kaliCreatorTab, tr("Kali Creator"));
setCentralWidget(m_tabWidget); setCentralWidget(m_tabWidget);
} }
@@ -246,6 +265,56 @@ void MainWindow::connectTabSignals()
this, &MainWindow::onStatusMessage); this, &MainWindow::onStatusMessage);
connect(m_maintenanceTab, &MaintenanceTab::statusMessage, connect(m_maintenanceTab, &MaintenanceTab::statusMessage,
this, &MainWindow::onStatusMessage); this, &MainWindow::onStatusMessage);
connect(m_sdCardTab, &SdCardTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_virtualDiskTab, &VirtualDiskTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_nonWinFsTab, &NonWindowsFsTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_linuxFlasherTab, &LinuxFlasherTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_kaliCreatorTab, &KaliCreatorTab::statusMessage,
this, &MainWindow::onStatusMessage);
}
void MainWindow::onUnlockFeatures()
{
if (m_hwdiagActive)
{
QMessageBox::information(this, tr("Already Unlocked"),
tr("Extended features are already active."));
return;
}
QString path = QFileDialog::getOpenFileName(
this, tr("Select Key File"), QString(), tr("Key Files (*.key);;All Files (*)"));
if (path.isEmpty())
return;
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
{
QMessageBox::warning(this, tr("Error"), tr("Could not open the selected file."));
return;
}
QByteArray content = file.readAll();
file.close();
// Verify SHA-256 of the file content (base64-encoded poem)
QByteArray hash = QCryptographicHash::hash(content, QCryptographicHash::Sha256).toHex();
if (hash == QByteArrayLiteral("f2cd6920ba4b09c79c105810f9eff9d73beb1f689b8f67099c1a39e5634059c5"))
{
hwdiag_activate();
QMessageBox::information(this, tr("Unlocked"),
tr("Extended features have been activated."));
}
else
{
QMessageBox::warning(this, tr("Invalid License"),
tr("The selected file is not a valid license."));
}
} }
void MainWindow::onAbout() void MainWindow::onAbout()
@@ -282,6 +351,11 @@ void MainWindow::onRefreshDisks()
m_diagnosticsTab->refreshDisks(m_lastSnapshot); m_diagnosticsTab->refreshDisks(m_lastSnapshot);
m_securityTab->refreshDisks(m_lastSnapshot); m_securityTab->refreshDisks(m_lastSnapshot);
m_maintenanceTab->refreshDisks(m_lastSnapshot); m_maintenanceTab->refreshDisks(m_lastSnapshot);
m_sdCardTab->refreshDisks(m_lastSnapshot);
m_virtualDiskTab->refreshDisks(m_lastSnapshot);
m_nonWinFsTab->refreshDisks(m_lastSnapshot);
m_linuxFlasherTab->refreshDisks(m_lastSnapshot);
m_kaliCreatorTab->refreshDisks(m_lastSnapshot);
statusBar()->showMessage( statusBar()->showMessage(
tr("Found %1 disk(s), %2 partition(s), %3 volume(s)") tr("Found %1 disk(s), %2 partition(s), %3 volume(s)")

View File

@@ -18,6 +18,11 @@ class ImagingTab;
class DiagnosticsTab; class DiagnosticsTab;
class SecurityTab; class SecurityTab;
class MaintenanceTab; class MaintenanceTab;
class SdCardTab;
class VirtualDiskTab;
class NonWindowsFsTab;
class LinuxFlasherTab;
class KaliCreatorTab;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@@ -43,6 +48,7 @@ private:
private slots: private slots:
void onAbout(); void onAbout();
void onRefreshDisks(); void onRefreshDisks();
void onUnlockFeatures();
void onStatusMessage(const QString& msg); void onStatusMessage(const QString& msg);
private: private:
@@ -56,6 +62,11 @@ private:
DiagnosticsTab* m_diagnosticsTab = nullptr; DiagnosticsTab* m_diagnosticsTab = nullptr;
SecurityTab* m_securityTab = nullptr; SecurityTab* m_securityTab = nullptr;
MaintenanceTab* m_maintenanceTab = nullptr; MaintenanceTab* m_maintenanceTab = nullptr;
SdCardTab* m_sdCardTab = nullptr;
VirtualDiskTab* m_virtualDiskTab = nullptr;
NonWindowsFsTab* m_nonWinFsTab = nullptr;
LinuxFlasherTab* m_linuxFlasherTab = nullptr;
KaliCreatorTab* m_kaliCreatorTab = nullptr;
// Hardware diagnostics module (vendor library) // Hardware diagnostics module (vendor library)
QWidget* m_hwdiagPanel = nullptr; QWidget* m_hwdiagPanel = nullptr;

View File

@@ -86,7 +86,7 @@ void DiskPartitionTab::setupUi()
m_diskTree->setModel(m_diskTreeModel); m_diskTree->setModel(m_diskTreeModel);
m_diskTree->setHeaderHidden(false); m_diskTree->setHeaderHidden(false);
m_diskTree->setAlternatingRowColors(true); m_diskTree->setAlternatingRowColors(true);
m_diskTree->setMinimumWidth(280); m_diskTree->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_diskTree->setEditTriggers(QAbstractItemView::NoEditTriggers); m_diskTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_diskTree->setSelectionMode(QAbstractItemView::SingleSelection); m_diskTree->setSelectionMode(QAbstractItemView::SingleSelection);
leftLayout->addWidget(m_diskTree); leftLayout->addWidget(m_diskTree);
@@ -142,7 +142,7 @@ void DiskPartitionTab::setupUi()
opLayout->addWidget(opLabel); opLayout->addWidget(opLabel);
m_operationListWidget = new QListWidget(); m_operationListWidget = new QListWidget();
m_operationListWidget->setMinimumWidth(220); m_operationListWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
opLayout->addWidget(m_operationListWidget); opLayout->addWidget(m_operationListWidget);
auto* buttonLayout = new QHBoxLayout(); auto* buttonLayout = new QHBoxLayout();
@@ -456,8 +456,56 @@ void DiskPartitionTab::onCreatePartition()
sizeGbSpin->setValue(1.0); sizeGbSpin->setValue(1.0);
form->addRow(tr("Size:"), sizeGbSpin); form->addRow(tr("Size:"), sizeGbSpin);
// Full filesystem list — label + corresponding enum value kept in sync
struct FsEntry { const char* label; FilesystemType type; };
static const FsEntry kCreateFsEntries[] = {
// ── Windows / Modern ──
{ "NTFS", FilesystemType::NTFS },
{ "FAT32", FilesystemType::FAT32 },
{ "FAT16", FilesystemType::FAT16 },
{ "FAT12 (floppy / tiny)", FilesystemType::FAT12 },
{ "exFAT (flash / large SD)", FilesystemType::ExFAT },
{ "ReFS (Windows Server)", FilesystemType::ReFS },
// ── Linux ──
{ "ext4", FilesystemType::Ext4 },
{ "ext3", FilesystemType::Ext3 },
{ "ext2", FilesystemType::Ext2 },
{ "Btrfs", FilesystemType::Btrfs },
{ "XFS", FilesystemType::XFS },
{ "ZFS", FilesystemType::ZFS },
{ "JFS", FilesystemType::JFS },
{ "ReiserFS", FilesystemType::ReiserFS },
{ "F2FS (flash-optimised)", FilesystemType::F2FS },
{ "JFFS2 (embedded flash)", FilesystemType::JFFS2 },
{ "NILFS2", FilesystemType::NILFS2 },
{ "Linux Swap", FilesystemType::SWAP_LINUX },
// ── Apple ──
{ "HFS+ (Mac OS Extended)", FilesystemType::HFSPlus },
{ "HFS (Classic Mac OS)", FilesystemType::HFS },
{ "APFS (detection only)", FilesystemType::APFS },
// ── Unix / BSD ──
{ "UFS (BSD / Solaris)", FilesystemType::UFS },
// ── Legacy / Retro ──
{ "HPFS (OS/2)", FilesystemType::HPFS },
{ "VFAT (long-name FAT)", FilesystemType::VFAT },
{ "UDF (optical / universal)",FilesystemType::UDF },
{ "ISO 9660 (CD-ROM)", FilesystemType::ISO9660 },
{ "Minix", FilesystemType::Minix },
{ "QNX4", FilesystemType::QNX4 },
{ "Amiga FFS", FilesystemType::AfFS },
{ "BeOS BFS", FilesystemType::BFS_BeOS },
{ "SquashFS (read-only)", FilesystemType::SquashFS },
{ "RomFS (read-only)", FilesystemType::RomFS },
// ── Console / Gaming ──
{ "FATX (Xbox / Xbox 360)", FilesystemType::FATX },
// ── Raw / unformatted ──
{ "Unformatted / Raw", FilesystemType::Raw },
};
constexpr int kCreateFsCount = static_cast<int>(std::size(kCreateFsEntries));
auto* fsCombo = new QComboBox(); auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2")}); for (int i = 0; i < kCreateFsCount; ++i)
fsCombo->addItem(QString::fromLatin1(kCreateFsEntries[i].label));
form->addRow(tr("Filesystem:"), fsCombo); form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit(); auto* labelEdit = new QLineEdit();
@@ -476,9 +524,8 @@ void DiskPartitionTab::onCreatePartition()
uint32_t sectorSize = diskInfo->sectorSize; uint32_t sectorSize = diskInfo->sectorSize;
SectorCount sectors = sizeBytes / sectorSize; SectorCount sectors = sizeBytes / sectorSize;
// Find first large enough gap // Find first large enough gap — simple: offset after last partition
SectorOffset startLba = DEFAULT_ALIGNMENT_SECTORS_512; SectorOffset startLba = DEFAULT_ALIGNMENT_SECTORS_512;
// Simple: use offset after last partition
for (const auto& p : m_snapshot.partitions) for (const auto& p : m_snapshot.partitions)
{ {
if (p.diskId == m_selectedDiskId) if (p.diskId == m_selectedDiskId)
@@ -496,16 +543,9 @@ void DiskPartitionTab::onCreatePartition()
params.sectorSize = sectorSize; params.sectorSize = sectorSize;
params.formatAfter = true; params.formatAfter = true;
// Map filesystem selection
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2
};
int fsIdx = fsCombo->currentIndex(); int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes))) if (fsIdx >= 0 && fsIdx < kCreateFsCount)
{ params.formatOptions.targetFs = kCreateFsEntries[fsIdx].type;
params.formatOptions.targetFs = fsTypes[fsIdx];
}
params.formatOptions.volumeLabel = labelEdit->text().toStdString(); params.formatOptions.volumeLabel = labelEdit->text().toStdString();
params.formatOptions.quickFormat = true; params.formatOptions.quickFormat = true;
@@ -618,8 +658,52 @@ void DiskPartitionTab::onFormatPartition()
dlg.setWindowTitle(tr("Format Partition")); dlg.setWindowTitle(tr("Format Partition"));
auto* form = new QFormLayout(&dlg); auto* form = new QFormLayout(&dlg);
struct FmtEntry { const char* label; FilesystemType type; };
static const FmtEntry kFmtFsEntries[] = {
// ── Windows / Modern ──
{ "NTFS", FilesystemType::NTFS },
{ "FAT32", FilesystemType::FAT32 },
{ "FAT16", FilesystemType::FAT16 },
{ "FAT12 (floppy / tiny)", FilesystemType::FAT12 },
{ "exFAT (flash / large SD)", FilesystemType::ExFAT },
{ "ReFS (Windows Server)", FilesystemType::ReFS },
// ── Linux ──
{ "ext4", FilesystemType::Ext4 },
{ "ext3", FilesystemType::Ext3 },
{ "ext2", FilesystemType::Ext2 },
{ "Btrfs", FilesystemType::Btrfs },
{ "XFS", FilesystemType::XFS },
{ "ZFS", FilesystemType::ZFS },
{ "JFS", FilesystemType::JFS },
{ "ReiserFS", FilesystemType::ReiserFS },
{ "F2FS (flash-optimised)", FilesystemType::F2FS },
{ "JFFS2 (embedded flash)", FilesystemType::JFFS2 },
{ "NILFS2", FilesystemType::NILFS2 },
{ "Linux Swap", FilesystemType::SWAP_LINUX },
// ── Apple ──
{ "HFS+ (Mac OS Extended)", FilesystemType::HFSPlus },
{ "HFS (Classic Mac OS)", FilesystemType::HFS },
// ── Unix / BSD ──
{ "UFS (BSD / Solaris)", FilesystemType::UFS },
// ── Legacy / Retro ──
{ "HPFS (OS/2)", FilesystemType::HPFS },
{ "VFAT (long-name FAT)", FilesystemType::VFAT },
{ "UDF (optical)", FilesystemType::UDF },
{ "ISO 9660 (CD-ROM)", FilesystemType::ISO9660 },
{ "Minix", FilesystemType::Minix },
{ "QNX4", FilesystemType::QNX4 },
{ "Amiga FFS", FilesystemType::AfFS },
{ "BeOS BFS", FilesystemType::BFS_BeOS },
{ "SquashFS (read-only)", FilesystemType::SquashFS },
{ "RomFS (read-only)", FilesystemType::RomFS },
// ── Console / Gaming ──
{ "FATX (Xbox / Xbox 360)", FilesystemType::FATX },
};
constexpr int kFmtFsCount = static_cast<int>(std::size(kFmtFsEntries));
auto* fsCombo = new QComboBox(); auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2"), tr("Linux Swap")}); for (int i = 0; i < kFmtFsCount; ++i)
fsCombo->addItem(QString::fromLatin1(kFmtFsEntries[i].label));
form->addRow(tr("Filesystem:"), fsCombo); form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit(); auto* labelEdit = new QLineEdit();
@@ -637,12 +721,6 @@ void DiskPartitionTab::onFormatPartition()
if (dlg.exec() != QDialog::Accepted) if (dlg.exec() != QDialog::Accepted)
return; return;
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2,
FilesystemType::SWAP_LINUX
};
FormatPartitionOp::Params params; FormatPartitionOp::Params params;
params.diskId = m_selectedDiskId; params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx; params.partitionIndex = partIdx;
@@ -659,8 +737,8 @@ void DiskPartitionTab::onFormatPartition()
} }
int fsIdx = fsCombo->currentIndex(); int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes))) if (fsIdx >= 0 && fsIdx < kFmtFsCount)
params.options.targetFs = fsTypes[fsIdx]; params.options.targetFs = kFmtFsEntries[fsIdx].type;
params.options.volumeLabel = labelEdit->text().toStdString(); params.options.volumeLabel = labelEdit->text().toStdString();
params.options.quickFormat = quickCheck->isChecked(); params.options.quickFormat = quickCheck->isChecked();

View File

@@ -6,16 +6,24 @@
#include "core/imaging/ImageRestorer.h" #include "core/imaging/ImageRestorer.h"
#include "core/imaging/IsoFlasher.h" #include "core/imaging/IsoFlasher.h"
#ifdef _WIN32
#include <windows.h>
#include <winioctl.h>
#endif
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QFileDialog> #include <QFileDialog>
#include <QGridLayout> #include <QGridLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
#include <QProcess>
#include <QProgressBar> #include <QProgressBar>
#include <QPushButton> #include <QPushButton>
#include <QTabWidget>
#include <QThread> #include <QThread>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -34,7 +42,12 @@ void ImagingTab::setupUi()
{ {
auto* layout = new QVBoxLayout(this); auto* layout = new QVBoxLayout(this);
// ===== Clone Disk ===== auto* innerTabs = new QTabWidget();
// ===== Tab 1: Clone Disk =====
auto* cloneWidget = new QWidget();
auto* cloneOuterLayout = new QVBoxLayout(cloneWidget);
auto* cloneGroup = new QGroupBox(tr("Clone Disk")); auto* cloneGroup = new QGroupBox(tr("Clone Disk"));
auto* cloneLayout = new QGridLayout(cloneGroup); auto* cloneLayout = new QGridLayout(cloneGroup);
@@ -67,9 +80,14 @@ void ImagingTab::setupUi()
connect(m_cloneBtn, &QPushButton::clicked, this, &ImagingTab::onCloneDisk); connect(m_cloneBtn, &QPushButton::clicked, this, &ImagingTab::onCloneDisk);
cloneLayout->addWidget(m_cloneBtn, 4, 2, Qt::AlignRight); cloneLayout->addWidget(m_cloneBtn, 4, 2, Qt::AlignRight);
layout->addWidget(cloneGroup); cloneOuterLayout->addWidget(cloneGroup);
cloneOuterLayout->addStretch();
innerTabs->addTab(cloneWidget, tr("Clone Disk"));
// ===== Tab 2: Create Image =====
auto* imageWidget = new QWidget();
auto* imageOuterLayout = new QVBoxLayout(imageWidget);
// ===== Create Image =====
auto* imageGroup = new QGroupBox(tr("Create Disk Image")); auto* imageGroup = new QGroupBox(tr("Create Disk Image"));
auto* imageLayout = new QGridLayout(imageGroup); auto* imageLayout = new QGridLayout(imageGroup);
@@ -101,9 +119,14 @@ void ImagingTab::setupUi()
connect(m_imageCreateBtn, &QPushButton::clicked, this, &ImagingTab::onCreateImage); connect(m_imageCreateBtn, &QPushButton::clicked, this, &ImagingTab::onCreateImage);
imageLayout->addWidget(m_imageCreateBtn, 4, 2, Qt::AlignRight); imageLayout->addWidget(m_imageCreateBtn, 4, 2, Qt::AlignRight);
layout->addWidget(imageGroup); imageOuterLayout->addWidget(imageGroup);
imageOuterLayout->addStretch();
innerTabs->addTab(imageWidget, tr("Create Image"));
// ===== Tab 3: Restore Image =====
auto* restoreWidget = new QWidget();
auto* restoreOuterLayout = new QVBoxLayout(restoreWidget);
// ===== Restore Image =====
auto* restoreGroup = new QGroupBox(tr("Restore Image")); auto* restoreGroup = new QGroupBox(tr("Restore Image"));
auto* restoreLayout = new QGridLayout(restoreGroup); auto* restoreLayout = new QGridLayout(restoreGroup);
@@ -140,9 +163,14 @@ void ImagingTab::setupUi()
connect(m_restoreBtn, &QPushButton::clicked, this, &ImagingTab::onRestoreImage); connect(m_restoreBtn, &QPushButton::clicked, this, &ImagingTab::onRestoreImage);
restoreLayout->addWidget(m_restoreBtn, 4, 2, Qt::AlignRight); restoreLayout->addWidget(m_restoreBtn, 4, 2, Qt::AlignRight);
layout->addWidget(restoreGroup); restoreOuterLayout->addWidget(restoreGroup);
restoreOuterLayout->addStretch();
innerTabs->addTab(restoreWidget, tr("Restore Image"));
// ===== Tab 4: Flash ISO/IMG =====
auto* flashWidget = new QWidget();
auto* flashOuterLayout = new QVBoxLayout(flashWidget);
// ===== Flash ISO/IMG =====
auto* flashGroup = new QGroupBox(tr("Flash ISO/IMG to USB")); auto* flashGroup = new QGroupBox(tr("Flash ISO/IMG to USB"));
auto* flashLayout = new QGridLayout(flashGroup); auto* flashLayout = new QGridLayout(flashGroup);
@@ -173,9 +201,155 @@ void ImagingTab::setupUi()
connect(m_flashBtn, &QPushButton::clicked, this, &ImagingTab::onFlashIso); connect(m_flashBtn, &QPushButton::clicked, this, &ImagingTab::onFlashIso);
flashLayout->addWidget(m_flashBtn, 3, 2, Qt::AlignRight); flashLayout->addWidget(m_flashBtn, 3, 2, Qt::AlignRight);
layout->addWidget(flashGroup); flashOuterLayout->addWidget(flashGroup);
flashOuterLayout->addStretch();
innerTabs->addTab(flashWidget, tr("Flash ISO/IMG"));
layout->addStretch(); // ===== Tab 5: Optical Disc (CD / DVD / Blu-ray) =====
auto* optWidget = new QWidget();
auto* optOuterLayout = new QVBoxLayout(optWidget);
// Drive selector row
auto* driveRow = new QHBoxLayout();
driveRow->addWidget(new QLabel(tr("Drive:")));
m_opticalDriveCombo = new QComboBox();
m_opticalDriveCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
driveRow->addWidget(m_opticalDriveCombo, 1);
m_opticalRefreshBtn = new QPushButton(tr("Refresh"));
connect(m_opticalRefreshBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalRefreshDrives);
driveRow->addWidget(m_opticalRefreshBtn);
optOuterLayout->addLayout(driveRow);
m_opticalDriveInfo = new QLabel(tr("No drive selected."));
m_opticalDriveInfo->setStyleSheet("color: #aaaaaa; font-style: italic; padding: 2px 4px;");
m_opticalDriveInfo->setWordWrap(true);
optOuterLayout->addWidget(m_opticalDriveInfo);
// Inner tab widget: Rip | Burn | Erase
auto* optTabs = new QTabWidget();
// ---- Rip tab ----
auto* ripWidget = new QWidget();
auto* ripLayout = new QGridLayout(ripWidget);
ripLayout->addWidget(new QLabel(tr("Output File:")), 0, 0);
m_ripOutputEdit = new QLineEdit();
m_ripOutputEdit->setPlaceholderText(tr("e.g. C:\\disc.iso"));
ripLayout->addWidget(m_ripOutputEdit, 0, 1);
auto* ripBrowseBtn = new QPushButton(tr("Browse..."));
connect(ripBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalBrowseRipOutput);
ripLayout->addWidget(ripBrowseBtn, 0, 2);
ripLayout->addWidget(new QLabel(tr("Format:")), 1, 0);
m_ripFormatCombo = new QComboBox();
m_ripFormatCombo->addItems({
tr("ISO 9660 (.iso) — standard, compatible with everything"),
tr("Raw (.bin/.cue) — preserves subchannel data (audio CDs)"),
tr("NRG (.nrg) — Nero format"),
});
ripLayout->addWidget(m_ripFormatCombo, 1, 1, 1, 2);
m_ripVerifyCheck = new QCheckBox(tr("Verify rip (SHA-256 checksum)"));
m_ripVerifyCheck->setChecked(true);
ripLayout->addWidget(m_ripVerifyCheck, 2, 1);
m_ripProgress = new QProgressBar();
m_ripProgress->setVisible(false);
ripLayout->addWidget(m_ripProgress, 3, 0, 1, 3);
m_ripStatusLabel = new QLabel();
m_ripStatusLabel->setWordWrap(true);
ripLayout->addWidget(m_ripStatusLabel, 4, 0, 1, 3);
m_ripBtn = new QPushButton(tr("Rip Disc to Image"));
m_ripBtn->setObjectName("applyButton");
connect(m_ripBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalRipDisc);
ripLayout->addWidget(m_ripBtn, 5, 2, Qt::AlignRight);
optTabs->addTab(ripWidget, tr("Rip Disc"));
// ---- Burn tab ----
auto* burnWidget = new QWidget();
auto* burnLayout = new QGridLayout(burnWidget);
burnLayout->addWidget(new QLabel(tr("Image File:")), 0, 0);
m_burnInputEdit = new QLineEdit();
m_burnInputEdit->setPlaceholderText(tr("Select ISO, IMG, BIN, or NRG file..."));
burnLayout->addWidget(m_burnInputEdit, 0, 1);
auto* burnBrowseBtn = new QPushButton(tr("Browse..."));
connect(burnBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalBrowseBurnInput);
burnLayout->addWidget(burnBrowseBtn, 0, 2);
burnLayout->addWidget(new QLabel(tr("Write Speed:")), 1, 0);
m_burnSpeedCombo = new QComboBox();
m_burnSpeedCombo->addItems({
tr("Maximum (auto)"), tr("1x"), tr("2x"), tr("4x"),
tr("8x"), tr("16x"), tr("24x"), tr("32x"), tr("48x"), tr("52x")
});
burnLayout->addWidget(m_burnSpeedCombo, 1, 1);
m_burnVerifyCheck = new QCheckBox(tr("Verify disc after burn"));
m_burnVerifyCheck->setChecked(true);
burnLayout->addWidget(m_burnVerifyCheck, 2, 1);
m_burnFinalizeCheck = new QCheckBox(tr("Finalize disc (close session — makes disc read-only)"));
m_burnFinalizeCheck->setChecked(true);
burnLayout->addWidget(m_burnFinalizeCheck, 3, 1, 1, 2);
m_burnProgress = new QProgressBar();
m_burnProgress->setVisible(false);
burnLayout->addWidget(m_burnProgress, 4, 0, 1, 3);
m_burnStatusLabel = new QLabel();
m_burnStatusLabel->setWordWrap(true);
burnLayout->addWidget(m_burnStatusLabel, 5, 0, 1, 3);
m_burnBtn = new QPushButton(tr("Burn Image to Disc"));
m_burnBtn->setObjectName("applyButton");
connect(m_burnBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalBurnImage);
burnLayout->addWidget(m_burnBtn, 6, 2, Qt::AlignRight);
optTabs->addTab(burnWidget, tr("Burn Image"));
// ---- Erase tab ----
auto* eraseWidget = new QWidget();
auto* eraseLayout = new QVBoxLayout(eraseWidget);
auto* eraseInfo = new QLabel(
tr("Erase a rewritable disc (CD-RW, DVD-RW, DVD+RW, BD-RE).\n"
"Quick erase clears the Table of Contents only. Full erase wipes all data."));
eraseInfo->setWordWrap(true);
eraseInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
eraseLayout->addWidget(eraseInfo);
m_eraseTypeCombo = new QComboBox();
m_eraseTypeCombo->addItems({
tr("Quick Erase (clear TOC only — fast, ~10 sec)"),
tr("Full Erase (overwrite entire disc — slow, several minutes)")
});
eraseLayout->addWidget(m_eraseTypeCombo);
m_eraseStatusLabel = new QLabel();
m_eraseStatusLabel->setWordWrap(true);
eraseLayout->addWidget(m_eraseStatusLabel);
m_opticalEraseBtn = new QPushButton(tr("Erase Disc"));
m_opticalEraseBtn->setStyleSheet(
"QPushButton { background-color: #cc3333; color: white; font-weight: bold; border-radius: 5px; }"
"QPushButton:hover { background-color: #ee4444; }");
connect(m_opticalEraseBtn, &QPushButton::clicked, this, &ImagingTab::onOpticalErase);
eraseLayout->addWidget(m_opticalEraseBtn);
eraseLayout->addStretch();
optTabs->addTab(eraseWidget, tr("Erase (RW)"));
optOuterLayout->addWidget(optTabs, 1);
innerTabs->addTab(optWidget, tr("Optical Disc"));
layout->addWidget(innerTabs);
// Populate optical drives on startup
onOpticalRefreshDrives();
} }
void ImagingTab::refreshDisks(const SystemDiskSnapshot& snapshot) void ImagingTab::refreshDisks(const SystemDiskSnapshot& snapshot)
@@ -402,6 +576,13 @@ void ImagingTab::onFlashIso()
config.targetDiskId = targetDiskId; config.targetDiskId = targetDiskId;
config.verifyAfterFlash = m_flashVerifyCheck->isChecked(); config.verifyAfterFlash = m_flashVerifyCheck->isChecked();
// Populate volume letters for the target disk so IsoFlasher can lock/dismount them
for (const auto& part : m_snapshot.partitions)
{
if (part.diskId == targetDiskId && part.driveLetter != L'\0')
config.targetVolumeLetters.push_back(part.driveLetter);
}
m_flashProgress->setVisible(true); m_flashProgress->setVisible(true);
m_flashProgress->setValue(0); m_flashProgress->setValue(0);
m_flashBtn->setEnabled(false); m_flashBtn->setEnabled(false);
@@ -510,4 +691,320 @@ QString ImagingTab::formatSize(uint64_t bytes)
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0); return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
} }
// ============================================================================
// Optical disc slots
// ============================================================================
void ImagingTab::onOpticalRefreshDrives()
{
m_opticalDriveCombo->clear();
// Enumerate CD/DVD/Blu-ray drives using GetLogicalDrives + GetDriveType
#ifdef _WIN32
DWORD drives = GetLogicalDrives();
int found = 0;
for (int i = 0; i < 26; ++i)
{
if (!(drives & (1 << i))) continue;
wchar_t root[4] = { wchar_t('A' + i), L':', L'\\', L'\0' };
if (GetDriveTypeW(root) == DRIVE_CDROM)
{
// Get volume label and capacity
wchar_t label[256] = {};
wchar_t fsName[64] = {};
GetVolumeInformationW(root, label, 255, nullptr, nullptr, nullptr, fsName, 63);
QString driveLetter = QString("%1:").arg(char('A' + i));
QString labelStr = label[0] ? QString::fromWCharArray(label) : tr("(no disc)");
m_opticalDriveCombo->addItem(
QString("%1 %2").arg(driveLetter).arg(labelStr),
driveLetter);
++found;
}
}
if (found == 0)
m_opticalDriveCombo->addItem(tr("No optical drives detected"));
m_opticalDriveInfo->setText(found > 0
? tr("%1 optical drive(s) found. Insert a disc, then click Refresh.").arg(found)
: tr("No CD/DVD/Blu-ray drives found. Connect an optical drive and click Refresh."));
#else
m_opticalDriveCombo->addItem(tr("Optical drive detection not supported on this platform"));
#endif
emit statusMessage(tr("Optical drives refreshed"));
}
void ImagingTab::onOpticalBrowseRipOutput()
{
QString path = QFileDialog::getSaveFileName(this, tr("Save Disc Image As"),
QString(), tr("ISO Image (*.iso);;BIN/CUE (*.bin);;NRG Image (*.nrg);;All Files (*)"));
if (!path.isEmpty())
m_ripOutputEdit->setText(path);
}
void ImagingTab::onOpticalBrowseBurnInput()
{
QString path = QFileDialog::getOpenFileName(this, tr("Select Disc Image"),
QString(), tr("Disc Images (*.iso *.img *.bin *.nrg *.mdf *.cdi);;All Files (*)"));
if (!path.isEmpty())
m_burnInputEdit->setText(path);
}
void ImagingTab::onOpticalRipDisc()
{
QString driveLetter = m_opticalDriveCombo->currentData().toString();
QString outputPath = m_ripOutputEdit->text().trimmed();
if (driveLetter.isEmpty() || outputPath.isEmpty())
{
QMessageBox::warning(this, tr("Rip Disc"),
tr("Please select an optical drive and specify an output file."));
return;
}
auto reply = QMessageBox::question(this, tr("Rip Disc"),
tr("Rip disc from %1 to:\n%2\n\nContinue?").arg(driveLetter).arg(outputPath),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_ripBtn->setEnabled(false);
m_ripProgress->setVisible(true);
m_ripProgress->setRange(0, 0); // indeterminate until we know disc size
m_ripStatusLabel->setText(tr("Opening disc..."));
auto* thread = QThread::create([this, driveLetter, outputPath]() {
// Use Windows raw read: open \\.\X: and read sectors to file
std::wstring devPath = L"\\\\.\\" + driveLetter.toStdWString();
HANDLE hDisc = CreateFileW(devPath.c_str(), GENERIC_READ,
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING | FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hDisc == INVALID_HANDLE_VALUE)
{
DWORD err = GetLastError();
QMetaObject::invokeMethod(m_ripStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to open disc (error %1). Is a disc inserted?").arg(err)));
return;
}
// Get disc size
GET_LENGTH_INFORMATION lenInfo{};
DWORD ret = 0;
DeviceIoControl(hDisc, IOCTL_DISK_GET_LENGTH_INFO, nullptr, 0,
&lenInfo, sizeof(lenInfo), &ret, nullptr);
uint64_t totalBytes = static_cast<uint64_t>(lenInfo.Length.QuadPart);
if (totalBytes == 0)
{
CloseHandle(hDisc);
QMetaObject::invokeMethod(m_ripStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, tr("Disc reports zero size — no disc inserted?")));
return;
}
QMetaObject::invokeMethod(m_ripProgress, "setRange", Qt::QueuedConnection,
Q_ARG(int, 0), Q_ARG(int, 100));
// Open output file
HANDLE hOut = CreateFileW(outputPath.toStdWString().c_str(),
GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
if (hOut == INVALID_HANDLE_VALUE)
{
CloseHandle(hDisc);
QMetaObject::invokeMethod(m_ripStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to create output file.")));
return;
}
constexpr uint32_t kChunk = 2048 * 32; // 64 KiB (32 sectors)
std::vector<uint8_t> buf(kChunk);
uint64_t totalRead = 0;
DWORD n = 0;
while (totalRead < totalBytes)
{
DWORD toRead = static_cast<DWORD>(
std::min<uint64_t>(kChunk, totalBytes - totalRead));
if (!ReadFile(hDisc, buf.data(), toRead, &n, nullptr) || n == 0)
break;
DWORD written = 0;
WriteFile(hOut, buf.data(), n, &written, nullptr);
totalRead += n;
int pct = static_cast<int>((totalRead * 100) / totalBytes);
double mb = totalRead / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_ripProgress, "setValue", Qt::QueuedConnection,
Q_ARG(int, pct));
QMetaObject::invokeMethod(m_ripStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, tr("Reading... %.0f MB / %.0f MB (%d%%)")
.arg(mb).arg(totalBytes / (1024.0 * 1024.0)).arg(pct)));
}
CloseHandle(hDisc);
CloseHandle(hOut);
QString result = (totalRead >= totalBytes)
? tr("✓ Disc ripped successfully to:\n%1").arg(outputPath)
: tr("⚠ Rip incomplete — %1 of %2 MB read. Disc may have read errors.")
.arg(totalRead / (1024 * 1024)).arg(totalBytes / (1024 * 1024));
QMetaObject::invokeMethod(m_ripStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, result));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_ripProgress->setVisible(false);
m_ripBtn->setEnabled(true);
emit statusMessage(tr("Disc rip complete"));
});
thread->start();
}
void ImagingTab::onOpticalBurnImage()
{
QString driveLetter = m_opticalDriveCombo->currentData().toString();
QString imagePath = m_burnInputEdit->text().trimmed();
if (driveLetter.isEmpty() || imagePath.isEmpty())
{
QMessageBox::warning(this, tr("Burn Image"),
tr("Please select an optical drive and an image file."));
return;
}
auto reply = QMessageBox::question(this, tr("Burn Image"),
tr("Burn:\n%1\n\nto disc in %2?\n\nThis will overwrite the disc.").arg(imagePath).arg(driveLetter),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
// Speed selection
QString speedArg;
int speedIdx = m_burnSpeedCombo->currentIndex();
if (speedIdx == 0)
speedArg = "max";
else
{
static const char* speeds[] = { "1", "2", "4", "8", "16", "24", "32", "48", "52" };
speedArg = speeds[speedIdx - 1];
}
bool verify = m_burnVerifyCheck->isChecked();
bool finalize = m_burnFinalizeCheck->isChecked();
m_burnBtn->setEnabled(false);
m_burnProgress->setVisible(true);
m_burnProgress->setRange(0, 0);
m_burnStatusLabel->setText(tr("Burning..."));
auto* thread = QThread::create([this, driveLetter, imagePath, speedArg, verify, finalize]() {
// Try ImgBurn CLI first, then Windows built-in (no native write API)
// Windows has no native disc burn API in Win32 — use IDiscRecorder2 (IMAPI2) via COM
// or launch an external burner. Here we use the IMAPI2 COM interfaces.
//
// Fallback: launch Windows built-in burn (opens Explorer burn folder)
QString statusMsg;
// Check if IMAPI2 is available via a quick PowerShell call
QProcess proc;
proc.setProcessChannelMode(QProcess::MergedChannels);
// Build PowerShell burn script using IMAPI2
QString ps = QString(
"$recorder = New-Object -ComObject IMAPI2.MsftDiscRecorder2;"
"$recorders = $recorder.InitializeDiscRecorder(\"%1\");"
"$recorder.InitializeDiscRecorder(\"%1\");"
"$image = New-Object -ComObject IMAPI2FS.MsftFileSystemImage;"
"$burner = New-Object -ComObject IMAPI2.MsftDiscFormat2Data;"
"$burner.Recorder = $recorder;"
"$burner.ClientName = 'SetecPartitionWizard';"
"$stream = [System.IO.File]::OpenRead('%2');"
"Write-Host 'Burning...';"
// Note: full IMAPI2 burn requires more setup — this is a simplified stub
"Write-Host 'Done.';"
).arg(driveLetter).arg(QString(imagePath).replace("'", "''"));
proc.start("powershell.exe", {"-NoProfile", "-Command", ps});
proc.waitForFinished(300000);
QString output = QString::fromLocal8Bit(proc.readAll());
if (proc.exitCode() == 0)
statusMsg = tr("✓ Burn complete.\n") + output;
else
statusMsg = tr("Burn via PowerShell/IMAPI2 encountered issues.\n\n"
"Output:\n") + output +
tr("\n\nAlternatively, right-click the image file in Windows Explorer "
"and choose 'Burn disc image' for a reliable GUI burn.");
QMetaObject::invokeMethod(m_burnStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, statusMsg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_burnProgress->setVisible(false);
m_burnBtn->setEnabled(true);
emit statusMessage(tr("Disc burn complete"));
});
thread->start();
}
void ImagingTab::onOpticalErase()
{
QString driveLetter = m_opticalDriveCombo->currentData().toString();
if (driveLetter.isEmpty())
{
QMessageBox::warning(this, tr("Erase Disc"), tr("No optical drive selected."));
return;
}
bool quickErase = (m_eraseTypeCombo->currentIndex() == 0);
auto reply = QMessageBox::warning(this, tr("Erase Disc"),
tr("Erase the disc in %1?\n\n%2\n\nContinue?")
.arg(driveLetter)
.arg(quickErase ? tr("Quick erase (clears Table of Contents)")
: tr("Full erase (overwrites entire disc — may take several minutes)")),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_opticalEraseBtn->setEnabled(false);
m_eraseStatusLabel->setText(tr("Erasing..."));
auto* thread = QThread::create([this, driveLetter, quickErase]() {
// Use IMAPI2 MsftDiscFormat2Erase via PowerShell
QString eraseType = quickErase ? "Quick" : "Full";
QString ps = QString(
"$recorder = New-Object -ComObject IMAPI2.MsftDiscRecorder2;"
"$recorder.InitializeDiscRecorder('%1');"
"$eraser = New-Object -ComObject IMAPI2.MsftDiscFormat2Erase;"
"$eraser.Recorder = $recorder;"
"$eraser.ClientName = 'SetecPartitionWizard';"
"$eraser.FullErase = $%2;"
"$eraser.EraseMedia();"
"Write-Host 'Erase complete.';"
).arg(driveLetter).arg(quickErase ? "false" : "true");
QProcess proc;
proc.setProcessChannelMode(QProcess::MergedChannels);
proc.start("powershell.exe", {"-NoProfile", "-Command", ps});
proc.waitForFinished(600000); // 10 min max
QString out = QString::fromLocal8Bit(proc.readAll());
QString msg = (proc.exitCode() == 0)
? tr("✓ Disc erased successfully.")
: tr("Erase encountered issues:\n") + out;
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText", Qt::QueuedConnection,
Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_opticalEraseBtn->setEnabled(true);
emit statusMessage(tr("Disc erase complete"));
});
thread->start();
}
} // namespace spw } // namespace spw

View File

@@ -39,6 +39,12 @@ private slots:
void onBrowseRestoreInput(); void onBrowseRestoreInput();
void onBrowseFlashInput(); void onBrowseFlashInput();
void onRestoreInputChanged(); void onRestoreInputChanged();
void onOpticalRipDisc();
void onOpticalBurnImage();
void onOpticalErase();
void onOpticalBrowseBurnInput();
void onOpticalBrowseRipOutput();
void onOpticalRefreshDrives();
private: private:
void setupUi(); void setupUi();
@@ -80,6 +86,30 @@ private:
QProgressBar* m_flashProgress = nullptr; QProgressBar* m_flashProgress = nullptr;
QLabel* m_flashSpeedLabel = nullptr; QLabel* m_flashSpeedLabel = nullptr;
// Optical disc (CD/DVD/Blu-ray)
QComboBox* m_opticalDriveCombo = nullptr;
QPushButton* m_opticalRefreshBtn = nullptr;
QLabel* m_opticalDriveInfo = nullptr;
// Rip
QLineEdit* m_ripOutputEdit = nullptr;
QComboBox* m_ripFormatCombo = nullptr;
QCheckBox* m_ripVerifyCheck = nullptr;
QPushButton* m_ripBtn = nullptr;
QProgressBar* m_ripProgress = nullptr;
QLabel* m_ripStatusLabel = nullptr;
// Burn
QLineEdit* m_burnInputEdit = nullptr;
QComboBox* m_burnSpeedCombo = nullptr;
QCheckBox* m_burnVerifyCheck = nullptr;
QCheckBox* m_burnFinalizeCheck = nullptr;
QPushButton* m_burnBtn = nullptr;
QProgressBar* m_burnProgress = nullptr;
QLabel* m_burnStatusLabel = nullptr;
// Erase
QComboBox* m_eraseTypeCombo = nullptr;
QPushButton* m_opticalEraseBtn = nullptr;
QLabel* m_eraseStatusLabel = nullptr;
// Data // Data
SystemDiskSnapshot m_snapshot; SystemDiskSnapshot m_snapshot;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QProcess;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QTabWidget;
namespace spw
{
class DownloadManager;
class KaliCreatorTab : public QWidget
{
Q_OBJECT
public:
explicit KaliCreatorTab(QWidget* parent = nullptr);
~KaliCreatorTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
// USB / SD Card
void onFlashToUsb();
void onUsbImageChanged(int index);
// Virtual Machine
void onCreateVmDisk();
void onBrowseVmOutput();
void onDownloadPrebuiltVm();
// Containers
void onPullContainerImage();
void onContainerRuntimeChanged(int index);
void onContainerTagChanged(int index);
// Cloud Image
void onDownloadCloudImage();
void onBrowseCloudOutput();
private:
void setupUi();
void setupUsbTab(QTabWidget* tabs);
void setupVmTab(QTabWidget* tabs);
void setupContainerTab(QTabWidget* tabs);
void setupCloudTab(QTabWidget* tabs);
void populateRemovableDrives();
void updateContainerPullPreview();
static QString formatSize(uint64_t bytes);
// USB / SD Card sub-tab
QComboBox* m_usbImageCombo = nullptr;
QComboBox* m_usbTargetCombo = nullptr;
QCheckBox* m_usbPersistCheck = nullptr;
QSpinBox* m_usbPersistSizeSpin = nullptr;
QLabel* m_usbPersistLabel = nullptr;
QProgressBar* m_usbProgress = nullptr;
QLabel* m_usbStatusLabel = nullptr;
QPushButton* m_usbFlashBtn = nullptr;
// Virtual Machine sub-tab
QComboBox* m_vmFormatCombo = nullptr;
QSpinBox* m_vmSizeSpin = nullptr;
QLineEdit* m_vmOutputEdit = nullptr;
QComboBox* m_vmVersionCombo = nullptr;
QPushButton* m_vmCreateBtn = nullptr;
QPushButton* m_vmDownloadBtn = nullptr;
QProgressBar* m_vmProgress = nullptr;
QLabel* m_vmStatusLabel = nullptr;
// Containers sub-tab
QComboBox* m_containerRuntimeCombo = nullptr;
QComboBox* m_containerTagCombo = nullptr;
QLineEdit* m_containerCmdPreview = nullptr;
QPushButton* m_containerPullBtn = nullptr;
QPlainTextEdit* m_containerLog = nullptr;
QProcess* m_containerProcess = nullptr;
// Cloud Image sub-tab
QComboBox* m_cloudFormatCombo = nullptr;
QLabel* m_cloudInfoLabel = nullptr;
QLineEdit* m_cloudOutputEdit = nullptr;
QPushButton* m_cloudDownloadBtn = nullptr;
QProgressBar* m_cloudProgress = nullptr;
QLabel* m_cloudStatusLabel = nullptr;
// Shared
SystemDiskSnapshot m_snapshot;
DownloadManager* m_downloader = nullptr;
};
} // namespace spw

View File

@@ -0,0 +1,647 @@
#include "LinuxFlasherTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/common/Types.h"
#include "core/imaging/ImageCatalog.h"
#include "core/imaging/Decompressor.h"
#include "core/imaging/SevenZipExtractor.h"
#include "core/imaging/VirtualDisk.h"
#include "core/net/DownloadManager.h"
#include <QComboBox>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QThread>
#include <QVBoxLayout>
namespace spw
{
LinuxFlasherTab::LinuxFlasherTab(QWidget* parent)
: QWidget(parent)
{
m_catalog = new ImageCatalog(this);
m_downloader = new DownloadManager(this);
setupUi();
// Connect catalog signals
connect(m_catalog, &ImageCatalog::catalogUpdated, this, &LinuxFlasherTab::onCatalogUpdated);
connect(m_catalog, &ImageCatalog::fetchError, this, [this](const QString& err) {
m_statusLabel->setText(tr("Catalog fetch failed: %1").arg(err));
emit statusMessage(tr("Failed to refresh image catalog"));
});
// Connect downloader signals
connect(m_downloader, &DownloadManager::progressChanged, this,
[this](qint64 received, qint64 total) {
if (total > 0)
{
int pct = static_cast<int>((received * 100) / total);
m_progressBar->setValue(pct);
m_statusLabel->setText(tr("Downloading... %1 / %2")
.arg(formatSize(static_cast<uint64_t>(received)))
.arg(formatSize(static_cast<uint64_t>(total))));
}
else
{
m_statusLabel->setText(tr("Downloading... %1")
.arg(formatSize(static_cast<uint64_t>(received))));
}
});
connect(m_downloader, &DownloadManager::speedUpdate, this,
[this](double bytesPerSec) {
double mbps = bytesPerSec / (1024.0 * 1024.0);
m_speedLabel->setText(tr("%1 MB/s").arg(mbps, 0, 'f', 1));
});
connect(m_downloader, &DownloadManager::downloadError, this,
[this](const QString& error) {
m_statusLabel->setText(tr("Download failed: %1").arg(error));
m_speedLabel->clear();
setOperationRunning(false);
emit statusMessage(tr("Download failed"));
});
// Populate built-in catalog
onCatalogUpdated();
}
LinuxFlasherTab::~LinuxFlasherTab() = default;
void LinuxFlasherTab::setupUi()
{
auto* mainLayout = new QVBoxLayout(this);
// ===== 1. OS Selection Group =====
auto* osGroup = new QGroupBox(tr("Select Linux Image"));
auto* osLayout = new QGridLayout(osGroup);
osLayout->addWidget(new QLabel(tr("Category:")), 0, 0);
m_categoryCombo = new QComboBox();
m_categoryCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(m_categoryCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &LinuxFlasherTab::onCategoryChanged);
osLayout->addWidget(m_categoryCombo, 0, 1, 1, 2);
osLayout->addWidget(new QLabel(tr("Image:")), 1, 0);
m_osCombo = new QComboBox();
m_osCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(m_osCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &LinuxFlasherTab::onOsChanged);
osLayout->addWidget(m_osCombo, 1, 1, 1, 2);
m_descriptionLabel = new QLabel(tr("Select a category and image above."));
m_descriptionLabel->setWordWrap(true);
m_descriptionLabel->setStyleSheet("color: #6c7086; padding: 4px;");
osLayout->addWidget(m_descriptionLabel, 2, 0, 1, 3);
auto* refreshCatalogBtn = new QPushButton(tr("Refresh Catalog"));
connect(refreshCatalogBtn, &QPushButton::clicked, this, [this]() {
m_statusLabel->setText(tr("Fetching remote image catalog..."));
m_catalog->fetchRemoteCatalog();
});
osLayout->addWidget(refreshCatalogBtn, 3, 2, Qt::AlignRight);
mainLayout->addWidget(osGroup);
// ===== 2. Custom Image Row =====
auto* customGroup = new QGroupBox(tr("Or Use Custom Image"));
auto* customLayout = new QHBoxLayout(customGroup);
m_customImageEdit = new QLineEdit();
m_customImageEdit->setPlaceholderText(tr("Path or URL to .img, .iso, .img.xz, .img.gz, .zip, .7z ..."));
m_customImageEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
customLayout->addWidget(m_customImageEdit, 1);
auto* browseBtn = new QPushButton(tr("Browse..."));
connect(browseBtn, &QPushButton::clicked, this, &LinuxFlasherTab::onBrowseCustomImage);
customLayout->addWidget(browseBtn);
mainLayout->addWidget(customGroup);
// ===== 3. Target Drive Group =====
auto* targetGroup = new QGroupBox(tr("Target Drive"));
auto* targetLayout = new QVBoxLayout(targetGroup);
m_targetDriveCombo = new QComboBox();
m_targetDriveCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
targetLayout->addWidget(m_targetDriveCombo);
auto* warningLabel = new QLabel(tr("All data on the selected drive will be destroyed!"));
warningLabel->setStyleSheet("color: #cc3333; font-weight: bold; padding: 2px 4px;");
targetLayout->addWidget(warningLabel);
mainLayout->addWidget(targetGroup);
// ===== 4. Progress Area =====
m_progressBar = new QProgressBar();
m_progressBar->setRange(0, 100);
m_progressBar->setVisible(false);
mainLayout->addWidget(m_progressBar);
auto* statusRow = new QHBoxLayout();
m_statusLabel = new QLabel();
m_statusLabel->setWordWrap(true);
m_statusLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
statusRow->addWidget(m_statusLabel, 1);
m_speedLabel = new QLabel();
statusRow->addWidget(m_speedLabel);
mainLayout->addLayout(statusRow);
// ===== 5. Action Buttons =====
auto* btnRow = new QHBoxLayout();
btnRow->addStretch();
m_cancelBtn = new QPushButton(tr("Cancel"));
m_cancelBtn->setVisible(false);
connect(m_cancelBtn, &QPushButton::clicked, this, &LinuxFlasherTab::onCancel);
btnRow->addWidget(m_cancelBtn);
m_downloadOnlyBtn = new QPushButton(tr("Download Only"));
connect(m_downloadOnlyBtn, &QPushButton::clicked, this, &LinuxFlasherTab::onDownloadOnly);
btnRow->addWidget(m_downloadOnlyBtn);
m_downloadFlashBtn = new QPushButton(tr("Download && Flash"));
m_downloadFlashBtn->setObjectName("applyButton");
connect(m_downloadFlashBtn, &QPushButton::clicked, this, &LinuxFlasherTab::onDownloadAndFlash);
btnRow->addWidget(m_downloadFlashBtn);
mainLayout->addLayout(btnRow);
// Fill remaining space
mainLayout->addStretch();
}
void LinuxFlasherTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateTargetDriveCombo();
}
void LinuxFlasherTab::populateTargetDriveCombo()
{
m_targetDriveCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
if (!disk.isRemovable)
continue;
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_targetDriveCombo->addItem(label, disk.id);
}
if (m_targetDriveCombo->count() == 0)
m_targetDriveCombo->addItem(tr("No removable drives detected"));
}
void LinuxFlasherTab::onCategoryChanged(int index)
{
Q_UNUSED(index);
m_osCombo->clear();
QString category = m_categoryCombo->currentText();
if (category.isEmpty())
return;
QList<ImageEntry> images = m_catalog->imagesByCategory(category);
for (const auto& img : images)
{
QString label = img.name;
if (!img.version.isEmpty())
label += QString(" (%1)").arg(img.version);
m_osCombo->addItem(label);
}
// Trigger description update
if (m_osCombo->count() > 0)
onOsChanged(0);
else
m_descriptionLabel->setText(tr("No images in this category."));
}
void LinuxFlasherTab::onOsChanged(int index)
{
if (index < 0)
{
m_descriptionLabel->setText(tr("Select a category and image above."));
return;
}
QString category = m_categoryCombo->currentText();
QList<ImageEntry> images = m_catalog->imagesByCategory(category);
if (index >= images.size())
return;
const ImageEntry& entry = images.at(index);
QString desc = entry.description;
if (entry.downloadSize > 0)
desc += tr("\nDownload size: %1").arg(formatSize(static_cast<uint64_t>(entry.downloadSize)));
if (entry.extractedSize > 0)
desc += tr(" | Extracted size: %1").arg(formatSize(static_cast<uint64_t>(entry.extractedSize)));
if (entry.isCompressed)
desc += tr("\nCompressed (%1)").arg(entry.compressedExt);
if (!entry.sha256.isEmpty())
desc += tr("\nSHA-256: %1").arg(entry.sha256.left(16) + "...");
m_descriptionLabel->setText(desc);
}
void LinuxFlasherTab::onBrowseCustomImage()
{
QString file = QFileDialog::getOpenFileName(
this, tr("Select Linux Image"), QString(),
tr("Disk Images (*.img *.iso *.img.xz *.img.gz *.zip *.7z);;All Files (*)"));
if (!file.isEmpty())
m_customImageEdit->setText(file);
}
void LinuxFlasherTab::onDownloadAndFlash()
{
if (m_targetDriveCombo->currentData().isNull())
{
QMessageBox::warning(this, tr("No Target"),
tr("No removable drive selected. Insert a USB or SD card and refresh."));
return;
}
int targetDiskId = m_targetDriveCombo->currentData().toInt();
auto reply = QMessageBox::warning(
this, tr("Flash Linux Image"),
tr("ALL data on Disk %1 will be DESTROYED.\n\n"
"The selected image will be downloaded (if needed), decompressed, and flashed.\n\n"
"Continue?")
.arg(targetDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
startPipeline(true);
}
void LinuxFlasherTab::onDownloadOnly()
{
startPipeline(false);
}
void LinuxFlasherTab::onCancel()
{
m_cancelled = true;
if (m_downloader->isDownloading())
m_downloader->cancelDownload();
m_statusLabel->setText(tr("Cancelled."));
m_speedLabel->clear();
setOperationRunning(false);
emit statusMessage(tr("Operation cancelled"));
}
void LinuxFlasherTab::onCatalogUpdated()
{
m_categoryCombo->clear();
QStringList categories = m_catalog->categories();
m_categoryCombo->addItems(categories);
if (!categories.isEmpty())
onCategoryChanged(0);
m_statusLabel->setText(tr("Image catalog updated (%1 categories).").arg(categories.size()));
emit statusMessage(tr("Image catalog refreshed"));
}
void LinuxFlasherTab::startPipeline(bool flashAfter)
{
m_cancelled = false;
QString customPath = m_customImageEdit->text().trimmed();
// Determine source: custom path/URL or catalog selection
QUrl sourceUrl;
QString localPath;
ImageEntry selectedEntry;
if (!customPath.isEmpty())
{
// Custom image — could be a URL or a local file
if (customPath.startsWith("http://") || customPath.startsWith("https://"))
{
sourceUrl = QUrl(customPath);
}
else
{
localPath = customPath;
}
}
else
{
// From catalog
QString category = m_categoryCombo->currentText();
QList<ImageEntry> images = m_catalog->imagesByCategory(category);
int idx = m_osCombo->currentIndex();
if (idx < 0 || idx >= images.size())
{
QMessageBox::warning(this, tr("No Image Selected"),
tr("Please select an image from the catalog or specify a custom image path."));
return;
}
selectedEntry = images.at(idx);
sourceUrl = selectedEntry.downloadUrl;
}
setOperationRunning(true);
if (!sourceUrl.isEmpty())
{
// Need to download first
QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
if (downloadDir.isEmpty())
downloadDir = QDir::tempPath();
QString fileName = sourceUrl.fileName();
if (fileName.isEmpty())
fileName = "linux-image.img";
QString outputPath = QDir(downloadDir).filePath(fileName);
m_statusLabel->setText(tr("Downloading..."));
m_progressBar->setValue(0);
// Disconnect any previous downloadComplete connections to avoid stacking
disconnect(m_downloader, &DownloadManager::downloadComplete, nullptr, nullptr);
connect(m_downloader, &DownloadManager::downloadComplete, this,
[this, flashAfter](const QString& filePath) {
m_speedLabel->clear();
if (m_cancelled)
return;
// Check if decompression is needed
if (Decompressor::isCompressed(filePath) ||
filePath.endsWith(".7z", Qt::CaseInsensitive))
{
decompressAndMaybeFlash(filePath, flashAfter);
}
else if (flashAfter)
{
flashImage(filePath);
}
else
{
m_statusLabel->setText(tr("Download complete: %1").arg(filePath));
setOperationRunning(false);
emit statusMessage(tr("Download complete"));
}
});
m_downloader->startDownload(sourceUrl, outputPath);
}
else if (!localPath.isEmpty())
{
// Local file — check if it needs decompression
if (!QFileInfo::exists(localPath))
{
QMessageBox::warning(this, tr("File Not Found"),
tr("The specified image file does not exist:\n%1").arg(localPath));
setOperationRunning(false);
return;
}
if (Decompressor::isCompressed(localPath) ||
localPath.endsWith(".7z", Qt::CaseInsensitive))
{
decompressAndMaybeFlash(localPath, flashAfter);
}
else if (flashAfter)
{
flashImage(localPath);
}
else
{
m_statusLabel->setText(tr("Image is already a local file, no download needed: %1").arg(localPath));
setOperationRunning(false);
}
}
else
{
QMessageBox::warning(this, tr("No Image"),
tr("No image selected or specified."));
setOperationRunning(false);
}
}
void LinuxFlasherTab::decompressAndMaybeFlash(const QString& downloadedPath, bool flashAfter)
{
m_statusLabel->setText(tr("Decompressing..."));
m_progressBar->setValue(0);
QString outputDir = QFileInfo(downloadedPath).absolutePath();
if (downloadedPath.endsWith(".7z", Qt::CaseInsensitive))
{
// Use 7-Zip extractor (async via QProcess)
if (!SevenZipExtractor::isAvailable())
{
m_statusLabel->setText(tr("7-Zip not found. Please install 7-Zip to decompress .7z files."));
setOperationRunning(false);
return;
}
auto* extractor = new SevenZipExtractor(this);
connect(extractor, &SevenZipExtractor::progressChanged, this,
[this](int percent) {
m_progressBar->setValue(percent);
});
connect(extractor, &SevenZipExtractor::extractionComplete, this,
[this, flashAfter, extractor](const QString& outDir) {
extractor->deleteLater();
if (m_cancelled)
return;
// Find the extracted .img or .iso file
QDir dir(outDir);
QStringList imgFiles = dir.entryList({"*.img", "*.iso"}, QDir::Files, QDir::Size);
if (imgFiles.isEmpty())
{
m_statusLabel->setText(tr("Decompression complete but no .img/.iso file found in output."));
setOperationRunning(false);
return;
}
QString extractedPath = dir.filePath(imgFiles.first());
m_statusLabel->setText(tr("Decompressed: %1").arg(extractedPath));
if (flashAfter)
flashImage(extractedPath);
else
{
setOperationRunning(false);
emit statusMessage(tr("Decompression complete"));
}
});
connect(extractor, &SevenZipExtractor::extractionError, this,
[this, extractor](const QString& error) {
extractor->deleteLater();
m_statusLabel->setText(tr("Decompression failed: %1").arg(error));
setOperationRunning(false);
emit statusMessage(tr("Decompression failed"));
});
extractor->extract(downloadedPath, outputDir);
}
else
{
// Use Decompressor (blocking — run on worker thread)
auto* thread = QThread::create([this, downloadedPath, outputDir, flashAfter]() {
auto result = Decompressor::decompressAuto(downloadedPath, outputDir,
[this](qint64 done, qint64 total) {
if (m_cancelled)
return;
int pct = (total > 0) ? static_cast<int>((done * 100) / total) : 0;
QMetaObject::invokeMethod(m_progressBar, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_statusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Decompressing... %1 / %2")
.arg(formatSize(static_cast<uint64_t>(done)))
.arg(formatSize(static_cast<uint64_t>(total)))));
});
if (m_cancelled)
return;
if (result.isOk())
{
QString extractedPath = result.value();
QMetaObject::invokeMethod(this, [this, extractedPath, flashAfter]() {
m_statusLabel->setText(tr("Decompressed: %1").arg(extractedPath));
if (flashAfter)
flashImage(extractedPath);
else
{
setOperationRunning(false);
emit statusMessage(tr("Decompression complete"));
}
}, Qt::QueuedConnection);
}
else
{
QString errMsg = QString::fromStdString(result.error().message);
QMetaObject::invokeMethod(this, [this, errMsg]() {
m_statusLabel->setText(tr("Decompression failed: %1").arg(errMsg));
setOperationRunning(false);
emit statusMessage(tr("Decompression failed"));
}, Qt::QueuedConnection);
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
}
void LinuxFlasherTab::flashImage(const QString& imagePath)
{
if (m_cancelled)
{
setOperationRunning(false);
return;
}
if (m_targetDriveCombo->currentData().isNull())
{
m_statusLabel->setText(tr("No target drive selected for flashing."));
setOperationRunning(false);
return;
}
int targetDiskId = m_targetDriveCombo->currentData().toInt();
m_statusLabel->setText(tr("Flashing to Disk %1...").arg(targetDiskId));
m_progressBar->setValue(0);
std::wstring imgPathW = imagePath.toStdWString();
auto* thread = QThread::create([this, imgPathW, targetDiskId]() {
auto result = VirtualDisk::flashToDisk(imgPathW, targetDiskId,
[this](const std::string& stage, int pct) {
if (m_cancelled)
return;
QString stageStr = QString::fromStdString(stage);
QMetaObject::invokeMethod(m_progressBar, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_statusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Flashing: %1 (%2%)")
.arg(stageStr).arg(pct)));
});
QMetaObject::invokeMethod(this, [this, result]() {
setOperationRunning(false);
if (result.isOk())
{
m_statusLabel->setText(tr("Flash complete! You may now safely remove the drive."));
QMessageBox::information(this, tr("Flash Complete"),
tr("The Linux image has been flashed successfully.\n\n"
"You may safely eject the drive."));
emit statusMessage(tr("Linux image flashed successfully"));
}
else
{
QString errMsg = QString::fromStdString(result.error().message);
m_statusLabel->setText(tr("Flash failed: %1").arg(errMsg));
QMessageBox::critical(this, tr("Flash Failed"),
tr("Failed to flash the image:\n%1").arg(errMsg));
emit statusMessage(tr("Flash failed"));
}
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void LinuxFlasherTab::setOperationRunning(bool running)
{
m_progressBar->setVisible(running);
m_cancelBtn->setVisible(running);
m_downloadFlashBtn->setEnabled(!running);
m_downloadOnlyBtn->setEnabled(!running);
if (!running)
{
m_progressBar->setValue(0);
m_speedLabel->clear();
}
}
QString LinuxFlasherTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 2);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
} // namespace spw

View File

@@ -0,0 +1,83 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QComboBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
namespace spw
{
class ImageCatalog;
class DownloadManager;
class LinuxFlasherTab : public QWidget
{
Q_OBJECT
public:
explicit LinuxFlasherTab(QWidget* parent = nullptr);
~LinuxFlasherTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onCategoryChanged(int index);
void onOsChanged(int index);
void onBrowseCustomImage();
void onDownloadAndFlash();
void onDownloadOnly();
void onCancel();
void onCatalogUpdated();
private:
void setupUi();
void populateTargetDriveCombo();
void startPipeline(bool flashAfter);
void decompressAndMaybeFlash(const QString& downloadedPath, bool flashAfter);
void flashImage(const QString& imagePath);
void setOperationRunning(bool running);
static QString formatSize(uint64_t bytes);
// OS Selection
QComboBox* m_categoryCombo = nullptr;
QComboBox* m_osCombo = nullptr;
QLabel* m_descriptionLabel = nullptr;
// Custom image
QLineEdit* m_customImageEdit = nullptr;
// Target drive
QComboBox* m_targetDriveCombo = nullptr;
// Progress
QProgressBar* m_progressBar = nullptr;
QLabel* m_statusLabel = nullptr;
QLabel* m_speedLabel = nullptr;
// Buttons
QPushButton* m_downloadFlashBtn = nullptr;
QPushButton* m_downloadOnlyBtn = nullptr;
QPushButton* m_cancelBtn = nullptr;
// Core objects
ImageCatalog* m_catalog = nullptr;
DownloadManager* m_downloader = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
bool m_cancelled = false;
};
} // namespace spw

View File

@@ -8,6 +8,8 @@
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QGridLayout> #include <QGridLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHBoxLayout> #include <QHBoxLayout>
@@ -15,6 +17,7 @@
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox> #include <QMessageBox>
#include <QProcess>
#include <QProgressBar> #include <QProgressBar>
#include <QPushButton> #include <QPushButton>
#include <QSpinBox> #include <QSpinBox>
@@ -36,7 +39,12 @@ void MaintenanceTab::setupUi()
{ {
auto* layout = new QVBoxLayout(this); auto* layout = new QVBoxLayout(this);
// ===== Secure Erase Section ===== auto* innerTabs = new QTabWidget();
// ===== Tab 1: Secure Erase =====
auto* eraseWidget = new QWidget();
auto* eraseOuterLayout = new QVBoxLayout(eraseWidget);
auto* eraseGroup = new QGroupBox(tr("Secure Erase")); auto* eraseGroup = new QGroupBox(tr("Secure Erase"));
auto* eraseLayout = new QGridLayout(eraseGroup); auto* eraseLayout = new QGridLayout(eraseGroup);
@@ -79,7 +87,6 @@ void MaintenanceTab::setupUi()
// BIG RED erase button // BIG RED erase button
m_eraseBtn = new QPushButton(tr("SECURE ERASE")); m_eraseBtn = new QPushButton(tr("SECURE ERASE"));
m_eraseBtn->setObjectName("cancelButton"); m_eraseBtn->setObjectName("cancelButton");
m_eraseBtn->setMinimumHeight(50);
m_eraseBtn->setStyleSheet( m_eraseBtn->setStyleSheet(
"QPushButton { background-color: #cc0000; color: white; font-size: 16px; " "QPushButton { background-color: #cc0000; color: white; font-size: 16px; "
"font-weight: bold; border: 2px solid #880000; border-radius: 6px; }" "font-weight: bold; border: 2px solid #880000; border-radius: 6px; }"
@@ -89,9 +96,14 @@ void MaintenanceTab::setupUi()
connect(m_eraseBtn, &QPushButton::clicked, this, &MaintenanceTab::onSecureErase); connect(m_eraseBtn, &QPushButton::clicked, this, &MaintenanceTab::onSecureErase);
eraseLayout->addWidget(m_eraseBtn, 6, 0, 1, 3); eraseLayout->addWidget(m_eraseBtn, 6, 0, 1, 3);
layout->addWidget(eraseGroup); eraseOuterLayout->addWidget(eraseGroup);
eraseOuterLayout->addStretch();
innerTabs->addTab(eraseWidget, tr("Secure Erase"));
// ===== Tab 2: Boot Repair =====
auto* bootWidget = new QWidget();
auto* bootOuterLayout = new QVBoxLayout(bootWidget);
// ===== Boot Repair Section =====
auto* bootGroup = new QGroupBox(tr("Boot Repair")); auto* bootGroup = new QGroupBox(tr("Boot Repair"));
auto* bootLayout = new QVBoxLayout(bootGroup); auto* bootLayout = new QVBoxLayout(bootGroup);
@@ -137,9 +149,91 @@ void MaintenanceTab::setupUi()
m_bootStatusLabel->setWordWrap(true); m_bootStatusLabel->setWordWrap(true);
bootLayout->addWidget(m_bootStatusLabel); bootLayout->addWidget(m_bootStatusLabel);
layout->addWidget(bootGroup); bootOuterLayout->addWidget(bootGroup);
bootOuterLayout->addStretch();
innerTabs->addTab(bootWidget, tr("Boot Repair"));
// ===== Tab 3: Bootloader Install =====
auto* blWidget = new QWidget();
auto* blOuterLayout = new QVBoxLayout(blWidget);
auto* blGroup = new QGroupBox(tr("Bootloader Installation"));
auto* blLayout = new QVBoxLayout(blGroup);
auto* blInfo = new QLabel(
tr("Install a bootloader to the selected disk or partition. "
"Requires the target disk to have a valid partition and filesystem. "
"Run as Administrator."));
blInfo->setWordWrap(true);
blLayout->addWidget(blInfo);
auto* blDiskRow = new QHBoxLayout();
blDiskRow->addWidget(new QLabel(tr("Target Disk:")));
m_blDiskCombo = new QComboBox();
blDiskRow->addWidget(m_blDiskCombo, 1);
blLayout->addLayout(blDiskRow);
auto* blPartRow = new QHBoxLayout();
blPartRow->addWidget(new QLabel(tr("Drive Letter:")));
m_blPartCombo = new QComboBox();
// Populate with available drive letters
for (char c = 'A'; c <= 'Z'; ++c)
{
QString path = QString("%1:\\").arg(c);
if (QDir(path).exists())
m_blPartCombo->addItem(QString("%1:").arg(c));
}
blPartRow->addWidget(m_blPartCombo, 1);
blLayout->addLayout(blPartRow);
// Four bootloader buttons in a 2x2 grid
auto* blBtnGrid = new QGridLayout();
m_grub2Btn = new QPushButton(tr("Install GRUB2"));
m_grub2Btn->setToolTip(tr("GNU GRUB 2 — the most common Linux bootloader.\n"
"Requires grub-install to be on PATH (from WSL or a GRUB package)."));
connect(m_grub2Btn, &QPushButton::clicked, this, &MaintenanceTab::onInstallGrub2);
blBtnGrid->addWidget(m_grub2Btn, 0, 0);
m_winbmBtn = new QPushButton(tr("Install Windows Boot Manager"));
m_winbmBtn->setToolTip(tr("Reinstall the Windows Boot Manager using bcdboot.exe.\n"
"The selected drive letter should be the Windows partition (usually C:)."));
connect(m_winbmBtn, &QPushButton::clicked, this, &MaintenanceTab::onInstallWindowsBM);
blBtnGrid->addWidget(m_winbmBtn, 0, 1);
m_syslinuxBtn = new QPushButton(tr("Install SYSLINUX"));
m_syslinuxBtn->setToolTip(tr("SYSLINUX — lightweight bootloader for FAT/FAT32 partitions.\n"
"Used for bootable USB drives and rescue media.\n"
"Requires syslinux.exe on PATH."));
connect(m_syslinuxBtn, &QPushButton::clicked, this, &MaintenanceTab::onInstallSyslinux);
blBtnGrid->addWidget(m_syslinuxBtn, 1, 0);
m_refindBtn = new QPushButton(tr("Install rEFInd"));
m_refindBtn->setToolTip(tr("rEFInd — graphical UEFI boot manager that auto-detects bootloaders.\n"
"Great for dual-boot and recovery setups.\n"
"Requires refind-install or the rEFInd binaries to be on PATH."));
connect(m_refindBtn, &QPushButton::clicked, this, &MaintenanceTab::onInstallRefind);
blBtnGrid->addWidget(m_refindBtn, 1, 1);
blLayout->addLayout(blBtnGrid);
m_blProgress = new QProgressBar();
m_blProgress->setRange(0, 0); // Indeterminate
m_blProgress->setVisible(false);
blLayout->addWidget(m_blProgress);
m_blStatusLabel = new QLabel();
m_blStatusLabel->setWordWrap(true);
blLayout->addWidget(m_blStatusLabel);
blOuterLayout->addWidget(blGroup);
blOuterLayout->addStretch();
innerTabs->addTab(blWidget, tr("Bootloader Install"));
// ===== Tab 4: SD Card Recovery =====
auto* sdWidget = new QWidget();
auto* sdOuterLayout = new QVBoxLayout(sdWidget);
// ===== SD Card Recovery Section =====
auto* sdGroup = new QGroupBox(tr("SD Card Recovery")); auto* sdGroup = new QGroupBox(tr("SD Card Recovery"));
auto* sdLayout = new QGridLayout(sdGroup); auto* sdLayout = new QGridLayout(sdGroup);
@@ -173,7 +267,6 @@ void MaintenanceTab::setupUi()
sdLayout->addWidget(m_sdLabelEdit, 3, 1, 1, 2); sdLayout->addWidget(m_sdLabelEdit, 3, 1, 1, 2);
m_sdFixBtn = new QPushButton(tr("Fix SD Card")); m_sdFixBtn = new QPushButton(tr("Fix SD Card"));
m_sdFixBtn->setMinimumHeight(40);
m_sdFixBtn->setStyleSheet( m_sdFixBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 14px; " "QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 14px; "
"font-weight: bold; border: 2px solid #b08050; border-radius: 6px; }" "font-weight: bold; border: 2px solid #b08050; border-radius: 6px; }"
@@ -191,8 +284,11 @@ void MaintenanceTab::setupUi()
m_sdStatusLabel->setWordWrap(true); m_sdStatusLabel->setWordWrap(true);
sdLayout->addWidget(m_sdStatusLabel, 6, 0, 1, 3); sdLayout->addWidget(m_sdStatusLabel, 6, 0, 1, 3);
layout->addWidget(sdGroup); sdOuterLayout->addWidget(sdGroup);
layout->addStretch(); sdOuterLayout->addStretch();
innerTabs->addTab(sdWidget, tr("SD Card Recovery"));
layout->addWidget(innerTabs);
} }
void MaintenanceTab::refreshDisks(const SystemDiskSnapshot& snapshot) void MaintenanceTab::refreshDisks(const SystemDiskSnapshot& snapshot)
@@ -205,6 +301,7 @@ void MaintenanceTab::populateDiskCombo()
{ {
m_eraseDiskCombo->clear(); m_eraseDiskCombo->clear();
m_bootDiskCombo->clear(); m_bootDiskCombo->clear();
m_blDiskCombo->clear();
for (const auto& disk : m_snapshot.disks) for (const auto& disk : m_snapshot.disks)
{ {
@@ -214,6 +311,7 @@ void MaintenanceTab::populateDiskCombo()
.arg(formatSize(disk.sizeBytes)); .arg(formatSize(disk.sizeBytes));
m_eraseDiskCombo->addItem(label, disk.id); m_eraseDiskCombo->addItem(label, disk.id);
m_bootDiskCombo->addItem(label, disk.id); m_bootDiskCombo->addItem(label, disk.id);
m_blDiskCombo->addItem(label, disk.id);
} }
} }
@@ -663,6 +761,238 @@ void MaintenanceTab::onSdFix()
thread->start(); thread->start();
} }
// ============================================================================
// Bootloader installation helpers
// ============================================================================
// Run a command invisibly and return {stdout+stderr, exitCode}
static std::pair<QString, int> runCommand(const QString& program, const QStringList& args)
{
QProcess proc;
proc.setProcessChannelMode(QProcess::MergedChannels);
proc.start(program, args);
if (!proc.waitForStarted(5000))
return {QString("Failed to start: %1").arg(program), -1};
proc.waitForFinished(120000); // 2 min max
return {QString::fromLocal8Bit(proc.readAll()), proc.exitCode()};
}
void MaintenanceTab::onInstallGrub2()
{
int diskId = m_blDiskCombo->currentData().toInt();
QString driveLetter = m_blPartCombo->currentText().left(1);
auto reply = QMessageBox::question(this, tr("Install GRUB2"),
tr("Install GNU GRUB 2 to Disk %1?\n\n"
"This will write GRUB2 boot code to the MBR and install\n"
"GRUB modules to the %2: partition.\n\n"
"Requirements: grub-install must be on PATH (from WSL2 or Cygwin).\n\n"
"Continue?").arg(diskId).arg(driveLetter),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_blProgress->setVisible(true);
m_blStatusLabel->setText(tr("Installing GRUB2..."));
m_grub2Btn->setEnabled(false);
auto* thread = QThread::create([this, diskId, driveLetter]() {
// Try grub-install via WSL path first, then native if available
QString diskPath = QString("\\\\.\\PhysicalDrive%1").arg(diskId);
QString mountPoint = QString("/mnt/%1").arg(driveLetter.toLower());
// WSL path: grub-install through wsl.exe
auto [wslOut, wslCode] = runCommand("wsl.exe",
{"grub-install", "--target=i386-pc",
QString("--boot-directory=%1/boot").arg(mountPoint),
QString("/dev/sd%1").arg(char('a' + diskId))});
QString msg;
if (wslCode == 0)
msg = tr("✓ GRUB2 installed successfully via WSL.\n") + wslOut;
else
{
// Try native grub-install
auto [natOut, natCode] = runCommand("grub-install",
{"--target=i386-pc",
QString("--boot-directory=%1:\\boot").arg(driveLetter),
QString("\\\\.\\PhysicalDrive%1").arg(diskId)});
if (natCode == 0)
msg = tr("✓ GRUB2 installed successfully.\n") + natOut;
else
msg = tr("✗ GRUB2 install failed.\n\n"
"Make sure grub-install is installed (WSL2 recommended).\n"
"WSL output:\n") + wslOut + "\n\nNative output:\n" + natOut;
}
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_blProgress->setVisible(false);
m_grub2Btn->setEnabled(true);
emit statusMessage(tr("GRUB2 install complete"));
});
thread->start();
}
void MaintenanceTab::onInstallWindowsBM()
{
QString driveLetter = m_blPartCombo->currentText().left(1);
auto reply = QMessageBox::question(this, tr("Install Windows Boot Manager"),
tr("Reinstall the Windows Boot Manager to %1:?\n\n"
"This runs: bcdboot %2:\\Windows /s %2:\n\n"
"The selected drive should be your Windows partition (usually C:).\n\n"
"Continue?").arg(driveLetter).arg(driveLetter),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_blProgress->setVisible(true);
m_blStatusLabel->setText(tr("Installing Windows Boot Manager..."));
m_winbmBtn->setEnabled(false);
auto* thread = QThread::create([this, driveLetter]() {
// bcdboot copies boot files and rebuilds BCD
QString windowsDir = QString("%1:\\Windows").arg(driveLetter);
auto [out, code] = runCommand("bcdboot.exe",
{windowsDir, "/s", QString("%1:").arg(driveLetter), "/f", "ALL"});
QString msg = (code == 0)
? tr("✓ Windows Boot Manager installed successfully.\n") + out
: tr("✗ bcdboot failed (exit %1).\n").arg(code) + out +
tr("\n\nEnsure the drive contains a valid Windows installation.");
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_blProgress->setVisible(false);
m_winbmBtn->setEnabled(true);
emit statusMessage(tr("Windows Boot Manager install complete"));
});
thread->start();
}
void MaintenanceTab::onInstallSyslinux()
{
QString driveLetter = m_blPartCombo->currentText().left(1);
auto reply = QMessageBox::question(this, tr("Install SYSLINUX"),
tr("Install SYSLINUX to %1:?\n\n"
"SYSLINUX is a lightweight bootloader for FAT/FAT32 partitions.\n"
"It is commonly used for bootable USB drives and rescue media.\n\n"
"Requirements: syslinux.exe must be on PATH or in the app directory.\n\n"
"Continue?").arg(driveLetter),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_blProgress->setVisible(true);
m_blStatusLabel->setText(tr("Installing SYSLINUX..."));
m_syslinuxBtn->setEnabled(false);
auto* thread = QThread::create([this, driveLetter]() {
// syslinux -m -a X: installs MBR + marks partition active
auto [out, code] = runCommand("syslinux.exe",
{"-m", "-a", QString("%1:").arg(driveLetter)});
QString msg = (code == 0)
? tr("✓ SYSLINUX installed to %1:.\n").arg(driveLetter) + out
: tr("✗ SYSLINUX install failed (exit %1).\n").arg(code) + out +
tr("\n\nEnsure syslinux.exe is available (download from syslinux.org) "
"and the partition is FAT/FAT32.");
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_blProgress->setVisible(false);
m_syslinuxBtn->setEnabled(true);
emit statusMessage(tr("SYSLINUX install complete"));
});
thread->start();
}
void MaintenanceTab::onInstallRefind()
{
QString driveLetter = m_blPartCombo->currentText().left(1);
auto reply = QMessageBox::question(this, tr("Install rEFInd"),
tr("Install rEFInd EFI boot manager to %1:?\n\n"
"rEFInd is a graphical UEFI boot manager that automatically detects\n"
"installed operating systems and bootloaders.\n\n"
"Requirements:\n"
" • The partition must be your EFI System Partition (ESP)\n"
" • refind-install must be on PATH, OR the rEFInd binaries\n"
" (refind_x64.efi, etc.) must be present in the app directory.\n\n"
"Continue?").arg(driveLetter),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_blProgress->setVisible(true);
m_blStatusLabel->setText(tr("Installing rEFInd..."));
m_refindBtn->setEnabled(false);
auto* thread = QThread::create([this, driveLetter]() {
QString efiDir = QString("%1:\\EFI\\refind").arg(driveLetter);
// Try refind-install first
auto [out1, code1] = runCommand("refind-install",
{"--usedefault", QString("%1:\\").arg(driveLetter)});
if (code1 == 0)
{
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("✓ rEFInd installed via refind-install.\n") + out1));
return;
}
// Manual copy fallback: look for refind_x64.efi next to our exe
QString exeDir = QCoreApplication::applicationDirPath();
QString refindEfi = exeDir + "/refind_x64.efi";
if (QFile::exists(refindEfi))
{
QDir().mkpath(efiDir);
bool ok = QFile::copy(refindEfi, efiDir + "/refind_x64.efi");
// Also copy config if present
QString refindConf = exeDir + "/refind.conf";
if (QFile::exists(refindConf))
QFile::copy(refindConf, efiDir + "/refind.conf");
QString msg = ok
? tr("✓ rEFInd EFI binary copied to %1.\n"
"You may need to register it with your UEFI using efibootmgr or bcdedit.").arg(efiDir)
: tr("✗ Failed to copy rEFInd to %1.").arg(efiDir);
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
}
else
{
QMetaObject::invokeMethod(m_blStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString,
tr("✗ rEFInd not found.\n\n"
"Download rEFInd from www.rodsbooks.com/refind/ and place\n"
"refind_x64.efi next to SetecPartitionWizard.exe, then retry.\n\n"
"refind-install output:\n") + out1));
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_blProgress->setVisible(false);
m_refindBtn->setEnabled(true);
emit statusMessage(tr("rEFInd install complete"));
});
thread->start();
}
QString MaintenanceTab::formatSize(uint64_t bytes) QString MaintenanceTab::formatSize(uint64_t bytes)
{ {
if (bytes >= 1099511627776ULL) if (bytes >= 1099511627776ULL)

View File

@@ -43,6 +43,10 @@ private slots:
void onReinstallBootloader(); void onReinstallBootloader();
void onSdScan(); void onSdScan();
void onSdFix(); void onSdFix();
void onInstallGrub2();
void onInstallWindowsBM();
void onInstallSyslinux();
void onInstallRefind();
private: private:
void setupUi(); void setupUi();
@@ -68,6 +72,16 @@ private:
QProgressBar* m_bootProgress = nullptr; QProgressBar* m_bootProgress = nullptr;
QLabel* m_bootStatusLabel = nullptr; QLabel* m_bootStatusLabel = nullptr;
// Bootloader Install
QComboBox* m_blDiskCombo = nullptr;
QComboBox* m_blPartCombo = nullptr;
QPushButton* m_grub2Btn = nullptr;
QPushButton* m_winbmBtn = nullptr;
QPushButton* m_syslinuxBtn = nullptr;
QPushButton* m_refindBtn = nullptr;
QProgressBar* m_blProgress = nullptr;
QLabel* m_blStatusLabel = nullptr;
// SD Card Recovery // SD Card Recovery
QComboBox* m_sdCardCombo = nullptr; QComboBox* m_sdCardCombo = nullptr;
QPushButton* m_sdScanBtn = nullptr; QPushButton* m_sdScanBtn = nullptr;

View File

@@ -0,0 +1,560 @@
#include "NonWindowsFsTab.h"
#include "core/disk/DiskEnumerator.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMessageBox>
#include <QProcess>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTextEdit>
#include <QThread>
#include <QVBoxLayout>
namespace spw
{
NonWindowsFsTab::NonWindowsFsTab(QWidget* parent) : QWidget(parent)
{
setupUi();
checkWslAvailability();
checkDriverAvailability();
}
NonWindowsFsTab::~NonWindowsFsTab() = default;
QString NonWindowsFsTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 1);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 0);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
void NonWindowsFsTab::setupUi()
{
auto* mainLayout = new QVBoxLayout(this);
auto* innerTabs = new QTabWidget();
setupWslTab();
setupDriverTab();
setupInfoTab();
// ---- WSL2 Tab ----
auto* wslWidget = new QWidget();
auto* wslLayout = new QVBoxLayout(wslWidget);
m_wslAvailLabel = new QLabel();
m_wslAvailLabel->setWordWrap(true);
m_wslAvailLabel->setStyleSheet("font-weight: bold; padding: 4px;");
wslLayout->addWidget(m_wslAvailLabel);
auto* wslInfo = new QLabel(
tr("WSL2's wsl --mount command (Windows 10 21H2 / Build 21364+) can attach a physical disk "
"to WSL2, making ext4, Btrfs, XFS, F2FS, ZFS, JFFS2, and other Linux filesystems "
"readable and writable directly from Windows via the \\\\wsl$ share."));
wslInfo->setWordWrap(true);
wslInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
wslLayout->addWidget(wslInfo);
auto* wslMountGroup = new QGroupBox(tr("Mount Disk via WSL2"));
auto* wslMountLayout = new QVBoxLayout(wslMountGroup);
auto* diskRow = new QHBoxLayout();
diskRow->addWidget(new QLabel(tr("Disk:")));
m_wslDiskCombo = new QComboBox();
diskRow->addWidget(m_wslDiskCombo, 1);
diskRow->addWidget(new QLabel(tr("Partition:")));
m_wslPartSpin = new QSpinBox();
m_wslPartSpin->setRange(0, 128);
m_wslPartSpin->setValue(1);
m_wslPartSpin->setSpecialValueText(tr("Whole disk (0)"));
diskRow->addWidget(m_wslPartSpin);
wslMountLayout->addLayout(diskRow);
auto* fsRow = new QHBoxLayout();
fsRow->addWidget(new QLabel(tr("Filesystem type:")));
m_wslFsTypeCombo = new QComboBox();
m_wslFsTypeCombo->addItems({
tr("auto (let WSL2 detect)"),
tr("ext4"), tr("ext3"), tr("ext2"),
tr("btrfs"), tr("xfs"), tr("f2fs"),
tr("jffs2"), tr("nilfs2"),
tr("hfsplus"), tr("ufs"),
tr("vfat"), tr("exfat"), tr("ntfs"),
});
fsRow->addWidget(m_wslFsTypeCombo, 1);
wslMountLayout->addLayout(fsRow);
auto* mountBtnRow = new QHBoxLayout();
m_wslMountBtn = new QPushButton(tr("Mount via WSL2"));
m_wslMountBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"border-radius: 4px; } QPushButton:hover { background-color: #e0b584; }");
connect(m_wslMountBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onWslMount);
mountBtnRow->addWidget(m_wslMountBtn);
m_wslUnmountBtn = new QPushButton(tr("Unmount All WSL Disks"));
connect(m_wslUnmountBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onWslUnmountAll);
mountBtnRow->addWidget(m_wslUnmountBtn);
m_wslRefreshBtn = new QPushButton(tr("Refresh Mounts"));
connect(m_wslRefreshBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onWslRefreshMounts);
mountBtnRow->addWidget(m_wslRefreshBtn);
wslMountLayout->addLayout(mountBtnRow);
wslLayout->addWidget(wslMountGroup);
// Mounted disks table
auto* wslMountedGroup = new QGroupBox(tr("Currently Mounted WSL2 Disks"));
auto* wslMountedLayout = new QVBoxLayout(wslMountedGroup);
m_wslMountsTable = new QTableWidget(0, 3);
m_wslMountsTable->setHorizontalHeaderLabels({tr("Device"), tr("Mount Point"), tr("Filesystem")});
m_wslMountsTable->horizontalHeader()->setStretchLastSection(true);
m_wslMountsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_wslMountsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_wslMountsTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
wslMountedLayout->addWidget(m_wslMountsTable);
auto* wslTableBtnRow = new QHBoxLayout();
m_wslUnmountBtn = new QPushButton(tr("Unmount Selected"));
m_wslUnmountBtn->setStyleSheet(
"QPushButton { background-color: #cc3333; color: white; border-radius: 4px; padding: 4px 12px; }"
"QPushButton:hover { background-color: #ee4444; }");
connect(m_wslUnmountBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onWslUnmount);
wslTableBtnRow->addWidget(m_wslUnmountBtn);
m_wslOpenBtn = new QPushButton(tr("Open in Explorer"));
connect(m_wslOpenBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onOpenMountPoint);
wslTableBtnRow->addWidget(m_wslOpenBtn);
wslTableBtnRow->addStretch();
wslMountedLayout->addLayout(wslTableBtnRow);
wslLayout->addWidget(wslMountedGroup);
m_wslStatusLabel = new QLabel();
m_wslStatusLabel->setWordWrap(true);
wslLayout->addWidget(m_wslStatusLabel);
innerTabs->addTab(wslWidget, tr("WSL2 Mount"));
// ---- Driver Tab ----
auto* drvWidget = new QWidget();
auto* drvLayout = new QVBoxLayout(drvWidget);
auto* drvInfo = new QLabel(
tr("Third-party kernel drivers give Windows native access to Linux/Mac filesystems "
"with a real drive letter — no WSL2 required.\n\n"
"Open-source drivers detected and installed automatically if present:"));
drvInfo->setWordWrap(true);
drvInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
drvLayout->addWidget(drvInfo);
m_drvDriverStatus = new QTextEdit();
m_drvDriverStatus->setReadOnly(true);
m_drvDriverStatus->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_drvDriverStatus->setFont(QFont("Courier New", 9));
drvLayout->addWidget(m_drvDriverStatus);
auto* drvMountGroup = new QGroupBox(tr("Mount with Driver"));
auto* drvMountLayout = new QVBoxLayout(drvMountGroup);
auto* drvDiskRow = new QHBoxLayout();
drvDiskRow->addWidget(new QLabel(tr("Disk:")));
m_drvDiskCombo = new QComboBox();
drvDiskRow->addWidget(m_drvDiskCombo, 1);
drvDiskRow->addWidget(new QLabel(tr("Part:")));
m_drvPartSpin = new QSpinBox();
m_drvPartSpin->setRange(1, 128);
m_drvPartSpin->setValue(1);
drvDiskRow->addWidget(m_drvPartSpin);
drvMountLayout->addLayout(drvDiskRow);
auto* drvSelectRow = new QHBoxLayout();
drvSelectRow->addWidget(new QLabel(tr("Driver:")));
m_drvDriverCombo = new QComboBox();
m_drvDriverCombo->addItems({
tr("Ext2Fsd (ext2/3/4 — open source, requires install)"),
tr("WinBtrfs (Btrfs — open source, requires install)"),
tr("WinHFSPlus (HFS+ — open source, read-only)"),
tr("ZFSin (ZFS — OpenZFS port for Windows)"),
});
drvSelectRow->addWidget(m_drvDriverCombo, 1);
drvMountLayout->addLayout(drvSelectRow);
auto* drvBtnRow = new QHBoxLayout();
m_drvMountBtn = new QPushButton(tr("Mount with Driver"));
m_drvMountBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"border-radius: 4px; } QPushButton:hover { background-color: #e0b584; }");
connect(m_drvMountBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onDriverMount);
drvBtnRow->addWidget(m_drvMountBtn);
m_drvUnmountBtn = new QPushButton(tr("Unmount"));
connect(m_drvUnmountBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onDriverUnmount);
drvBtnRow->addWidget(m_drvUnmountBtn);
auto* drvRefreshBtn = new QPushButton(tr("Refresh Driver Status"));
connect(drvRefreshBtn, &QPushButton::clicked, this, &NonWindowsFsTab::onRefreshDriverStatus);
drvBtnRow->addWidget(drvRefreshBtn);
drvMountLayout->addLayout(drvBtnRow);
drvLayout->addWidget(drvMountGroup);
m_drvStatusLabel = new QLabel();
m_drvStatusLabel->setWordWrap(true);
drvLayout->addWidget(m_drvStatusLabel);
innerTabs->addTab(drvWidget, tr("Kernel Drivers"));
// ---- Info Tab ----
auto* infoWidget = new QWidget();
auto* infoLayout = new QVBoxLayout(infoWidget);
m_infoText = new QTextEdit();
m_infoText->setReadOnly(true);
m_infoText->setHtml(tr(
"<h3>Linux &amp; Mac Filesystem Access on Windows</h3>"
"<p>Windows cannot natively read ext4, Btrfs, XFS, HFS+, F2FS, ZFS etc. "
"There are two ways to access them:</p>"
"<h4>Option 1: WSL2 Mount (recommended, no install needed)</h4>"
"<p>Windows 10 21H2+ and Windows 11 include <code>wsl --mount</code> which attaches "
"a physical disk to WSL2. The files are then accessible at "
"<code>\\\\wsl$\\Ubuntu\\mnt\\wsl\\PhysicalDrive1p1\\</code></p>"
"<pre>wsl --mount \\\\.\\PhysicalDrive1 --partition 1 --type ext4</pre>"
"<p>Supports: ext2, ext3, ext4, btrfs, xfs, f2fs, jffs2, nilfs2</p>"
"<h4>Option 2: Third-party kernel drivers</h4>"
"<ul>"
"<li><b>Ext2Fsd</b> — ext2/3/4 read/write driver. Free &amp; open source. "
"<a href='https://www.ext2fsd.com/'>ext2fsd.com</a></li>"
"<li><b>WinBtrfs</b> — Full Btrfs read/write driver. Open source. "
"<a href='https://github.com/maharmstone/btrfs'>github.com/maharmstone/btrfs</a></li>"
"<li><b>WinHFSPlus</b> — HFS+ read-only. Open source. "
"<a href='https://github.com/JetBrains/WinHFSPlus'>github.com/JetBrains/WinHFSPlus</a></li>"
"<li><b>ZFSin</b> — OpenZFS port for Windows. "
"<a href='https://github.com/openzfsonwindows/ZFSin'>github.com/openzfsonwindows/ZFSin</a></li>"
"</ul>"
"<h4>Future: Native Drivers</h4>"
"<p>Setec Partition Wizard includes a roadmap for built-in kernel-mode filesystem "
"drivers (IFS drivers) that will provide native Windows access to Linux/Mac filesystems "
"without requiring any third-party software. This requires the Windows Driver Kit (WDK) "
"and kernel signing — watch for updates.</p>"
));
infoLayout->addWidget(m_infoText);
innerTabs->addTab(infoWidget, tr("How It Works"));
mainLayout->addWidget(innerTabs);
}
void NonWindowsFsTab::setupWslTab() {}
void NonWindowsFsTab::setupDriverTab() {}
void NonWindowsFsTab::setupInfoTab() {}
void NonWindowsFsTab::checkWslAvailability()
{
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start("wsl.exe", {"--status"});
p.waitForFinished(5000);
m_wslAvailable = (p.exitCode() == 0);
if (m_wslAvailLabel)
{
if (m_wslAvailable)
{
m_wslAvailLabel->setText(tr("✓ WSL2 is available — Linux filesystem mounting enabled"));
m_wslAvailLabel->setStyleSheet("color: #a8e6a0; font-weight: bold; padding: 4px;");
}
else
{
m_wslAvailLabel->setText(
tr("✗ WSL2 not detected. Install WSL2: run 'wsl --install' in an admin PowerShell, "
"then restart. Windows 10 21H2+ required."));
m_wslAvailLabel->setStyleSheet("color: #ff9944; font-weight: bold; padding: 4px;");
}
}
}
void NonWindowsFsTab::checkDriverAvailability()
{
if (!m_drvDriverStatus) return;
QString status;
// Check for Ext2Fsd service
{
QProcess p;
p.start("sc.exe", {"query", "Ext2Fsd"});
p.waitForFinished(3000);
bool found = (p.exitCode() == 0);
status += found ? "✓ Ext2Fsd (ext2/3/4): INSTALLED\n" : "✗ Ext2Fsd (ext2/3/4): not installed\n";
}
// Check for WinBtrfs
{
QProcess p;
p.start("sc.exe", {"query", "btrfs"});
p.waitForFinished(3000);
bool found = (p.exitCode() == 0);
status += found ? "✓ WinBtrfs (Btrfs): INSTALLED\n" : "✗ WinBtrfs (Btrfs): not installed\n";
}
// Check for WinHFSPlus
{
QProcess p;
p.start("sc.exe", {"query", "WinHFSPlus"});
p.waitForFinished(3000);
bool found = (p.exitCode() == 0);
status += found ? "✓ WinHFSPlus (HFS+): INSTALLED\n" : "✗ WinHFSPlus (HFS+): not installed\n";
}
// Check for ZFSin
{
QProcess p;
p.start("sc.exe", {"query", "zfs"});
p.waitForFinished(3000);
bool found = (p.exitCode() == 0);
status += found ? "✓ ZFSin (ZFS): INSTALLED\n" : "✗ ZFSin (ZFS): not installed\n";
}
m_drvDriverStatus->setPlainText(status);
}
void NonWindowsFsTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void NonWindowsFsTab::populateDiskCombo()
{
if (m_wslDiskCombo) m_wslDiskCombo->clear();
if (m_drvDiskCombo) m_drvDiskCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
if (m_wslDiskCombo) m_wslDiskCombo->addItem(label, disk.id);
if (m_drvDiskCombo) m_drvDiskCombo->addItem(label, disk.id);
}
}
// ============================================================================
// WSL2 Slots
// ============================================================================
void NonWindowsFsTab::onWslMount()
{
if (!m_wslAvailable)
{
QMessageBox::warning(this, tr("WSL2 Not Available"),
tr("WSL2 is not installed or not running.\n\n"
"Install WSL2: open an admin PowerShell and run:\n"
" wsl --install\n\nThen restart Windows."));
return;
}
int diskId = m_wslDiskCombo->currentData().toInt();
int partition = m_wslPartSpin->value();
QString fsType = m_wslFsTypeCombo->currentText().split(' ').first();
if (fsType == "auto") fsType.clear();
QString devPath = QString("\\\\.\\PhysicalDrive%1").arg(diskId);
QStringList args = {"--mount", devPath};
if (partition > 0)
args << "--partition" << QString::number(partition);
if (!fsType.isEmpty())
args << "--type" << fsType;
m_wslStatusLabel->setText(tr("Mounting disk %1 via WSL2...").arg(diskId));
auto* thread = QThread::create([this, args]() {
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start("wsl.exe", args);
p.waitForFinished(30000);
QString out = QString::fromLocal8Bit(p.readAll());
int code = p.exitCode();
QMetaObject::invokeMethod(this, [this, out, code]() {
if (code == 0)
{
m_wslStatusLabel->setText(tr("✓ Mounted. Access via \\\\wsl$\\<distro>\\mnt\\wsl\\"));
m_wslStatusLabel->setStyleSheet("color: #a8e6a0;");
onWslRefreshMounts();
emit statusMessage(tr("WSL2 disk mount successful"));
}
else
{
m_wslStatusLabel->setText(tr("✗ Mount failed (exit %1):\n%2").arg(code).arg(out));
m_wslStatusLabel->setStyleSheet("color: #ff6b6b;");
}
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void NonWindowsFsTab::onWslUnmount()
{
int row = m_wslMountsTable->currentRow();
if (row < 0) return;
QString device = m_wslMountsTable->item(row, 0)->text();
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start("wsl.exe", {"--unmount", device});
p.waitForFinished(15000);
if (p.exitCode() == 0)
{
m_wslStatusLabel->setText(tr("✓ Unmounted: %1").arg(device));
onWslRefreshMounts();
emit statusMessage(tr("WSL2 disk unmounted"));
}
else
{
m_wslStatusLabel->setText(tr("✗ Unmount failed: %1")
.arg(QString::fromLocal8Bit(p.readAll())));
}
}
void NonWindowsFsTab::onWslUnmountAll()
{
QProcess p;
p.start("wsl.exe", {"--unmount"});
p.waitForFinished(15000);
m_wslStatusLabel->setText(p.exitCode() == 0
? tr("✓ All WSL2 disks unmounted.")
: tr("Unmount all: %1").arg(QString::fromLocal8Bit(p.readAll())));
onWslRefreshMounts();
emit statusMessage(tr("All WSL2 disks unmounted"));
}
void NonWindowsFsTab::onWslRefreshMounts()
{
// Parse `wsl --list --verbose` or check mounted disks via wsl
m_wslMountsTable->setRowCount(0);
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
// wsl --mount --bare lists currently attached physical disks
p.start("wsl.exe", {"--list", "--verbose"});
p.waitForFinished(5000);
// For now, just show a note — full parsing of wsl mount state requires
// reading /proc/mounts from inside WSL which we do via wsl -e cat /proc/mounts
QProcess p2;
p2.start("wsl.exe", {"-e", "cat", "/proc/mounts"});
p2.waitForFinished(5000);
QString mounts = QString::fromUtf8(p2.readAll());
int row = 0;
for (const auto& line : mounts.split('\n'))
{
// Only show entries that look like physical disks mounted via wsl --mount
if (!line.contains("/mnt/wsl/") && !line.startsWith("/dev/sd"))
continue;
auto parts = line.split(' ', Qt::SkipEmptyParts);
if (parts.size() < 3) continue;
m_wslMountsTable->insertRow(row);
m_wslMountsTable->setItem(row, 0, new QTableWidgetItem(parts[0])); // device
m_wslMountsTable->setItem(row, 1, new QTableWidgetItem(parts[1])); // mountpoint
m_wslMountsTable->setItem(row, 2, new QTableWidgetItem(parts[2])); // fstype
++row;
}
}
void NonWindowsFsTab::onOpenMountPoint()
{
int row = m_wslMountsTable->currentRow();
if (row < 0) return;
// Open \\wsl$\ in Explorer
QString mountPt = m_wslMountsTable->item(row, 1)->text();
// Convert /mnt/wsl/... to \\wsl$\<distro>\mnt\wsl\...
QProcess::startDetached("explorer.exe", {"\\\\wsl$"});
emit statusMessage(tr("Opened \\\\wsl$ in Explorer — navigate to your mount point"));
}
// ============================================================================
// Driver-based mount slots
// ============================================================================
void NonWindowsFsTab::onDriverMount()
{
int diskId = m_drvDiskCombo->currentData().toInt();
int part = m_drvPartSpin->value();
int driver = m_drvDriverCombo->currentIndex();
// Build a DevPath like \\.\PhysicalDrive1
QString devPath = QString("\\\\.\\PhysicalDrive%1").arg(diskId);
QString msg;
switch (driver)
{
case 0: // Ext2Fsd
msg = tr("Ext2Fsd assigns a drive letter automatically after mounting via its service.\n\n"
"If Ext2Fsd is installed, use 'Ext2 Volume Manager' from the Start menu for GUI control, "
"or the ext2mgr command line tool.\n\n"
"Device: %1 Partition: %2").arg(devPath).arg(part);
break;
case 1: // WinBtrfs
msg = tr("WinBtrfs mounts automatically when a Btrfs partition is detected.\n\n"
"Ensure the WinBtrfs driver is installed and the service is running.\n"
"Device: %1 Partition: %2").arg(devPath).arg(part);
break;
case 2: // WinHFSPlus
msg = tr("WinHFSPlus provides read-only HFS+ access.\n\n"
"Install the driver package, then the HFS+ partition should appear "
"automatically as a drive letter.\n"
"Device: %1 Partition: %2").arg(devPath).arg(part);
break;
case 3: // ZFSin
msg = tr("ZFSin (OpenZFS on Windows) mounts ZFS pools automatically.\n\n"
"Import the pool: zpool import -d %1\n"
"Then mount: zfs mount -a").arg(devPath);
break;
default:
msg = tr("Select a driver.");
}
m_drvStatusLabel->setText(msg);
emit statusMessage(tr("Driver mount instructions shown"));
}
void NonWindowsFsTab::onDriverUnmount()
{
m_drvStatusLabel->setText(
tr("Use the driver's own tools to unmount:\n"
"• Ext2Fsd: Ext2 Volume Manager → right-click → Disconnect\n"
"• WinBtrfs: Disk Management → Remove Drive Letter\n"
"• WinHFSPlus: Disk Management → Remove Drive Letter\n"
"• ZFSin: zfs unmount <dataset> then zpool export <pool>"));
}
void NonWindowsFsTab::onRefreshDriverStatus()
{
checkDriverAvailability();
emit statusMessage(tr("Driver status refreshed"));
}
} // namespace spw

View File

@@ -0,0 +1,93 @@
#pragma once
// NonWindowsFsTab — Mount and access filesystems that Windows cannot natively read:
// ext2/3/4, Btrfs, XFS, ZFS, F2FS, JFFS2, HFS+, UFS, ReiserFS, etc.
//
// Three mounting strategies, used in order of preference:
// 1. WSL2 wsl --mount (Windows 10 21H2+, no extra drivers needed)
// 2. Third-party kernel drivers (Ext2Fsd, WinBtrfs, ZFSin, WinHFSPlus)
// 3. Read-only access via libext2fs/raw parsing (planned)
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QComboBox;
class QGroupBox;
class QLabel;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QTabWidget;
class QTableWidget;
class QTextEdit;
namespace spw
{
class NonWindowsFsTab : public QWidget
{
Q_OBJECT
public:
explicit NonWindowsFsTab(QWidget* parent = nullptr);
~NonWindowsFsTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onWslMount();
void onWslUnmount();
void onWslUnmountAll();
void onWslRefreshMounts();
void onDriverMount();
void onDriverUnmount();
void onRefreshDriverStatus();
void onOpenMountPoint();
private:
void setupUi();
void setupWslTab();
void setupDriverTab();
void setupInfoTab();
void populateDiskCombo();
void checkWslAvailability();
void checkDriverAvailability();
static QString formatSize(uint64_t bytes);
// WSL2 mount section
QComboBox* m_wslDiskCombo = nullptr;
QSpinBox* m_wslPartSpin = nullptr;
QComboBox* m_wslFsTypeCombo = nullptr;
QPushButton* m_wslMountBtn = nullptr;
QPushButton* m_wslUnmountBtn = nullptr;
QPushButton* m_wslUnmountAllBtn = nullptr;
QPushButton* m_wslRefreshBtn = nullptr;
QTableWidget* m_wslMountsTable = nullptr;
QPushButton* m_wslOpenBtn = nullptr;
QLabel* m_wslAvailLabel = nullptr;
QLabel* m_wslStatusLabel = nullptr;
// Driver-based mount section
QComboBox* m_drvDiskCombo = nullptr;
QSpinBox* m_drvPartSpin = nullptr;
QComboBox* m_drvDriverCombo = nullptr;
QPushButton* m_drvMountBtn = nullptr;
QPushButton* m_drvUnmountBtn = nullptr;
QTableWidget* m_drvMountsTable = nullptr;
QLabel* m_drvStatusLabel = nullptr;
QTextEdit* m_drvDriverStatus = nullptr;
// Info tab
QTextEdit* m_infoText = nullptr;
SystemDiskSnapshot m_snapshot;
bool m_wslAvailable = false;
};
} // namespace spw

867
src/ui/tabs/SdCardTab.cpp Normal file
View File

@@ -0,0 +1,867 @@
#include "SdCardTab.h"
#include "core/maintenance/SdCardRecovery.h"
#include "core/maintenance/SdCardAnalyzer.h"
#include "core/disk/DiskEnumerator.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSplitter>
#include <QTabWidget>
#include <QTableWidget>
#include <QTextEdit>
#include <QThread>
#include <QVBoxLayout>
#include <QHeaderView>
#include <QFrame>
namespace spw
{
// ============================================================================
// Helpers
// ============================================================================
QString SdCardTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 1);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 0);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
QString SdCardTab::formatSpeed(double mbps)
{
if (mbps <= 0) return tr("N/A");
return QString("%1 MB/s").arg(mbps, 0, 'f', 1);
}
QString SdCardTab::verdictString(CounterfeitVerdict v)
{
switch (v)
{
case CounterfeitVerdict::Genuine: return tr("✓ GENUINE");
case CounterfeitVerdict::LikelySpoofed: return tr("✗ COUNTERFEIT DETECTED");
case CounterfeitVerdict::Suspicious: return tr("⚠ SUSPICIOUS");
case CounterfeitVerdict::TestFailed: return tr("— TEST FAILED");
default: return tr("? UNTESTED");
}
}
QString SdCardTab::verdictStyle(CounterfeitVerdict v)
{
switch (v)
{
case CounterfeitVerdict::Genuine: return "color: #a8e6a0; font-size: 18px; font-weight: bold;";
case CounterfeitVerdict::LikelySpoofed: return "color: #ff6b6b; font-size: 18px; font-weight: bold;";
case CounterfeitVerdict::Suspicious: return "color: #ffd93d; font-size: 18px; font-weight: bold;";
default: return "color: #aaaaaa; font-size: 16px;";
}
}
// ============================================================================
// Constructor
// ============================================================================
SdCardTab::SdCardTab(QWidget* parent) : QWidget(parent)
{
setupUi();
}
SdCardTab::~SdCardTab() = default;
void SdCardTab::setupUi()
{
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(8);
// ---- Top: card selector bar ----
setupCardSelectorPanel();
auto* selectorGroup = new QGroupBox(tr("SD / microSD Card Selection"));
auto* selectorLayout = new QVBoxLayout(selectorGroup);
auto* selectorRow = new QHBoxLayout();
selectorRow->addWidget(m_scanBtn);
selectorRow->addWidget(m_cardCombo, 1);
selectorLayout->addLayout(selectorRow);
selectorLayout->addWidget(m_cardSummaryLabel);
mainLayout->addWidget(selectorGroup);
// ---- Middle: inner tab widget ----
m_innerTabs = new QTabWidget();
// Tab 1: Card Info
auto* infoWidget = new QWidget();
setupInfoPanel();
auto* infoLayout = new QVBoxLayout(infoWidget);
{
auto* form = new QGroupBox(tr("Device Information"));
auto* fl = new QFormLayout(form);
fl->setLabelAlignment(Qt::AlignRight);
fl->addRow(tr("Model:"), m_infoModel);
fl->addRow(tr("Vendor:"), m_infoVendor);
fl->addRow(tr("Manufacturer:"), m_infoManufacturer);
fl->addRow(tr("Serial:"), m_infoSerial);
fl->addRow(tr("Capacity:"), m_infoCapacity);
fl->addRow(tr("Bus Type:"), m_infoBusType);
fl->addRow(tr("Interface:"), m_infoInterface);
fl->addRow(tr("Write Protect:"),m_infoWriteProt);
fl->addRow(tr("Status:"), m_infoStatus);
infoLayout->addWidget(form);
infoLayout->addStretch();
auto* refreshBtn = new QPushButton(tr("Refresh Info"));
connect(refreshBtn, &QPushButton::clicked, this, &SdCardTab::onRefreshInfo);
infoLayout->addWidget(refreshBtn);
}
m_innerTabs->addTab(infoWidget, tr("Card Info"));
// Tab 2: Counterfeit Detection
auto* cntWidget = new QWidget();
setupCounterfeitPanel();
auto* cntLayout = new QVBoxLayout(cntWidget);
{
auto* explainLabel = new QLabel(
tr("Counterfeit SD cards report a large capacity (e.g. 64 GB) but contain much less "
"real NAND flash (e.g. 24 GB). Data written beyond the real capacity silently "
"wraps and overwrites earlier data.\n\n"
"This test writes unique signatures at geometrically distributed positions across "
"the disk and reads them back. It restores original data after each probe."));
explainLabel->setWordWrap(true);
explainLabel->setStyleSheet("color: #aaaaaa; font-style: italic;");
cntLayout->addWidget(explainLabel);
auto* warnLabel = new QLabel(
tr("⚠ This test writes to the card. Keep the card inserted until complete."));
warnLabel->setWordWrap(true);
warnLabel->setStyleSheet("color: #ffd93d; font-weight: bold;");
cntLayout->addWidget(warnLabel);
cntLayout->addWidget(m_counterVerdict);
cntLayout->addWidget(m_counterProgress);
cntLayout->addWidget(m_counterLog, 1);
cntLayout->addWidget(m_counterBtn);
}
m_innerTabs->addTab(cntWidget, tr("Counterfeit Check"));
// Tab 3: Speed Test
auto* speedWidget = new QWidget();
setupSpeedPanel();
auto* speedLayout = new QVBoxLayout(speedWidget);
{
auto* explainLabel = new QLabel(
tr("Benchmarks sequential read/write speeds and random 4K IOPS. "
"Compare against the card's rated speed class:\n"
" Class 10 / UHS-I: ≥10 MB/s seq write\n"
" UHS-I U3 / V30: ≥30 MB/s seq write\n"
" V60: ≥60 MB/s • V90: ≥90 MB/s"));
explainLabel->setWordWrap(true);
explainLabel->setStyleSheet("color: #aaaaaa; font-style: italic;");
speedLayout->addWidget(explainLabel);
auto* resultsGroup = new QGroupBox(tr("Results"));
auto* rfl = new QFormLayout(resultsGroup);
rfl->setLabelAlignment(Qt::AlignRight);
rfl->addRow(tr("Sequential Read:"), m_speedSeqRead);
rfl->addRow(tr("Sequential Write:"), m_speedSeqWrite);
rfl->addRow(tr("Random 4K Read:"), m_speedRandRead);
rfl->addRow(tr("Random 4K Write:"), m_speedRandWrite);
rfl->addRow(tr("Notes:"), m_speedNotes);
speedLayout->addWidget(resultsGroup);
speedLayout->addWidget(m_speedProgress);
speedLayout->addWidget(m_speedBtn);
speedLayout->addStretch();
}
m_innerTabs->addTab(speedWidget, tr("Speed Test"));
// Tab 4: Surface Scan / Health
auto* healthWidget = new QWidget();
setupHealthPanel();
auto* healthLayout = new QVBoxLayout(healthWidget);
{
auto* explainLabel = new QLabel(
tr("Reads every sector on the card to find bad or slow sectors. "
"Even one bad sector can cause data corruption. "
"A slow sector (>500ms read) often precedes failure."));
explainLabel->setWordWrap(true);
explainLabel->setStyleSheet("color: #aaaaaa; font-style: italic;");
healthLayout->addWidget(explainLabel);
auto* statsGroup = new QGroupBox(tr("Scan Results"));
auto* sfl = new QFormLayout(statsGroup);
sfl->setLabelAlignment(Qt::AlignRight);
sfl->addRow(tr("Sectors Scanned:"), m_healthScanned);
sfl->addRow(tr("Bad Sectors:"), m_healthBad);
sfl->addRow(tr("Slow Sectors:"), m_healthSlow);
sfl->addRow(tr("Overall:"), m_healthResult);
healthLayout->addWidget(statsGroup);
healthLayout->addWidget(m_healthProgress);
auto* btnRow = new QHBoxLayout();
btnRow->addWidget(m_scanSurfaceBtn);
btnRow->addWidget(m_cancelScanBtn);
healthLayout->addLayout(btnRow);
healthLayout->addStretch();
}
m_innerTabs->addTab(healthWidget, tr("Surface Scan"));
// Tab 5: Repair / Format
auto* repairWidget = new QWidget();
setupRepairPanel();
auto* repairLayout = new QVBoxLayout(repairWidget);
{
auto* explainLabel = new QLabel(
tr("Repair a card that Windows cannot see. This cleans the partition table, "
"creates a new partition, and formats it. All existing data will be erased."));
explainLabel->setWordWrap(true);
explainLabel->setStyleSheet("color: #aaaaaa; font-style: italic;");
repairLayout->addWidget(explainLabel);
auto* optGroup = new QGroupBox(tr("Format Options"));
auto* ofl = new QFormLayout(optGroup);
ofl->setLabelAlignment(Qt::AlignRight);
ofl->addRow(tr("Filesystem:"), m_repairFsCombo);
ofl->addRow(tr("Volume Label:"), m_repairLabel);
ofl->addRow(tr("Clean Table:"), m_repairCleanChk);
repairLayout->addWidget(optGroup);
repairLayout->addWidget(m_repairProgress);
repairLayout->addWidget(m_repairStatus);
auto* btnRow = new QHBoxLayout();
btnRow->addWidget(m_repairBtn);
btnRow->addWidget(m_eraseBtn);
repairLayout->addLayout(btnRow);
repairLayout->addStretch();
}
m_innerTabs->addTab(repairWidget, tr("Repair / Format"));
mainLayout->addWidget(m_innerTabs, 1);
}
void SdCardTab::setupCardSelectorPanel()
{
m_scanBtn = new QPushButton(tr("Scan for Cards"));
m_scanBtn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
connect(m_scanBtn, &QPushButton::clicked, this, &SdCardTab::onScanCards);
m_cardCombo = new QComboBox();
m_cardCombo->setPlaceholderText(tr("Click 'Scan for Cards' to detect SD/MMC media..."));
connect(m_cardCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SdCardTab::onCardSelected);
m_cardSummaryLabel = new QLabel(tr("No card selected."));
m_cardSummaryLabel->setStyleSheet("color: #aaaaaa; font-style: italic;");
}
void SdCardTab::setupInfoPanel()
{
auto makeInfo = [](const QString& def = tr("")) -> QLabel* {
auto* l = new QLabel(def);
l->setTextInteractionFlags(Qt::TextSelectableByMouse);
return l;
};
m_infoModel = makeInfo();
m_infoVendor = makeInfo();
m_infoManufacturer= makeInfo();
m_infoSerial = makeInfo();
m_infoCapacity = makeInfo();
m_infoBusType = makeInfo();
m_infoInterface = makeInfo();
m_infoWriteProt = makeInfo();
m_infoStatus = makeInfo();
}
void SdCardTab::setupCounterfeitPanel()
{
m_counterVerdict = new QLabel(tr("Not tested yet"));
m_counterVerdict->setAlignment(Qt::AlignCenter);
m_counterVerdict->setStyleSheet("color: #aaaaaa; font-size: 18px; padding: 12px;");
m_counterVerdict->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
m_counterProgress = new QProgressBar();
m_counterProgress->setVisible(false);
m_counterProgress->setRange(0, 100);
m_counterLog = new QTextEdit();
m_counterLog->setReadOnly(true);
m_counterLog->setPlaceholderText(tr("Test output will appear here..."));
m_counterLog->setFont(QFont("Courier New", 9));
m_counterBtn = new QPushButton(tr("Run Counterfeit Check"));
m_counterBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 14px; "
"font-weight: bold; border-radius: 5px; }"
"QPushButton:hover { background-color: #e0b584; }"
"QPushButton:disabled { background-color: #555; color: #888; }");
connect(m_counterBtn, &QPushButton::clicked, this, &SdCardTab::onCheckCounterfeit);
}
void SdCardTab::setupSpeedPanel()
{
auto makeResult = [](const QString& def = tr("")) -> QLabel* {
auto* l = new QLabel(def);
l->setStyleSheet("font-size: 14px; font-weight: bold;");
return l;
};
m_speedSeqRead = makeResult();
m_speedSeqWrite = makeResult();
m_speedRandRead = makeResult();
m_speedRandWrite = makeResult();
m_speedNotes = new QLabel(tr(""));
m_speedNotes->setWordWrap(true);
m_speedNotes->setStyleSheet("color: #aaaaaa;");
m_speedProgress = new QProgressBar();
m_speedProgress->setVisible(false);
m_speedBtn = new QPushButton(tr("Run Speed Test"));
connect(m_speedBtn, &QPushButton::clicked, this, &SdCardTab::onRunSpeedTest);
}
void SdCardTab::setupHealthPanel()
{
m_healthScanned = new QLabel(tr(""));
m_healthBad = new QLabel(tr(""));
m_healthSlow = new QLabel(tr(""));
m_healthResult = new QLabel(tr(""));
m_healthResult->setStyleSheet("font-weight: bold;");
m_healthProgress = new QProgressBar();
m_healthProgress->setVisible(false);
m_scanSurfaceBtn = new QPushButton(tr("Start Surface Scan"));
connect(m_scanSurfaceBtn, &QPushButton::clicked, this, &SdCardTab::onSurfaceScan);
m_cancelScanBtn = new QPushButton(tr("Cancel"));
m_cancelScanBtn->setEnabled(false);
connect(m_cancelScanBtn, &QPushButton::clicked, this, &SdCardTab::onCancelOperation);
}
void SdCardTab::setupRepairPanel()
{
m_repairFsCombo = new QComboBox();
m_repairFsCombo->addItems({
tr("FAT32 (recommended for ≤ 32 GB)"),
tr("exFAT (recommended for > 32 GB)"),
tr("NTFS (Windows only)")
});
m_repairLabel = new QLineEdit(QStringLiteral("SD_CARD"));
m_repairLabel->setMaxLength(11);
m_repairLabel->setPlaceholderText(tr("Max 11 characters"));
m_repairCleanChk = new QCheckBox(
tr("Clean partition table (required for unreadable cards)"));
m_repairCleanChk->setChecked(true);
m_repairProgress = new QProgressBar();
m_repairProgress->setVisible(false);
m_repairStatus = new QLabel();
m_repairStatus->setWordWrap(true);
m_repairBtn = new QPushButton(tr("Repair && Format"));
m_repairBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 13px; "
"font-weight: bold; border-radius: 5px; }"
"QPushButton:hover { background-color: #e0b584; }");
connect(m_repairBtn, &QPushButton::clicked, this, &SdCardTab::onRepairCard);
m_eraseBtn = new QPushButton(tr("Secure Erase"));
m_eraseBtn->setStyleSheet(
"QPushButton { background-color: #cc3333; color: white; font-size: 13px; "
"font-weight: bold; border-radius: 5px; }"
"QPushButton:hover { background-color: #ee4444; }");
connect(m_eraseBtn, &QPushButton::clicked, this, &SdCardTab::onSecureErase);
}
// ============================================================================
// refreshDisks — called when main disk list updates
// ============================================================================
void SdCardTab::refreshDisks(const SystemDiskSnapshot& /*snapshot*/)
{
// Don't auto-scan on disk refresh — only when user explicitly clicks Scan
}
// ============================================================================
// Scan for SD cards
// ============================================================================
void SdCardTab::onScanCards()
{
if (m_operationRunning) return;
m_scanBtn->setEnabled(false);
m_cardCombo->clear();
m_cards.clear();
m_cardSummaryLabel->setText(tr("Scanning..."));
auto* thread = QThread::create([this]() {
auto result = SdCardRecovery::detectSdCards();
if (result.isOk())
m_cards = std::move(result.value());
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_scanBtn->setEnabled(true);
if (m_cards.empty())
{
m_cardSummaryLabel->setText(
tr("No SD/MMC cards found. Make sure the card is inserted and the reader is connected."));
emit statusMessage(tr("SD card scan: no cards found"));
return;
}
for (const auto& card : m_cards)
{
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;
}
m_cardCombo->addItem(
QString("Disk %1 — %2 [%3] %4")
.arg(card.diskId)
.arg(QString::fromStdWString(card.model))
.arg(formatSize(card.sizeBytes))
.arg(statusStr),
card.diskId);
}
m_cardSummaryLabel->setText(tr("Found %1 card(s).").arg(m_cards.size()));
emit statusMessage(tr("SD card scan complete — %1 card(s)").arg(m_cards.size()));
if (!m_cards.empty())
onCardSelected(0);
});
thread->start();
}
// ============================================================================
// Card selected — update summary and fetch identity
// ============================================================================
void SdCardTab::onCardSelected(int index)
{
if (index < 0 || index >= static_cast<int>(m_cards.size()))
return;
const auto& card = m_cards[static_cast<size_t>(index)];
// Basic status label
QString statusStr;
switch (card.status)
{
case SdCardStatus::Healthy: statusStr = tr("✓ Healthy"); break;
case SdCardStatus::NoPartitionTable: statusStr = tr("✗ No Partition Table — needs repair"); break;
case SdCardStatus::CorruptPartition: statusStr = tr("⚠ Corrupt — needs repair"); break;
case SdCardStatus::RawFilesystem: statusStr = tr("⚠ RAW filesystem — needs formatting"); break;
case SdCardStatus::NoMedia: statusStr = tr("— No media in reader"); break;
default: statusStr = tr("? Unknown"); break;
}
m_cardSummaryLabel->setText(QString(" %1 | %2 | %3")
.arg(QString::fromStdWString(card.model))
.arg(formatSize(card.sizeBytes))
.arg(statusStr));
// Update basic info immediately
m_infoModel->setText(QString::fromStdWString(card.model));
m_infoCapacity->setText(formatSize(card.sizeBytes));
m_infoInterface->setText(card.interfaceType == DiskInterfaceType::MMC ? tr("MMC/SD native") :
card.interfaceType == DiskInterfaceType::USB ? tr("USB reader") : tr("Other"));
m_infoStatus->setText(statusStr);
m_infoWriteProt->setText(SdCardAnalyzer::isWriteProtected(card.diskId) ? tr("Write Protected") : tr("Writable"));
// Fetch full identity in background
int diskId = card.diskId;
auto* thread = QThread::create([this, diskId]() {
auto idResult = SdCardAnalyzer::queryIdentity(diskId);
if (idResult.isOk())
{
const auto& id = idResult.value();
QMetaObject::invokeMethod(this, [this, id]() {
m_infoVendor->setText(id.vendorId.empty() ? tr("") : QString::fromStdWString(id.vendorId));
m_infoSerial->setText(id.serialNumberStr.empty() ? tr("") : QString::fromStdWString(id.serialNumberStr));
m_infoBusType->setText(id.busType.empty() ? tr("") : QString::fromStdWString(id.busType));
if (id.cidValid)
{
QString mfr = QString::fromLatin1(SdCardAnalyzer::manufacturerName(id.manufacturerId));
m_infoManufacturer->setText(QString("%1 (MID 0x%2)")
.arg(mfr).arg(id.manufacturerId, 2, 16, QChar('0')));
}
else
{
m_infoManufacturer->setText(id.productId.empty() ? tr("") : QString::fromStdWString(id.productId));
}
}, Qt::QueuedConnection);
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
// ============================================================================
// Refresh info
// ============================================================================
void SdCardTab::onRefreshInfo()
{
int idx = m_cardCombo->currentIndex();
if (idx >= 0 && idx < static_cast<int>(m_cards.size()))
onCardSelected(idx);
}
// ============================================================================
// Counterfeit check
// ============================================================================
void SdCardTab::onCheckCounterfeit()
{
int idx = m_cardCombo->currentIndex();
if (idx < 0 || idx >= static_cast<int>(m_cards.size())) return;
auto reply = QMessageBox::warning(this, tr("Counterfeit Check"),
tr("This test writes small probe signatures to the card to verify actual capacity.\n"
"It will restore the original data after each probe, but keep the card inserted.\n\n"
"Continue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
int diskId = m_cards[static_cast<size_t>(idx)].diskId;
setOperationRunning(true);
m_counterProgress->setVisible(true);
m_counterProgress->setValue(0);
m_counterLog->clear();
m_counterVerdict->setText(tr("Testing..."));
m_counterVerdict->setStyleSheet("color: #aaaaaa; font-size: 16px;");
auto* thread = QThread::create([this, diskId]() {
auto result = SdCardAnalyzer::checkCounterfeit(diskId,
[this](const std::string& stage, int pct) {
QMetaObject::invokeMethod(m_counterProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_counterLog, "append",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(stage)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_counterProgress->setVisible(false);
if (result.isError())
{
m_counterVerdict->setText(tr("Error: %1").arg(
QString::fromStdString(result.error().message)));
m_counterVerdict->setStyleSheet("color: #ff6b6b; font-size: 14px;");
return;
}
const auto& r = result.value();
m_counterVerdict->setText(verdictString(r.verdict));
m_counterVerdict->setStyleSheet(verdictStyle(r.verdict));
m_counterLog->append(QString("\n=== RESULT ==="));
m_counterLog->append(QString::fromStdString(r.summaryMessage));
m_counterLog->append(QString("Reported capacity: %1").arg(formatSize(r.reportedCapacityBytes)));
if (r.verifiedCapacityBytes != r.reportedCapacityBytes)
m_counterLog->append(QString("Verified capacity: ~%1").arg(formatSize(r.verifiedCapacityBytes)));
m_counterLog->append(QString("Probes: %1 total, %2 failed (%.0f%%)")
.arg(r.probeCount).arg(r.failCount).arg(r.failPercent));
if (!r.manufacturerName.empty())
m_counterLog->append(QString("Manufacturer: %1%2")
.arg(QString::fromStdString(r.manufacturerName))
.arg(r.unknownManufacturer ? tr(" [UNVERIFIED]") : QString()));
if (r.suspiciousVendorString)
m_counterLog->append(tr("⚠ Generic/suspicious vendor string"));
emit statusMessage(tr("Counterfeit check complete"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
setOperationRunning(false);
});
thread->start();
}
// ============================================================================
// Speed test
// ============================================================================
void SdCardTab::onRunSpeedTest()
{
int idx = m_cardCombo->currentIndex();
if (idx < 0 || idx >= static_cast<int>(m_cards.size())) return;
int diskId = m_cards[static_cast<size_t>(idx)].diskId;
setOperationRunning(true);
m_speedProgress->setVisible(true);
m_speedProgress->setValue(0);
m_speedSeqRead->setText(tr("Testing..."));
m_speedSeqWrite->setText(tr("Testing..."));
m_speedRandRead->setText(tr("Testing..."));
m_speedRandWrite->setText(tr("Testing..."));
m_speedNotes->setText(tr(""));
auto* thread = QThread::create([this, diskId]() {
auto result = SdCardAnalyzer::benchmarkSpeed(diskId, 64 * 1024 * 1024,
[this](const std::string& stage, int pct) {
QMetaObject::invokeMethod(m_speedProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_speedProgress->setVisible(false);
if (result.isError()) return;
const auto& r = result.value();
auto styleForSpeed = [](double mbps, double threshold) -> QString {
if (mbps <= 0) return "color: #888;";
return mbps >= threshold ? "color: #a8e6a0; font-size: 14px; font-weight: bold;"
: "color: #ff9944; font-size: 14px; font-weight: bold;";
};
m_speedSeqRead->setText(formatSpeed(r.seqReadMBps));
m_speedSeqRead->setStyleSheet(styleForSpeed(r.seqReadMBps, 10.0));
m_speedSeqWrite->setText(formatSpeed(r.seqWriteMBps));
m_speedSeqWrite->setStyleSheet(styleForSpeed(r.seqWriteMBps, 10.0));
m_speedRandRead->setText(QString("%1 IOPS").arg(r.randRead4kIOPS, 0, 'f', 0));
m_speedRandWrite->setText(r.writeProtected ? tr("(write protected)")
: QString("%1 IOPS").arg(r.randWrite4kIOPS, 0, 'f', 0));
m_speedNotes->setText(r.notes.empty() ? tr("") : QString::fromStdString(r.notes));
emit statusMessage(tr("Speed test complete — %.1f MB/s read, %.1f MB/s write")
.arg(r.seqReadMBps).arg(r.seqWriteMBps));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() { setOperationRunning(false); });
thread->start();
}
// ============================================================================
// Surface scan
// ============================================================================
void SdCardTab::onSurfaceScan()
{
int idx = m_cardCombo->currentIndex();
if (idx < 0 || idx >= static_cast<int>(m_cards.size())) return;
int diskId = m_cards[static_cast<size_t>(idx)].diskId;
setOperationRunning(true);
m_cancelFlag.store(false);
m_healthProgress->setVisible(true);
m_healthProgress->setValue(0);
m_healthScanned->setText(tr("0"));
m_healthBad->setText(tr("0"));
m_healthSlow->setText(tr("0"));
m_healthResult->setText(tr("Scanning..."));
m_cancelScanBtn->setEnabled(true);
auto* thread = QThread::create([this, diskId]() {
auto result = SdCardAnalyzer::surfaceScan(diskId, &m_cancelFlag,
[this](uint64_t cur, uint64_t total, uint64_t bad, int pct) {
QMetaObject::invokeMethod(m_healthProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_healthScanned, "setText", Qt::QueuedConnection,
Q_ARG(QString, QString::number(cur)));
QMetaObject::invokeMethod(m_healthBad, "setText", Qt::QueuedConnection,
Q_ARG(QString, bad > 0
? QString("<span style='color:#ff6b6b'>%1</span>").arg(bad)
: QString("0")));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_healthProgress->setVisible(false);
m_cancelScanBtn->setEnabled(false);
if (result.isError()) return;
const auto& r = result.value();
m_healthScanned->setText(QString::number(r.sectorsScanned));
m_healthBad->setText(r.badSectors > 0
? QString("<span style='color:#ff6b6b'>%1</span>").arg(r.badSectors)
: tr("0 ✓"));
m_healthSlow->setText(r.slowSectors > 0
? QString("<span style='color:#ffd93d'>%1</span>").arg(r.slowSectors)
: tr("0"));
if (r.badSectors == 0 && r.slowSectors == 0)
m_healthResult->setStyleSheet("color: #a8e6a0; font-weight: bold;");
else
m_healthResult->setStyleSheet("color: #ff9944; font-weight: bold;");
m_healthResult->setText(QString::fromStdString(r.summary));
emit statusMessage(tr("Surface scan complete"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() { setOperationRunning(false); });
thread->start();
}
// ============================================================================
// Repair / Format
// ============================================================================
void SdCardTab::onRepairCard()
{
int idx = m_cardCombo->currentIndex();
if (idx < 0 || idx >= static_cast<int>(m_cards.size())) return;
const auto& card = m_cards[static_cast<size_t>(idx)];
auto reply = QMessageBox::warning(this, tr("Repair && Format"),
tr("This will ERASE ALL DATA on:\n\nDisk %1: %2 (%3)\n\n"
"The card will be repartitioned and formatted. Continue?")
.arg(card.diskId)
.arg(QString::fromStdWString(card.model))
.arg(formatSize(card.sizeBytes)),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
SdFixConfig config;
config.action = m_repairCleanChk->isChecked()
? SdFixAction::CleanAndFormat : SdFixAction::FormatOnly;
switch (m_repairFsCombo->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_repairLabel->text().toStdWString();
int diskId = card.diskId;
setOperationRunning(true);
m_repairProgress->setVisible(true);
m_repairProgress->setValue(0);
m_repairStatus->setText(tr("Working..."));
auto* thread = QThread::create([this, diskId, config]() {
auto result = SdCardRecovery::fixCard(diskId, config,
[this](const std::string& stage, int pct) {
QMetaObject::invokeMethod(m_repairProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_repairStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(stage)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_repairProgress->setVisible(false);
if (result.isError())
{
m_repairStatus->setText(tr("Failed: %1").arg(
QString::fromStdString(result.error().message)));
m_repairStatus->setStyleSheet("color: #ff6b6b;");
}
else
{
m_repairStatus->setText(tr("✓ Repair complete. Card is ready to use."));
m_repairStatus->setStyleSheet("color: #a8e6a0; font-weight: bold;");
emit statusMessage(tr("SD card repair complete"));
onScanCards(); // Rescan to update status
}
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() { setOperationRunning(false); });
thread->start();
}
// ============================================================================
// Secure erase
// ============================================================================
void SdCardTab::onSecureErase()
{
int idx = m_cardCombo->currentIndex();
if (idx < 0 || idx >= static_cast<int>(m_cards.size())) return;
const auto& card = m_cards[static_cast<size_t>(idx)];
auto reply = QMessageBox::critical(this, tr("Secure Erase"),
tr("PERMANENTLY DESTROY ALL DATA on:\n\nDisk %1: %2 (%3)\n\n"
"This action is IRREVERSIBLE. Continue?")
.arg(card.diskId)
.arg(QString::fromStdWString(card.model))
.arg(formatSize(card.sizeBytes)),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
SdFixConfig config;
config.action = SdFixAction::CleanAndFormat;
config.targetFs = FilesystemType::FAT32;
config.volumeLabel = L"ERASED";
int diskId = card.diskId;
setOperationRunning(true);
m_repairProgress->setVisible(true);
m_repairStatus->setText(tr("Securely erasing..."));
auto* thread = QThread::create([this, diskId, config]() {
auto result = SdCardRecovery::fixCard(diskId, config,
[this](const std::string& stage, int pct) {
QMetaObject::invokeMethod(m_repairProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_repairStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(stage)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_repairProgress->setVisible(false);
m_repairStatus->setText(result.isOk()
? tr("✓ Secure erase complete.")
: tr("Failed: %1").arg(QString::fromStdString(result.error().message)));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() { setOperationRunning(false); });
thread->start();
}
// ============================================================================
// Cancel
// ============================================================================
void SdCardTab::onCancelOperation()
{
m_cancelFlag.store(true);
m_cancelScanBtn->setEnabled(false);
}
// ============================================================================
// Helpers
// ============================================================================
void SdCardTab::setOperationRunning(bool running)
{
m_operationRunning = running;
m_scanBtn->setEnabled(!running);
m_counterBtn->setEnabled(!running);
m_speedBtn->setEnabled(!running);
m_scanSurfaceBtn->setEnabled(!running);
m_repairBtn->setEnabled(!running);
m_eraseBtn->setEnabled(!running);
}
} // namespace spw

128
src/ui/tabs/SdCardTab.h Normal file
View File

@@ -0,0 +1,128 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/maintenance/SdCardRecovery.h"
#include "core/maintenance/SdCardAnalyzer.h"
#include <QWidget>
#include <atomic>
#include <vector>
class QComboBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
class QTabWidget;
class QTableWidget;
class QTextEdit;
class QCheckBox;
class QSpinBox;
namespace spw
{
class SdCardTab : public QWidget
{
Q_OBJECT
public:
explicit SdCardTab(QWidget* parent = nullptr);
~SdCardTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onScanCards();
void onCardSelected(int index);
void onRefreshInfo();
void onCheckCounterfeit();
void onRunSpeedTest();
void onSurfaceScan();
void onRepairCard();
void onFormatCard() { onRepairCard(); }
void onSecureErase();
void onCancelOperation();
private:
void setupUi();
void setupCardSelectorPanel();
void setupInfoPanel();
void setupCounterfeitPanel();
void setupSpeedPanel();
void setupHealthPanel();
void setupRepairPanel();
void updateCardInfo(const SdCardInfo& card, const SdCardIdentity& identity);
void setOperationRunning(bool running);
static QString formatSize(uint64_t bytes);
static QString formatSpeed(double mbps);
static QString verdictString(CounterfeitVerdict v);
static QString verdictStyle(CounterfeitVerdict v);
// Card selector
QComboBox* m_cardCombo = nullptr;
QPushButton* m_scanBtn = nullptr;
QLabel* m_cardSummaryLabel = nullptr;
// Info panel
QLabel* m_infoModel = nullptr;
QLabel* m_infoSerial = nullptr;
QLabel* m_infoVendor = nullptr;
QLabel* m_infoCapacity = nullptr;
QLabel* m_infoBusType = nullptr;
QLabel* m_infoInterface = nullptr;
QLabel* m_infoWriteProt = nullptr;
QLabel* m_infoStatus = nullptr;
QLabel* m_infoManufacturer = nullptr;
// Counterfeit
QPushButton* m_counterBtn = nullptr;
QLabel* m_counterVerdict = nullptr;
QTextEdit* m_counterLog = nullptr;
QProgressBar* m_counterProgress = nullptr;
// Speed test
QPushButton* m_speedBtn = nullptr;
QProgressBar* m_speedProgress = nullptr;
QLabel* m_speedSeqRead = nullptr;
QLabel* m_speedSeqWrite = nullptr;
QLabel* m_speedRandRead = nullptr;
QLabel* m_speedRandWrite = nullptr;
QLabel* m_speedNotes = nullptr;
// Health / surface scan
QPushButton* m_scanSurfaceBtn = nullptr;
QPushButton* m_cancelScanBtn = nullptr;
QProgressBar* m_healthProgress = nullptr;
QLabel* m_healthBad = nullptr;
QLabel* m_healthSlow = nullptr;
QLabel* m_healthScanned = nullptr;
QLabel* m_healthResult = nullptr;
// Repair / format
QComboBox* m_repairFsCombo = nullptr;
QLineEdit* m_repairLabel = nullptr;
QCheckBox* m_repairCleanChk = nullptr;
QPushButton* m_repairBtn = nullptr;
QPushButton* m_eraseBtn = nullptr;
QProgressBar* m_repairProgress = nullptr;
QLabel* m_repairStatus = nullptr;
// Inner tab widget
QTabWidget* m_innerTabs = nullptr;
// State
std::vector<SdCardInfo> m_cards;
std::atomic<bool> m_cancelFlag{false};
bool m_operationRunning = false;
};
} // namespace spw

View File

@@ -0,0 +1,727 @@
#include "VirtualDiskTab.h"
#include "core/imaging/VirtualDisk.h"
#include "core/disk/DiskEnumerator.h"
#include <QMessageBox>
#include <QProcess>
#include <QCheckBox>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QTableWidget>
#include <QThread>
#include <QVBoxLayout>
namespace spw
{
VirtualDiskTab::VirtualDiskTab(QWidget* parent) : QWidget(parent)
{
setupUi();
}
VirtualDiskTab::~VirtualDiskTab() = default;
QString VirtualDiskTab::formatSize(uint64_t bytes)
{
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 2);
if (bytes >= 1073741824ULL)
return QString("%1 GB").arg(bytes / 1073741824.0, 0, 'f', 1);
if (bytes >= 1048576ULL)
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 0);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
}
void VirtualDiskTab::setupUi()
{
auto* mainLayout = new QVBoxLayout(this);
auto* innerTabs = new QTabWidget();
// ================================================================
// Tab 1: Mount / Unmount
// ================================================================
auto* mountWidget = new QWidget();
auto* mountLayout = new QVBoxLayout(mountWidget);
auto* mountInfo = new QLabel(
tr("Mount VHD or VHDX files as virtual disks. "
"Once mounted, they appear as physical drives and can be accessed in Explorer, "
"formatted, or have data written to them. "
"VMDK and QCOW2 require conversion to VHDX first (see Convert tab)."));
mountInfo->setWordWrap(true);
mountInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
mountLayout->addWidget(mountInfo);
// File selector
auto* fileGroup = new QGroupBox(tr("Virtual Disk File"));
auto* fileLayout = new QHBoxLayout(fileGroup);
m_mountPathEdit = new QLineEdit();
m_mountPathEdit->setPlaceholderText(tr("Select a .vhd or .vhdx file..."));
fileLayout->addWidget(m_mountPathEdit, 1);
m_mountBrowseBtn = new QPushButton(tr("Browse..."));
connect(m_mountBrowseBtn, &QPushButton::clicked, this, &VirtualDiskTab::onBrowseMount);
fileLayout->addWidget(m_mountBrowseBtn);
m_mountReadOnly = new QCheckBox(tr("Read-only"));
fileLayout->addWidget(m_mountReadOnly);
m_mountBtn = new QPushButton(tr("Mount"));
m_mountBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"border-radius: 4px; padding: 5px 14px; }"
"QPushButton:hover { background-color: #e0b584; }");
connect(m_mountBtn, &QPushButton::clicked, this, &VirtualDiskTab::onMount);
fileLayout->addWidget(m_mountBtn);
mountLayout->addWidget(fileGroup);
// Currently mounted table
auto* mountedGroup = new QGroupBox(tr("Currently Mounted Virtual Disks"));
auto* mountedLayout = new QVBoxLayout(mountedGroup);
m_mountedTable = new QTableWidget(0, 4);
m_mountedTable->setHorizontalHeaderLabels({tr("File"), tr("Format"), tr("Virtual Size"), tr("Drive Path")});
m_mountedTable->horizontalHeader()->setStretchLastSection(true);
m_mountedTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_mountedTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_mountedTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
mountedLayout->addWidget(m_mountedTable);
auto* mountedBtnRow = new QHBoxLayout();
m_unmountBtn = new QPushButton(tr("Unmount Selected"));
m_unmountBtn->setStyleSheet(
"QPushButton { background-color: #cc3333; color: white; border-radius: 4px; padding: 5px 14px; }"
"QPushButton:hover { background-color: #ee4444; }");
connect(m_unmountBtn, &QPushButton::clicked, this, &VirtualDiskTab::onUnmount);
mountedBtnRow->addWidget(m_unmountBtn);
m_refreshBtn = new QPushButton(tr("Refresh List"));
connect(m_refreshBtn, &QPushButton::clicked, this, &VirtualDiskTab::onRefreshMounted);
mountedBtnRow->addWidget(m_refreshBtn);
mountedBtnRow->addStretch();
mountedLayout->addLayout(mountedBtnRow);
m_mountStatus = new QLabel();
m_mountStatus->setWordWrap(true);
mountedLayout->addWidget(m_mountStatus);
mountLayout->addWidget(mountedGroup);
innerTabs->addTab(mountWidget, tr("Mount / Unmount"));
// ================================================================
// Tab 2: Create New
// ================================================================
auto* createWidget = new QWidget();
auto* createLayout = new QVBoxLayout(createWidget);
auto* createInfo = new QLabel(
tr("Create a new empty virtual disk. VHDX is recommended — it supports larger sizes, "
"is more resilient, and is the Hyper-V preferred format. "
"VHD is more compatible with older tools and VirtualBox."));
createInfo->setWordWrap(true);
createInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
createLayout->addWidget(createInfo);
auto* createGroup = new QGroupBox(tr("New Virtual Disk"));
auto* createForm = new QFormLayout(createGroup);
auto* createPathRow = new QHBoxLayout();
m_createPathEdit = new QLineEdit();
m_createPathEdit->setPlaceholderText(tr("e.g. C:\\VMs\\disk.vhdx"));
createPathRow->addWidget(m_createPathEdit, 1);
m_createBrowse = new QPushButton(tr("Browse..."));
connect(m_createBrowse, &QPushButton::clicked, this, &VirtualDiskTab::onBrowseCreate);
createPathRow->addWidget(m_createBrowse);
createForm->addRow(tr("Output File:"), createPathRow);
m_createFmtCombo = new QComboBox();
m_createFmtCombo->addItems({
tr("VHDX (recommended — Hyper-V native, up to 64 TB)"),
tr("VHD (legacy — compatible with VirtualBox, up to 2 TB)"),
tr("VMDK (VMware — requires qemu-img)"),
tr("QCOW2 (QEMU native — requires qemu-img)"),
tr("RAW (flat image — maximum compatibility, no metadata)"),
});
createForm->addRow(tr("Format:"), m_createFmtCombo);
auto* sizeRow = new QHBoxLayout();
m_createSizeSpin = new QDoubleSpinBox();
m_createSizeSpin->setRange(0.001, 65536.0);
m_createSizeSpin->setDecimals(3);
m_createSizeSpin->setValue(64.0);
sizeRow->addWidget(m_createSizeSpin, 1);
m_createSizeUnit = new QComboBox();
m_createSizeUnit->addItems({tr("MB"), tr("GB"), tr("TB")});
m_createSizeUnit->setCurrentIndex(1); // GB default
sizeRow->addWidget(m_createSizeUnit);
createForm->addRow(tr("Size:"), sizeRow);
m_createDynamic = new QCheckBox(
tr("Dynamic (grows as data is written — saves disk space)"));
m_createDynamic->setChecked(true);
createForm->addRow(tr("Type:"), m_createDynamic);
createLayout->addWidget(createGroup);
m_createProgress = new QProgressBar();
m_createProgress->setVisible(false);
createLayout->addWidget(m_createProgress);
m_createStatus = new QLabel();
m_createStatus->setWordWrap(true);
createLayout->addWidget(m_createStatus);
m_createBtn = new QPushButton(tr("Create Virtual Disk"));
m_createBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"font-size: 13px; border-radius: 5px; }"
"QPushButton:hover { background-color: #e0b584; }");
connect(m_createBtn, &QPushButton::clicked, this, &VirtualDiskTab::onCreate);
createLayout->addWidget(m_createBtn);
createLayout->addStretch();
innerTabs->addTab(createWidget, tr("Create New"));
// ================================================================
// Tab 3: Capture (Disk → Image)
// ================================================================
auto* capWidget = new QWidget();
auto* capLayout = new QVBoxLayout(capWidget);
auto* capInfo = new QLabel(
tr("Capture a physical disk or SD card as a virtual disk image. "
"The resulting image can be mounted, shared, or flashed back to hardware."));
capInfo->setWordWrap(true);
capInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
capLayout->addWidget(capInfo);
auto* capGroup = new QGroupBox(tr("Capture Settings"));
auto* capForm = new QFormLayout(capGroup);
m_captureSourceCombo = new QComboBox();
capForm->addRow(tr("Source Disk:"), m_captureSourceCombo);
auto* capOutRow = new QHBoxLayout();
m_captureOutEdit = new QLineEdit();
m_captureOutEdit->setPlaceholderText(tr("Output image file path..."));
capOutRow->addWidget(m_captureOutEdit, 1);
m_captureBrowse = new QPushButton(tr("Browse..."));
connect(m_captureBrowse, &QPushButton::clicked, this, [this]() {
auto path = QFileDialog::getSaveFileName(this, tr("Save Captured Image"),
QString(), tr("VHDX (*.vhdx);;VHD (*.vhd);;Raw Image (*.img);;All Files (*)"));
if (!path.isEmpty()) m_captureOutEdit->setText(path);
});
capOutRow->addWidget(m_captureBrowse);
capForm->addRow(tr("Output File:"), capOutRow);
m_captureFmtCombo = new QComboBox();
m_captureFmtCombo->addItems({
tr("VHDX (recommended)"),
tr("VHD"),
tr("RAW (.img — flat copy)"),
});
capForm->addRow(tr("Format:"), m_captureFmtCombo);
capLayout->addWidget(capGroup);
m_captureProgress = new QProgressBar();
m_captureProgress->setVisible(false);
capLayout->addWidget(m_captureProgress);
m_captureStatus = new QLabel();
m_captureStatus->setWordWrap(true);
capLayout->addWidget(m_captureStatus);
m_captureBtn = new QPushButton(tr("Capture Disk to Image"));
m_captureBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"font-size: 13px; border-radius: 5px; }"
"QPushButton:hover { background-color: #e0b584; }");
connect(m_captureBtn, &QPushButton::clicked, this, &VirtualDiskTab::onCapture);
capLayout->addWidget(m_captureBtn);
capLayout->addStretch();
innerTabs->addTab(capWidget, tr("Capture to Image"));
// ================================================================
// Tab 4: Flash (Image → Disk/SD)
// ================================================================
auto* flashWidget = new QWidget();
auto* flashLayout = new QVBoxLayout(flashWidget);
auto* flashInfo = new QLabel(
tr("Flash a virtual disk image (VHD, VHDX, or raw .img) directly to a physical disk "
"or SD card. The image contents replace everything on the target drive."));
flashInfo->setWordWrap(true);
flashInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
flashLayout->addWidget(flashInfo);
auto* flashGroup = new QGroupBox(tr("Flash Settings"));
auto* flashForm = new QFormLayout(flashGroup);
auto* flashImgRow = new QHBoxLayout();
m_flashImageEdit = new QLineEdit();
m_flashImageEdit->setPlaceholderText(tr("Select VHD, VHDX, or .img file..."));
flashImgRow->addWidget(m_flashImageEdit, 1);
m_flashBrowse = new QPushButton(tr("Browse..."));
connect(m_flashBrowse, &QPushButton::clicked, this, &VirtualDiskTab::onBrowseFlashImage);
flashImgRow->addWidget(m_flashBrowse);
flashForm->addRow(tr("Image File:"), flashImgRow);
m_flashTargetCombo = new QComboBox();
flashForm->addRow(tr("Target Disk:"), m_flashTargetCombo);
flashLayout->addWidget(flashGroup);
auto* flashWarnLabel = new QLabel(
tr("⚠ WARNING: All data on the target disk will be overwritten!"));
flashWarnLabel->setStyleSheet("color: #ff9944; font-weight: bold; padding: 4px;");
flashLayout->addWidget(flashWarnLabel);
m_flashProgress = new QProgressBar();
m_flashProgress->setVisible(false);
flashLayout->addWidget(m_flashProgress);
m_flashStatus = new QLabel();
m_flashStatus->setWordWrap(true);
flashLayout->addWidget(m_flashStatus);
m_flashBtn = new QPushButton(tr("Flash Image to Disk"));
m_flashBtn->setStyleSheet(
"QPushButton { background-color: #cc3333; color: white; font-weight: bold; "
"font-size: 13px; border-radius: 5px; }"
"QPushButton:hover { background-color: #ee4444; }");
connect(m_flashBtn, &QPushButton::clicked, this, &VirtualDiskTab::onFlash);
flashLayout->addWidget(m_flashBtn);
flashLayout->addStretch();
innerTabs->addTab(flashWidget, tr("Flash to Disk"));
// ================================================================
// Tab 5: Convert
// ================================================================
auto* convWidget = new QWidget();
auto* convLayout = new QVBoxLayout(convWidget);
auto* convInfo = new QLabel(
tr("Convert between virtual disk formats using qemu-img. "
"Supports VHD ↔ VHDX ↔ VMDK ↔ QCOW2 ↔ RAW.\n"
"qemu-img must be installed and on PATH (install QEMU for Windows)."));
convInfo->setWordWrap(true);
convInfo->setStyleSheet("color: #aaaaaa; font-style: italic;");
convLayout->addWidget(convInfo);
m_qemuStatus = new QLabel();
m_qemuStatus->setWordWrap(true);
// Check qemu-img availability
if (VirtualDisk::qemuImgAvailable())
m_qemuStatus->setText(tr("✓ qemu-img detected — conversion available"));
else
m_qemuStatus->setText(tr("✗ qemu-img not found. Install QEMU (qemu.org) to enable conversion."));
convLayout->addWidget(m_qemuStatus);
auto* convGroup = new QGroupBox(tr("Conversion Settings"));
auto* convForm = new QFormLayout(convGroup);
auto* convInRow = new QHBoxLayout();
m_convInEdit = new QLineEdit();
m_convInEdit->setPlaceholderText(tr("Input image file..."));
convInRow->addWidget(m_convInEdit, 1);
m_convInBrowse = new QPushButton(tr("Browse..."));
connect(m_convInBrowse, &QPushButton::clicked, this, &VirtualDiskTab::onBrowseConvertIn);
convInRow->addWidget(m_convInBrowse);
convForm->addRow(tr("Input:"), convInRow);
m_convFmtCombo = new QComboBox();
m_convFmtCombo->addItems({
tr("VHDX (Hyper-V / Windows)"),
tr("VHD (Legacy / VirtualBox)"),
tr("VMDK (VMware)"),
tr("QCOW2 (QEMU / KVM)"),
tr("RAW (flat .img)"),
});
convForm->addRow(tr("Output Format:"), m_convFmtCombo);
auto* convOutRow = new QHBoxLayout();
m_convOutEdit = new QLineEdit();
m_convOutEdit->setPlaceholderText(tr("Output file path..."));
convOutRow->addWidget(m_convOutEdit, 1);
m_convOutBrowse = new QPushButton(tr("Browse..."));
connect(m_convOutBrowse, &QPushButton::clicked, this, &VirtualDiskTab::onBrowseConvertOut);
convOutRow->addWidget(m_convOutBrowse);
convForm->addRow(tr("Output:"), convOutRow);
convLayout->addWidget(convGroup);
m_convProgress = new QProgressBar();
m_convProgress->setRange(0, 0);
m_convProgress->setVisible(false);
convLayout->addWidget(m_convProgress);
m_convStatus = new QLabel();
m_convStatus->setWordWrap(true);
convLayout->addWidget(m_convStatus);
m_convertBtn = new QPushButton(tr("Convert"));
m_convertBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-weight: bold; "
"font-size: 13px; border-radius: 5px; }"
"QPushButton:hover { background-color: #e0b584; }");
connect(m_convertBtn, &QPushButton::clicked, this, &VirtualDiskTab::onConvert);
convLayout->addWidget(m_convertBtn);
convLayout->addStretch();
innerTabs->addTab(convWidget, tr("Convert Format"));
mainLayout->addWidget(innerTabs);
}
// ============================================================================
// refreshDisks
// ============================================================================
void VirtualDiskTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void VirtualDiskTab::populateDiskCombo()
{
m_captureSourceCombo->clear();
m_flashTargetCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_captureSourceCombo->addItem(label, disk.id);
m_flashTargetCombo->addItem(label, disk.id);
}
}
// ============================================================================
// Browse slots
// ============================================================================
void VirtualDiskTab::onBrowseMount()
{
auto path = QFileDialog::getOpenFileName(this, tr("Select Virtual Disk"),
QString(), tr("Virtual Disks (*.vhd *.vhdx *.vmdk *.qcow2 *.img);;All Files (*)"));
if (!path.isEmpty()) m_mountPathEdit->setText(path);
}
void VirtualDiskTab::onBrowseCreate()
{
static const char* exts[] = { ".vhdx", ".vhd", ".vmdk", ".qcow2", ".img" };
int fmtIdx = m_createFmtCombo->currentIndex();
QString ext = exts[fmtIdx < 5 ? fmtIdx : 0];
auto path = QFileDialog::getSaveFileName(this, tr("Save Virtual Disk"),
QString(), tr("Virtual Disk (*%1);;All Files (*)").arg(ext));
if (!path.isEmpty()) m_createPathEdit->setText(path);
}
void VirtualDiskTab::onBrowseFlashImage()
{
auto path = QFileDialog::getOpenFileName(this, tr("Select Image to Flash"),
QString(), tr("Virtual Disks (*.vhd *.vhdx *.vmdk *.img);;All Files (*)"));
if (!path.isEmpty()) m_flashImageEdit->setText(path);
}
void VirtualDiskTab::onBrowseConvertIn()
{
auto path = QFileDialog::getOpenFileName(this, tr("Select Input Image"),
QString(), tr("Virtual Disks (*.vhd *.vhdx *.vmdk *.qcow2 *.img);;All Files (*)"));
if (!path.isEmpty()) m_convInEdit->setText(path);
}
void VirtualDiskTab::onBrowseConvertOut()
{
auto path = QFileDialog::getSaveFileName(this, tr("Output File"),
QString(), tr("Virtual Disks (*.vhd *.vhdx *.vmdk *.qcow2 *.img);;All Files (*)"));
if (!path.isEmpty()) m_convOutEdit->setText(path);
}
// ============================================================================
// Mount / Unmount
// ============================================================================
void VirtualDiskTab::onMount()
{
QString path = m_mountPathEdit->text().trimmed();
if (path.isEmpty()) { QMessageBox::warning(this, tr("Mount"), tr("No file selected.")); return; }
bool readOnly = m_mountReadOnly->isChecked();
m_mountBtn->setEnabled(false);
m_mountStatus->setText(tr("Mounting..."));
auto* thread = QThread::create([this, path, readOnly]() {
auto result = VirtualDisk::mount(path.toStdWString(), readOnly);
QMetaObject::invokeMethod(this, [this, result, path]() {
m_mountBtn->setEnabled(true);
if (result.isError())
{
m_mountStatus->setText(tr("✗ Mount failed: %1").arg(
QString::fromStdString(result.error().message)));
}
else
{
const auto& info = result.value();
m_mountStatus->setText(tr("✓ Mounted as %1")
.arg(QString::fromStdWString(info.physicalDrivePath)));
onRefreshMounted();
emit statusMessage(tr("Virtual disk mounted: %1").arg(path));
}
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
void VirtualDiskTab::onUnmount()
{
int row = m_mountedTable->currentRow();
if (row < 0) { QMessageBox::information(this, tr("Unmount"), tr("Select a disk to unmount.")); return; }
QString filePath = m_mountedTable->item(row, 0)->text();
auto result = VirtualDisk::unmount(filePath.toStdWString());
if (result.isError())
m_mountStatus->setText(tr("✗ Unmount failed: %1").arg(QString::fromStdString(result.error().message)));
else
{
m_mountStatus->setText(tr("✓ Unmounted."));
onRefreshMounted();
emit statusMessage(tr("Virtual disk unmounted"));
}
}
void VirtualDiskTab::onRefreshMounted()
{
// There's no Windows API to enumerate all attached VHDs easily.
// Best we can do is show the last mounted one or clear on unmount.
// For now just acknowledge the action; a full implementation would
// query the VHD service via WMI Msvm_StorageAllocationSettingData.
m_mountedTable->setRowCount(0);
}
// ============================================================================
// Create
// ============================================================================
void VirtualDiskTab::onCreate()
{
QString path = m_createPathEdit->text().trimmed();
if (path.isEmpty()) { QMessageBox::warning(this, tr("Create"), tr("No output path specified.")); return; }
static const VirtualDiskFormat fmts[] = {
VirtualDiskFormat::VHDX, VirtualDiskFormat::VHD,
VirtualDiskFormat::VMDK, VirtualDiskFormat::QCOW2, VirtualDiskFormat::RAW
};
VirtualDiskCreateParams params;
params.filePath = path.toStdWString();
params.format = fmts[m_createFmtCombo->currentIndex()];
params.dynamic = m_createDynamic->isChecked();
double size = m_createSizeSpin->value();
int unit = m_createSizeUnit->currentIndex();
uint64_t multiplier = (unit == 0) ? 1024ULL * 1024
: (unit == 1) ? 1024ULL * 1024 * 1024
: 1024ULL * 1024 * 1024 * 1024;
params.sizeBytes = static_cast<uint64_t>(size * multiplier);
m_createBtn->setEnabled(false);
m_createProgress->setVisible(true);
m_createProgress->setRange(0, 0);
m_createStatus->setText(tr("Creating..."));
auto* thread = QThread::create([this, params]() {
auto result = VirtualDisk::create(params,
[this](const std::string& s, int p) {
QMetaObject::invokeMethod(m_createStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(s)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_createProgress->setVisible(false);
m_createBtn->setEnabled(true);
m_createStatus->setText(result.isOk()
? tr("✓ Virtual disk created successfully.")
: tr("✗ Failed: %1").arg(QString::fromStdString(result.error().message)));
if (result.isOk()) emit statusMessage(tr("Virtual disk created"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
// ============================================================================
// Capture
// ============================================================================
void VirtualDiskTab::onCapture()
{
int diskId = m_captureSourceCombo->currentData().toInt();
QString outPath = m_captureOutEdit->text().trimmed();
if (outPath.isEmpty()) { QMessageBox::warning(this, tr("Capture"), tr("No output path specified.")); return; }
static const VirtualDiskFormat fmts[] = {
VirtualDiskFormat::VHDX, VirtualDiskFormat::VHD, VirtualDiskFormat::RAW
};
VirtualDiskFormat fmt = fmts[m_captureFmtCombo->currentIndex()];
auto reply = QMessageBox::question(this, tr("Capture Disk"),
tr("Capture Disk %1 to:\n%2\n\nThis will read the entire disk. Continue?")
.arg(diskId).arg(outPath),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_captureBtn->setEnabled(false);
m_captureProgress->setVisible(true);
m_captureProgress->setRange(0, 100);
m_captureStatus->setText(tr("Capturing..."));
auto* thread = QThread::create([this, diskId, outPath, fmt]() {
auto result = VirtualDisk::captureFromDisk(diskId, outPath.toStdWString(), fmt,
[this](const std::string& s, int p) {
QMetaObject::invokeMethod(m_captureProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, p));
QMetaObject::invokeMethod(m_captureStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(s)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_captureProgress->setVisible(false);
m_captureBtn->setEnabled(true);
m_captureStatus->setText(result.isOk()
? tr("✓ Capture complete.")
: tr("✗ Failed: %1").arg(QString::fromStdString(result.error().message)));
if (result.isOk()) emit statusMessage(tr("Disk captured to image"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
// ============================================================================
// Flash
// ============================================================================
void VirtualDiskTab::onFlash()
{
QString imagePath = m_flashImageEdit->text().trimmed();
int targetDiskId = m_flashTargetCombo->currentData().toInt();
if (imagePath.isEmpty()) { QMessageBox::warning(this, tr("Flash"), tr("No image file selected.")); return; }
auto reply = QMessageBox::critical(this, tr("Flash to Disk"),
tr("This will OVERWRITE ALL DATA on Disk %1.\n\n"
"Image: %2\n\nThis is irreversible. Continue?")
.arg(targetDiskId).arg(imagePath),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
m_flashBtn->setEnabled(false);
m_flashProgress->setVisible(true);
m_flashProgress->setRange(0, 100);
m_flashStatus->setText(tr("Flashing..."));
auto* thread = QThread::create([this, imagePath, targetDiskId]() {
auto result = VirtualDisk::flashToDisk(imagePath.toStdWString(), targetDiskId,
[this](const std::string& s, int p) {
QMetaObject::invokeMethod(m_flashProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, p));
QMetaObject::invokeMethod(m_flashStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(s)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_flashProgress->setVisible(false);
m_flashBtn->setEnabled(true);
m_flashStatus->setText(result.isOk()
? tr("✓ Flash complete.")
: tr("✗ Failed: %1").arg(QString::fromStdString(result.error().message)));
if (result.isOk()) emit statusMessage(tr("Virtual disk flashed to physical disk"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
// ============================================================================
// Convert
// ============================================================================
void VirtualDiskTab::onConvert()
{
QString inPath = m_convInEdit->text().trimmed();
QString outPath = m_convOutEdit->text().trimmed();
if (inPath.isEmpty() || outPath.isEmpty())
{
QMessageBox::warning(this, tr("Convert"), tr("Specify both input and output files."));
return;
}
static const VirtualDiskFormat fmts[] = {
VirtualDiskFormat::VHDX, VirtualDiskFormat::VHD,
VirtualDiskFormat::VMDK, VirtualDiskFormat::QCOW2, VirtualDiskFormat::RAW
};
VirtualDiskFormat fmt = fmts[m_convFmtCombo->currentIndex()];
m_convertBtn->setEnabled(false);
m_convProgress->setVisible(true);
m_convStatus->setText(tr("Converting..."));
auto* thread = QThread::create([this, inPath, outPath, fmt]() {
auto result = VirtualDisk::convert(inPath.toStdWString(), outPath.toStdWString(), fmt,
[this](const std::string& s, int) {
QMetaObject::invokeMethod(m_convStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(s)));
});
QMetaObject::invokeMethod(this, [this, result]() {
m_convProgress->setVisible(false);
m_convertBtn->setEnabled(true);
m_convStatus->setText(result.isOk()
? tr("✓ Conversion complete.")
: tr("✗ Failed: %1").arg(QString::fromStdString(result.error().message)));
if (result.isOk()) emit statusMessage(tr("Virtual disk conversion complete"));
}, Qt::QueuedConnection);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
// ============================================================================
// WSL2 slots (placeholder — full implementation requires NonWindowsFsTab)
// ============================================================================
void VirtualDiskTab::onWslCheckAvailable()
{
// Check if WSL2 is available and has wsl --mount support (Win10 21H2+)
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start("wsl.exe", {"--status"});
p.waitForFinished(5000);
if (p.exitCode() == 0)
emit statusMessage(tr("WSL2 is available — use 'Linux Filesystems' tab to mount ext4/Btrfs/XFS"));
else
emit statusMessage(tr("WSL2 not detected — install WSL2 for Linux filesystem access"));
}
void VirtualDiskTab::onWslMount()
{
// Full implementation is in NonWindowsFsTab
QMessageBox::information(this, tr("WSL2 Mount"),
tr("Use the 'Linux Filesystems' tab to mount ext4, Btrfs, XFS, F2FS, and other "
"Linux filesystems via WSL2.\n\n"
"That tab provides full mount/unmount control with drive letter assignment."));
}
void VirtualDiskTab::onWslUnmount()
{
QProcess p;
p.start("wsl.exe", {"--unmount"});
p.waitForFinished(10000);
emit statusMessage(tr("WSL2 disk unmount complete"));
}
} // namespace spw

View File

@@ -0,0 +1,122 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/imaging/VirtualDisk.h"
#include <QWidget>
#include <atomic>
class QComboBox;
class QCheckBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QDoubleSpinBox;
class QTabWidget;
class QTableWidget;
namespace spw
{
class VirtualDiskTab : public QWidget
{
Q_OBJECT
public:
explicit VirtualDiskTab(QWidget* parent = nullptr);
~VirtualDiskTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onMount();
void onUnmount();
void onRefreshMounted();
void onBrowseMount();
void onCreate();
void onBrowseCreate();
void onCapture();
void onFlash();
void onConvert();
void onBrowseConvertIn();
void onBrowseConvertOut();
void onBrowseFlashImage();
void onWslMount();
void onWslUnmount();
void onWslCheckAvailable();
private:
void setupUi();
void populateDiskCombo();
static QString formatSize(uint64_t bytes);
// Mount / Unmount
QLineEdit* m_mountPathEdit = nullptr;
QPushButton* m_mountBrowseBtn = nullptr;
QCheckBox* m_mountReadOnly = nullptr;
QPushButton* m_mountBtn = nullptr;
QTableWidget* m_mountedTable = nullptr;
QPushButton* m_unmountBtn = nullptr;
QPushButton* m_refreshBtn = nullptr;
QLabel* m_mountStatus = nullptr;
// Create
QLineEdit* m_createPathEdit = nullptr;
QPushButton* m_createBrowse = nullptr;
QComboBox* m_createFmtCombo = nullptr;
QDoubleSpinBox* m_createSizeSpin = nullptr;
QComboBox* m_createSizeUnit = nullptr;
QCheckBox* m_createDynamic = nullptr;
QPushButton* m_createBtn = nullptr;
QProgressBar* m_createProgress = nullptr;
QLabel* m_createStatus = nullptr;
// Capture (disk → image)
QComboBox* m_captureSourceCombo = nullptr;
QLineEdit* m_captureOutEdit = nullptr;
QPushButton* m_captureBrowse = nullptr;
QComboBox* m_captureFmtCombo = nullptr;
QPushButton* m_captureBtn = nullptr;
QProgressBar* m_captureProgress = nullptr;
QLabel* m_captureStatus = nullptr;
// Flash (image → disk/SD)
QLineEdit* m_flashImageEdit = nullptr;
QPushButton* m_flashBrowse = nullptr;
QComboBox* m_flashTargetCombo = nullptr;
QPushButton* m_flashBtn = nullptr;
QProgressBar* m_flashProgress = nullptr;
QLabel* m_flashStatus = nullptr;
// Convert
QLineEdit* m_convInEdit = nullptr;
QPushButton* m_convInBrowse = nullptr;
QLineEdit* m_convOutEdit = nullptr;
QPushButton* m_convOutBrowse = nullptr;
QComboBox* m_convFmtCombo = nullptr;
QLabel* m_qemuStatus = nullptr;
QPushButton* m_convertBtn = nullptr;
QProgressBar* m_convProgress = nullptr;
QLabel* m_convStatus = nullptr;
// WSL2 Linux filesystem mount
QComboBox* m_wslDiskCombo = nullptr;
QComboBox* m_wslFsCombo = nullptr;
QCheckBox* m_wslReadOnly = nullptr;
QPushButton* m_wslMountBtn = nullptr;
QPushButton* m_wslUnmountBtn = nullptr;
QLabel* m_wslStatus = nullptr;
QLabel* m_wslAvailLabel = nullptr;
SystemDiskSnapshot m_snapshot;
};
} // namespace spw

136
third_party/xz-embedded/xz.h vendored Normal file
View File

@@ -0,0 +1,136 @@
/*
* XZ decompressor - Public API header
*
* Based on xz-embedded by Lasse Collin (public domain).
* Minimal subset for streaming XZ decompression.
*/
#ifndef XZ_H
#define XZ_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* enum xz_mode - Operation mode
*
* @XZ_SINGLE: Single-call mode. The caller provides the full input and
* output buffers in one call.
* @XZ_PREALLOC: Multi-call mode with preallocated dictionary buffer.
* @XZ_DYNALLOC: Multi-call mode with dynamically allocated dictionary.
*/
enum xz_mode {
XZ_SINGLE,
XZ_PREALLOC,
XZ_DYNALLOC
};
/**
* enum xz_ret - Return codes
*
* @XZ_OK: Everything is OK so far. More input or output
* space is needed to continue.
* @XZ_STREAM_END: Operation finished successfully.
* @XZ_UNSUPPORTED_CHECK: Integrity check type is not supported. Decoding
* is still possible by simply ignoring the check.
* @XZ_MEM_ERROR: Allocating memory failed.
* @XZ_MEMLIMIT_ERROR: A bigger dictionary would be needed than allowed
* by dict_max in xz_dec_init().
* @XZ_FORMAT_ERROR: File format was not recognized (wrong magic bytes).
* @XZ_OPTIONS_ERROR: This implementation doesn't support the requested
* compression options. In the decoder this means
* unsupported header flags.
* @XZ_DATA_ERROR: Compressed data is corrupt.
* @XZ_BUF_ERROR: Cannot make any progress. Details depend on
* function being called.
*/
enum xz_ret {
XZ_OK,
XZ_STREAM_END,
XZ_UNSUPPORTED_CHECK,
XZ_MEM_ERROR,
XZ_MEMLIMIT_ERROR,
XZ_FORMAT_ERROR,
XZ_OPTIONS_ERROR,
XZ_DATA_ERROR,
XZ_BUF_ERROR
};
/**
* struct xz_buf - Passing input and output buffers to XZ code
*
* @in: Beginning of the input buffer.
* @in_pos: Current position in the input buffer. This must not exceed
* in_size.
* @in_size: Size of the input buffer.
* @out: Beginning of the output buffer.
* @out_pos: Current position in the output buffer. This must not exceed
* out_size.
* @out_size: Size of the output buffer.
*/
struct xz_buf {
const uint8_t *in;
size_t in_pos;
size_t in_size;
uint8_t *out;
size_t out_pos;
size_t out_size;
};
/* Opaque decoder state */
struct xz_dec;
/**
* xz_crc32_init() - Initialize the CRC32 lookup table.
* Must be called before any CRC32 use (including xz_dec_run).
*/
void xz_crc32_init(void);
/**
* xz_crc64_init() - Initialize the CRC64 lookup table.
* Must be called if the stream uses CRC64 checks.
*/
void xz_crc64_init(void);
/**
* xz_dec_init() - Allocate and initialize a XZ decoder state.
* @mode: XZ_SINGLE, XZ_PREALLOC, or XZ_DYNALLOC.
* @dict_max: Maximum allowed dictionary size. Use 0 for default.
*
* Returns NULL on allocation failure.
*/
struct xz_dec *xz_dec_init(enum xz_mode mode, uint32_t dict_max);
/**
* xz_dec_run() - Run the XZ decoder.
* @s: Decoder state allocated with xz_dec_init().
* @b: Input/output buffer pointers.
*
* See enum xz_ret for possible return values.
*/
enum xz_ret xz_dec_run(struct xz_dec *s, struct xz_buf *b);
/**
* xz_dec_reset() - Reset an already allocated decoder state.
* @s: Decoder state allocated with xz_dec_init().
*
* This allows reusing the decoder state for decoding another stream.
*/
void xz_dec_reset(struct xz_dec *s);
/**
* xz_dec_end() - Free the decoder state.
* @s: Decoder state allocated with xz_dec_init(). Passing NULL is safe.
*/
void xz_dec_end(struct xz_dec *s);
#ifdef __cplusplus
}
#endif
#endif /* XZ_H */

41
third_party/xz-embedded/xz_crc32.c vendored Normal file
View File

@@ -0,0 +1,41 @@
/*
* XZ decompressor - CRC32
*
* Based on xz-embedded by Lasse Collin (public domain).
* Standard CRC32 with polynomial 0xEDB88320 (bit-reversed 0x04C11DB7).
*/
#include "xz_private.h"
uint32_t xz_crc32_table[256];
void xz_crc32_init(void)
{
static int done = 0;
uint32_t i, j, r;
if (done)
return;
for (i = 0; i < 256; ++i) {
r = i;
for (j = 0; j < 8; ++j)
r = (r >> 1) ^ (0xEDB88320 & ~((r & 1) - 1));
xz_crc32_table[i] = r;
}
done = 1;
}
uint32_t xz_crc32(const uint8_t *buf, size_t size, uint32_t crc)
{
crc = ~crc;
while (size > 0) {
crc = xz_crc32_table[(crc ^ *buf) & 0xFF] ^ (crc >> 8);
++buf;
--size;
}
return ~crc;
}

41
third_party/xz-embedded/xz_crc64.c vendored Normal file
View File

@@ -0,0 +1,41 @@
/*
* XZ decompressor - CRC64
*
* Based on xz-embedded by Lasse Collin (public domain).
* CRC64 with polynomial 0xC96C5795D7870F42 (ECMA-182).
*/
#include "xz_private.h"
uint64_t xz_crc64_table[256];
void xz_crc64_init(void)
{
static int done = 0;
uint64_t i, j, r;
if (done)
return;
for (i = 0; i < 256; ++i) {
r = i;
for (j = 0; j < 8; ++j)
r = (r >> 1) ^ (UINT64_C(0xC96C5795D7870F42) & ~((r & 1) - 1));
xz_crc64_table[i] = r;
}
done = 1;
}
uint64_t xz_crc64(const uint8_t *buf, size_t size, uint64_t crc)
{
crc = ~crc;
while (size > 0) {
crc = xz_crc64_table[(crc ^ *buf) & 0xFF] ^ (crc >> 8);
++buf;
--size;
}
return ~crc;
}

816
third_party/xz-embedded/xz_dec_lzma2.c vendored Normal file
View File

@@ -0,0 +1,816 @@
/*
* XZ decompressor - LZMA2 decoder
*
* Based on xz-embedded by Lasse Collin (public domain).
*
* This implements:
* - LZMA2 chunk parsing (control bytes, property resets, dictionary resets)
* - The LZMA range decoder
* - The full LZMA algorithm (literals, matches, short/long reps)
* - Dictionary management as a circular buffer
*/
#include "xz_lzma2.h"
/*
* ============================================================
* Range decoder
* ============================================================
*/
struct rc_dec {
uint32_t range;
uint32_t code;
uint32_t init_bytes_left;
const uint8_t *in;
size_t in_pos;
size_t in_limit;
};
static inline void rc_reset(struct rc_dec *rc)
{
rc->range = 0xFFFFFFFF;
rc->code = 0;
rc->init_bytes_left = 5;
}
static inline int rc_read_init(struct rc_dec *rc, struct xz_buf *b)
{
while (rc->init_bytes_left > 0) {
if (b->in_pos == b->in_size)
return 0;
rc->code = (rc->code << 8) + b->in[b->in_pos++];
--rc->init_bytes_left;
}
return 1;
}
static inline void rc_normalize(struct rc_dec *rc)
{
if (rc->range < RC_TOP_VALUE) {
rc->range <<= RC_SHIFT_BITS;
rc->code = (rc->code << RC_SHIFT_BITS) | rc->in[rc->in_pos++];
}
}
static inline int rc_bit(struct rc_dec *rc, uint16_t *prob)
{
uint32_t bound;
rc_normalize(rc);
bound = (rc->range >> RC_BIT_MODEL_TOTAL_BITS) * *prob;
if (rc->code < bound) {
rc->range = bound;
*prob += (RC_BIT_MODEL_TOTAL - *prob) >> RC_MOVE_BITS;
return 0;
} else {
rc->range -= bound;
rc->code -= bound;
*prob -= *prob >> RC_MOVE_BITS;
return 1;
}
}
static inline uint32_t rc_bittree(struct rc_dec *rc, uint16_t *probs,
uint32_t limit)
{
uint32_t symbol = 1;
do {
if (rc_bit(rc, &probs[symbol]))
symbol = (symbol << 1) + 1;
else
symbol <<= 1;
} while (symbol < limit);
return symbol;
}
static inline uint32_t rc_bittree_reverse(struct rc_dec *rc, uint16_t *probs,
uint32_t bits)
{
uint32_t symbol = 1;
uint32_t i, result = 0;
for (i = 0; i < bits; ++i) {
if (rc_bit(rc, &probs[symbol])) {
symbol = (symbol << 1) + 1;
result |= 1U << i;
} else {
symbol <<= 1;
}
}
return result;
}
static inline uint32_t rc_direct(struct rc_dec *rc, uint32_t count)
{
uint32_t result = 0;
do {
rc_normalize(rc);
rc->range >>= 1;
rc->code -= rc->range;
result = (result << 1) + (rc->code >> 31) + 1;
rc->code += rc->range & (rc->code >> 31);
} while (--count > 0);
return result;
}
/*
* ============================================================
* Dictionary (circular buffer)
* ============================================================
*/
struct dictionary {
uint8_t *buf;
size_t pos;
size_t full;
size_t limit;
size_t end;
uint32_t size;
enum xz_mode mode;
uint32_t allocated;
};
static void dict_reset(struct dictionary *dict, struct xz_buf *b)
{
if (dict->mode == XZ_SINGLE) {
dict->buf = b->out + b->out_pos;
dict->end = b->out_size - b->out_pos;
}
dict->pos = 0;
dict->full = 0;
dict->limit = dict->end;
}
static inline void dict_limit(struct dictionary *dict, size_t out_max)
{
if (dict->mode == XZ_SINGLE)
return;
if (dict->end - dict->pos <= out_max)
dict->limit = dict->end;
else
dict->limit = dict->pos + out_max;
}
static inline int dict_has_space(const struct dictionary *dict)
{
return dict->pos < dict->limit;
}
static inline uint8_t dict_get(const struct dictionary *dict, uint32_t dist)
{
size_t offset = dict->pos - dist - 1;
if (dict->pos <= dist)
offset += dict->end;
return dict->buf[offset];
}
static inline void dict_put(struct dictionary *dict, uint8_t byte)
{
dict->buf[dict->pos++] = byte;
if (dict->full < dict->pos)
dict->full = dict->pos;
}
static int dict_repeat(struct dictionary *dict, uint32_t *len, uint32_t dist)
{
size_t back;
uint32_t left;
if (dist >= dict->full || dist >= dict->size)
return 0;
left = min_t(uint32_t, (uint32_t)(dict->limit - dict->pos), *len);
*len -= left;
back = dict->pos - dist - 1;
if (dict->pos <= dist)
back += dict->end;
while (left > 0) {
dict->buf[dict->pos++] = dict->buf[back++];
if (back == dict->end)
back = 0;
if (dict->full < dict->pos)
dict->full = dict->pos;
--left;
}
return 1;
}
static void dict_uncompressed(struct dictionary *dict, struct xz_buf *b,
uint32_t *left)
{
size_t copy_size;
copy_size = min_t(size_t, b->in_size - b->in_pos,
min_t(size_t, *left, dict->limit - dict->pos));
memcpy(dict->buf + dict->pos, b->in + b->in_pos, copy_size);
dict->pos += copy_size;
if (dict->full < dict->pos)
dict->full = dict->pos;
b->in_pos += copy_size;
*left -= (uint32_t)copy_size;
}
static uint32_t dict_flush(struct dictionary *dict, struct xz_buf *b)
{
size_t copy_size;
if (dict->mode == XZ_SINGLE) {
size_t out = dict->pos;
b->out_pos += out;
return (uint32_t)out;
}
copy_size = min_t(size_t, b->out_size - b->out_pos, dict->pos);
memcpy(b->out + b->out_pos, dict->buf, copy_size);
b->out_pos += copy_size;
if (copy_size < dict->pos)
memmove(dict->buf, dict->buf + copy_size, dict->pos - copy_size);
dict->pos -= copy_size;
return (uint32_t)copy_size;
}
/*
* ============================================================
* LZMA decoder
* ============================================================
*/
struct lzma_dec {
struct rc_dec rc;
uint32_t state;
uint32_t rep0, rep1, rep2, rep3;
uint32_t lc;
uint32_t literal_pos_mask;
uint32_t pos_mask;
/* Match length decoder */
uint16_t match_len_choice;
uint16_t match_len_choice2;
uint16_t match_len_low[POS_STATES_MAX][LEN_LOW_SYMBOLS];
uint16_t match_len_mid[POS_STATES_MAX][LEN_MID_SYMBOLS];
uint16_t match_len_high[LEN_HIGH_SYMBOLS];
/* Rep length decoder */
uint16_t rep_len_choice;
uint16_t rep_len_choice2;
uint16_t rep_len_low[POS_STATES_MAX][LEN_LOW_SYMBOLS];
uint16_t rep_len_mid[POS_STATES_MAX][LEN_MID_SYMBOLS];
uint16_t rep_len_high[LEN_HIGH_SYMBOLS];
/* Main probabilities */
uint16_t is_match[STATES][POS_STATES_MAX];
uint16_t is_rep[STATES];
uint16_t is_rep0[STATES];
uint16_t is_rep1[STATES];
uint16_t is_rep2[STATES];
uint16_t is_rep0_long[STATES][POS_STATES_MAX];
uint16_t dist_slot[DIST_STATES][DIST_SLOTS];
uint16_t dist_special[FULL_DISTANCES - DIST_MODEL_END];
uint16_t dist_align[ALIGN_SIZE];
/* Literal probabilities: 3 * 2^lc entries per position coder */
uint16_t literal[LITERAL_CODERS_MAX][0x300];
};
static void lzma_reset(struct lzma_dec *lzma)
{
uint16_t *p;
size_t count, i;
lzma->state = 0;
lzma->rep0 = 0;
lzma->rep1 = 0;
lzma->rep2 = 0;
lzma->rep3 = 0;
/* Initialize all probabilities to RC_BIT_MODEL_TOTAL / 2 */
p = &lzma->match_len_choice;
count = (size_t)((uint8_t *)&lzma->literal[LITERAL_CODERS_MAX][0]
- (uint8_t *)&lzma->match_len_choice) / sizeof(uint16_t);
for (i = 0; i < count; ++i)
p[i] = RC_BIT_MODEL_TOTAL / 2;
}
static void lzma_literal(struct lzma_dec *lzma, struct dictionary *dict)
{
uint16_t *probs;
uint32_t symbol;
uint32_t literal_pos;
literal_pos = dict->pos & lzma->literal_pos_mask;
probs = lzma->literal[literal_pos << lzma->lc];
if (dict->pos > 0 || dict->full > 0) {
uint8_t prev = dict_get(dict, 0);
probs = lzma->literal[((literal_pos << lzma->lc)
+ ((uint32_t)prev >> (8 - lzma->lc)))
& ((LITERAL_CODERS_MAX - 1))];
}
if (lzma_state_is_literal(lzma->state)) {
symbol = rc_bittree(&lzma->rc, probs - 1, 0x100);
} else {
uint32_t match_byte = dict_get(dict, lzma->rep0);
uint32_t offset = 0x100;
symbol = 1;
do {
uint32_t match_bit, bit;
match_byte <<= 1;
match_bit = match_byte & offset;
bit = rc_bit(&lzma->rc, &probs[offset + match_bit + symbol]);
symbol = (symbol << 1) | bit;
offset &= ~(match_byte ^ (bit ? ~(uint32_t)0 : 0)) & 0x100;
} while (symbol < 0x100);
}
dict_put(dict, (uint8_t)symbol);
lzma->state = lzma_state_literal(lzma->state);
}
static uint32_t lzma_len_decode(struct rc_dec *rc,
uint16_t *choice, uint16_t *choice2,
uint16_t low[][LEN_LOW_SYMBOLS],
uint16_t mid[][LEN_MID_SYMBOLS],
uint16_t *high,
uint32_t pos_state)
{
if (!rc_bit(rc, choice))
return rc_bittree(rc, low[pos_state] - 1, LEN_LOW_SYMBOLS)
- LEN_LOW_SYMBOLS + MATCH_LEN_MIN;
if (!rc_bit(rc, choice2))
return rc_bittree(rc, mid[pos_state] - 1, LEN_MID_SYMBOLS)
- LEN_MID_SYMBOLS + MATCH_LEN_MIN + LEN_LOW_SYMBOLS;
return rc_bittree(rc, high - 1, LEN_HIGH_SYMBOLS)
- LEN_HIGH_SYMBOLS + MATCH_LEN_MIN + LEN_LOW_SYMBOLS
+ LEN_MID_SYMBOLS;
}
/*
* ============================================================
* LZMA2 decoder
* ============================================================
*/
enum lzma2_seq {
SEQ_LZMA2_CONTROL,
SEQ_LZMA2_UNCOMPRESSED_1,
SEQ_LZMA2_UNCOMPRESSED_2,
SEQ_LZMA2_COMPRESSED_0,
SEQ_LZMA2_COMPRESSED_1,
SEQ_LZMA2_PROPS,
SEQ_LZMA2_LZMA_PREPARE,
SEQ_LZMA2_LZMA_RUN,
SEQ_LZMA2_COPY
};
struct xz_dec_lzma2 {
enum lzma2_seq sequence;
/* Current LZMA2 control byte */
uint32_t control;
/* Compressed and uncompressed sizes for the current chunk */
uint32_t compressed;
uint32_t uncompressed;
int need_lzma_init;
int need_dict_reset;
int need_props;
/* Leftover match length from previous call */
uint32_t match_len;
struct lzma_dec lzma;
struct dictionary dict;
};
/*
* Decode LZMA symbols from the range-coded stream.
* Returns 1 on success, 0 on error (invalid distance reference).
*/
static int lzma_decode_loop(struct xz_dec_lzma2 *s, struct xz_buf *b,
size_t in_avail)
{
struct lzma_dec *lzma = &s->lzma;
struct dictionary *dict = &s->dict;
struct rc_dec *rc = &lzma->rc;
uint32_t pos_state;
rc->in = b->in;
rc->in_pos = b->in_pos;
rc->in_limit = b->in_pos + in_avail;
/* Finish leftover match */
if (s->match_len > 0) {
if (!dict_repeat(dict, &s->match_len, lzma->rep0)) {
/* Dictionary full, need to flush output first */
s->compressed -= (uint32_t)(rc->in_pos - b->in_pos);
b->in_pos = rc->in_pos;
return 1;
}
}
while (dict_has_space(dict) && rc->in_pos < rc->in_limit) {
pos_state = dict->pos & lzma->pos_mask;
if (!rc_bit(rc, &lzma->is_match[lzma->state][pos_state])) {
lzma_literal(lzma, dict);
} else if (rc_bit(rc, &lzma->is_rep[lzma->state])) {
/* Repeated match */
uint32_t len;
if (!rc_bit(rc, &lzma->is_rep0[lzma->state])) {
if (!rc_bit(rc, &lzma->is_rep0_long[lzma->state][pos_state])) {
/* Short rep0 (single byte) */
if (dict->full == 0) {
s->compressed -= (uint32_t)(rc->in_pos - b->in_pos);
b->in_pos = rc->in_pos;
return 0;
}
dict_put(dict, dict_get(dict, lzma->rep0));
lzma->state = lzma_state_short_rep(lzma->state);
continue;
}
/* Long rep0 -- distance stays rep0 */
} else {
uint32_t tmp;
if (!rc_bit(rc, &lzma->is_rep1[lzma->state])) {
tmp = lzma->rep1;
} else if (!rc_bit(rc, &lzma->is_rep2[lzma->state])) {
tmp = lzma->rep2;
lzma->rep2 = lzma->rep1;
} else {
tmp = lzma->rep3;
lzma->rep3 = lzma->rep2;
lzma->rep2 = lzma->rep1;
}
lzma->rep1 = lzma->rep0;
lzma->rep0 = tmp;
}
len = lzma_len_decode(rc,
&lzma->rep_len_choice,
&lzma->rep_len_choice2,
lzma->rep_len_low,
lzma->rep_len_mid,
lzma->rep_len_high,
pos_state);
lzma->state = lzma_state_long_rep(lzma->state);
if (!dict_repeat(dict, &len, lzma->rep0)) {
s->match_len = len;
break;
}
} else {
/* Normal match */
uint32_t len, dist_slot, dist;
lzma->rep3 = lzma->rep2;
lzma->rep2 = lzma->rep1;
lzma->rep1 = lzma->rep0;
len = lzma_len_decode(rc,
&lzma->match_len_choice,
&lzma->match_len_choice2,
lzma->match_len_low,
lzma->match_len_mid,
lzma->match_len_high,
pos_state);
dist_slot = rc_bittree(rc,
lzma->dist_slot[lzma_get_dist_state(len)] - 1,
DIST_SLOTS) - DIST_SLOTS;
if (dist_slot < DIST_MODEL_START) {
dist = dist_slot;
} else {
uint32_t limit = (dist_slot >> 1) - 1;
dist = (2 | (dist_slot & 1)) << limit;
if (dist_slot < DIST_MODEL_END) {
dist += rc_bittree_reverse(rc,
lzma->dist_special + dist - dist_slot - 1,
limit);
} else {
dist += rc_direct(rc, limit - ALIGN_BITS) << ALIGN_BITS;
dist += rc_bittree_reverse(rc, lzma->dist_align,
ALIGN_BITS);
}
}
lzma->rep0 = dist;
lzma->state = lzma_state_match(lzma->state);
if (dist >= dict->full || dist >= dict->size) {
s->compressed -= (uint32_t)(rc->in_pos - b->in_pos);
b->in_pos = rc->in_pos;
return 0;
}
if (!dict_repeat(dict, &len, dist)) {
s->match_len = len;
break;
}
}
}
s->compressed -= (uint32_t)(rc->in_pos - b->in_pos);
b->in_pos = rc->in_pos;
return 1;
}
/*
* Allocate an LZMA2 decoder.
*/
struct xz_dec_lzma2 *xz_dec_lzma2_create(enum xz_mode mode, uint32_t dict_max)
{
struct xz_dec_lzma2 *s;
s = (struct xz_dec_lzma2 *)malloc(sizeof(*s));
if (s == NULL)
return NULL;
memset(s, 0, sizeof(*s));
s->dict.mode = mode;
s->dict.size = dict_max;
if (mode == XZ_PREALLOC && dict_max > 0) {
s->dict.buf = (uint8_t *)malloc(dict_max);
if (s->dict.buf == NULL) {
free(s);
return NULL;
}
s->dict.allocated = dict_max;
}
return s;
}
/*
* Reset the LZMA2 decoder for a new block.
*/
enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2 *s, uint8_t props)
{
uint32_t dict_size;
if (props > 40)
return XZ_OPTIONS_ERROR;
if (props == 40) {
dict_size = 0xFFFFFFFF;
} else {
dict_size = 2 + (props & 1);
dict_size <<= props / 2 + 11;
}
if (s->dict.mode != XZ_SINGLE && s->dict.size > 0 && dict_size > s->dict.size)
return XZ_MEMLIMIT_ERROR;
if (s->dict.mode == XZ_DYNALLOC) {
if (s->dict.allocated < dict_size) {
free(s->dict.buf);
s->dict.buf = (uint8_t *)malloc(dict_size);
if (s->dict.buf == NULL) {
s->dict.allocated = 0;
return XZ_MEM_ERROR;
}
s->dict.allocated = dict_size;
}
}
if (s->dict.mode != XZ_SINGLE)
s->dict.end = dict_size;
s->sequence = SEQ_LZMA2_CONTROL;
s->need_dict_reset = 1;
s->need_lzma_init = 1;
s->need_props = 1;
s->match_len = 0;
return XZ_OK;
}
/*
* LZMA2 main decoding loop.
*/
enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2 *s, struct xz_buf *b)
{
for (;;) {
switch (s->sequence) {
case SEQ_LZMA2_CONTROL:
if (b->in_pos >= b->in_size)
return XZ_OK;
s->control = b->in[b->in_pos++];
if (s->control == 0x00)
return XZ_STREAM_END;
if (s->control < 0x03) {
/*
* Uncompressed chunk:
* 0x01 = no dictionary reset
* 0x02 = dictionary reset
*/
if (s->control == 0x02) {
s->need_dict_reset = 0;
s->need_lzma_init = 1;
dict_reset(&s->dict, b);
} else if (s->need_dict_reset) {
return XZ_DATA_ERROR;
}
s->sequence = SEQ_LZMA2_UNCOMPRESSED_1;
break;
}
/*
* LZMA chunk. The control byte encodes:
* 0x80-0x9F: no reset
* 0xA0-0xBF: state reset
* 0xC0-0xDF: state reset + new properties
* 0xE0-0xFF: full reset (state, props, dict)
*
* Bits 4..0 = uncompressed size high bits.
*/
if (s->control >= 0xE0) {
s->need_dict_reset = 0;
dict_reset(&s->dict, b);
} else if (s->need_dict_reset) {
return XZ_DATA_ERROR;
}
if (s->control >= 0xA0)
s->need_lzma_init = 1;
if (s->control >= 0xC0) {
s->need_props = 1;
}
s->uncompressed = (s->control & 0x1F) << 16;
s->sequence = SEQ_LZMA2_UNCOMPRESSED_1;
break;
case SEQ_LZMA2_UNCOMPRESSED_1:
if (b->in_pos >= b->in_size)
return XZ_OK;
s->uncompressed += (uint32_t)b->in[b->in_pos++] << 8;
s->sequence = SEQ_LZMA2_UNCOMPRESSED_2;
break;
case SEQ_LZMA2_UNCOMPRESSED_2:
if (b->in_pos >= b->in_size)
return XZ_OK;
s->uncompressed += (uint32_t)b->in[b->in_pos++] + 1;
if (s->control < 0x03) {
/* Uncompressed copy: "compressed" size == uncompressed size */
s->compressed = s->uncompressed;
s->sequence = SEQ_LZMA2_COPY;
break;
}
/* LZMA chunk: read compressed size */
s->sequence = SEQ_LZMA2_COMPRESSED_0;
break;
case SEQ_LZMA2_COMPRESSED_0:
if (b->in_pos >= b->in_size)
return XZ_OK;
s->compressed = (uint32_t)b->in[b->in_pos++] << 8;
s->sequence = SEQ_LZMA2_COMPRESSED_1;
break;
case SEQ_LZMA2_COMPRESSED_1:
if (b->in_pos >= b->in_size)
return XZ_OK;
s->compressed += (uint32_t)b->in[b->in_pos++] + 1;
if (s->need_props)
s->sequence = SEQ_LZMA2_PROPS;
else
s->sequence = SEQ_LZMA2_LZMA_PREPARE;
break;
case SEQ_LZMA2_PROPS:
if (b->in_pos >= b->in_size)
return XZ_OK;
{
uint8_t props_byte = b->in[b->in_pos++];
uint32_t lc, lp, pb;
--s->compressed;
if (props_byte >= 9 * 5 * 5)
return XZ_DATA_ERROR;
lc = props_byte % 9;
props_byte /= 9;
lp = props_byte % 5;
pb = props_byte / 5;
if (lc + lp > 4)
return XZ_DATA_ERROR;
s->lzma.lc = lc;
s->lzma.literal_pos_mask = (1U << lp) - 1;
s->lzma.pos_mask = (1U << pb) - 1;
s->need_props = 0;
}
s->sequence = SEQ_LZMA2_LZMA_PREPARE;
break;
case SEQ_LZMA2_LZMA_PREPARE:
if (s->need_lzma_init) {
lzma_reset(&s->lzma);
rc_reset(&s->lzma.rc);
s->need_lzma_init = 0;
}
if (!rc_read_init(&s->lzma.rc, b))
return XZ_OK;
s->compressed -= 5;
s->match_len = 0;
s->sequence = SEQ_LZMA2_LZMA_RUN;
break;
case SEQ_LZMA2_LZMA_RUN: {
size_t in_avail;
dict_limit(&s->dict, s->uncompressed);
in_avail = min_t(size_t, b->in_size - b->in_pos, s->compressed);
if (!lzma_decode_loop(s, b, in_avail))
return XZ_DATA_ERROR;
{
uint32_t produced = dict_flush(&s->dict, b);
s->uncompressed -= produced;
}
if (s->uncompressed == 0) {
/* Chunk complete. Compressed may still have trailing
* bytes from the range coder (up to 5). Skip them. */
s->sequence = SEQ_LZMA2_CONTROL;
break;
}
if (b->out_pos == b->out_size
|| (b->in_pos == b->in_size && s->compressed > 0))
return XZ_OK;
break;
}
case SEQ_LZMA2_COPY:
dict_limit(&s->dict, s->uncompressed);
dict_uncompressed(&s->dict, b, &s->compressed);
{
uint32_t produced = dict_flush(&s->dict, b);
s->uncompressed -= produced;
}
if (s->uncompressed == 0) {
s->sequence = SEQ_LZMA2_CONTROL;
break;
}
if (b->in_pos == b->in_size || b->out_pos == b->out_size)
return XZ_OK;
break;
default:
return XZ_DATA_ERROR;
}
}
}
void xz_dec_lzma2_end(struct xz_dec_lzma2 *s)
{
if (s != NULL) {
if (s->dict.mode != XZ_SINGLE)
free(s->dict.buf);
free(s);
}
}

652
third_party/xz-embedded/xz_dec_stream.c vendored Normal file
View File

@@ -0,0 +1,652 @@
/*
* XZ decompressor - Stream decoder
*
* Based on xz-embedded by Lasse Collin (public domain).
*
* This parses the XZ container format: stream header, block headers,
* index, and stream footer. Actual data decompression is delegated
* to the LZMA2 decoder.
*/
#include "xz_private.h"
/* Sizes of the stream header and footer fields */
#define STREAM_HEADER_SIZE 12
#define HEADER_MAGIC_SIZE 6
#define FOOTER_MAGIC_SIZE 2
/*
* Supported check types. We accept None, CRC32, CRC64, and SHA-256
* (though SHA-256 is not verified -- we just skip those bytes).
*/
#define CHECK_NONE 0x00
#define CHECK_CRC32 0x01
#define CHECK_CRC64 0x04
#define CHECK_SHA256 0x0A
/* Sizes of the integrity check fields */
static const uint8_t check_sizes[16] = {
0, 4, 4, 4,
8, 8, 8, 16,
16, 16, 32, 32,
32, 64, 64, 64
};
/* Maximum block header size (encoded in the first byte as (real_size / 4) - 1) */
#define BLOCK_HEADER_SIZE_MAX 1024
/*
* Stream decoder states. The decoder is a state machine driven
* by xz_dec_run(), consuming input and producing output as it
* transitions through these states.
*/
enum xz_dec_stream_state {
SEQ_STREAM_HEADER,
SEQ_BLOCK_START,
SEQ_BLOCK_HEADER,
SEQ_BLOCK_UNCOMPRESS,
SEQ_BLOCK_PADDING,
SEQ_BLOCK_CHECK,
SEQ_INDEX,
SEQ_INDEX_PADDING,
SEQ_INDEX_CRC32,
SEQ_STREAM_FOOTER
};
struct xz_dec {
/* Current sequence/state in the stream decoder */
enum xz_dec_stream_state sequence;
/* Position within the current sequence state */
uint32_t pos;
/* Variable-length integer accumulator for index parsing */
uint64_t vli;
uint32_t vli_count;
/* Allocated operating mode */
enum xz_mode mode;
/*
* True once the stream footer has been verified; used to
* accept stream padding between concatenated streams.
*/
int allow_buf_error;
/* CRC32 of stream flags for footer verification */
uint32_t crc32_context;
/* Temporary buffer for collecting small structures */
struct {
uint8_t buf[BLOCK_HEADER_SIZE_MAX];
size_t pos;
size_t size;
} temp;
/* Block state */
struct {
/* Uncompressed size from block header (or VLI_UNKNOWN) */
uint64_t compressed;
uint64_t uncompressed;
/* Running counts during decompression */
uint64_t count_compressed;
uint64_t count_uncompressed;
/* Size of the integrity check for this stream */
uint32_t check_size;
uint32_t check_type;
/* Hash of block sizes for index verification */
uint64_t hash_compressed;
uint64_t hash_uncompressed;
uint32_t hash_count;
} block;
/* Index state */
struct {
uint64_t compressed;
uint64_t uncompressed;
uint64_t size;
uint32_t count;
uint64_t hash_compressed;
uint64_t hash_uncompressed;
uint32_t hash_count;
} index;
/* Check value accumulation buffer */
struct {
uint8_t buf[64]; /* big enough for SHA-256 */
uint32_t pos;
} check;
/* Stream flags from the stream header */
uint32_t stream_flags;
/* LZMA2 decoder instance */
struct xz_dec_lzma2 *lzma2;
};
#define VLI_UNKNOWN UINT64_MAX
#define VLI_MAX (UINT64_MAX / 2)
#define VLI_BYTES_MAX 9
/*
* Fill the temporary buffer from the input. Returns true when
* the temp buffer is full (temp.pos == temp.size).
*/
static int fill_temp(struct xz_dec *s, struct xz_buf *b)
{
size_t copy_size = min_t(size_t,
s->temp.size - s->temp.pos,
b->in_size - b->in_pos);
memcpy(s->temp.buf + s->temp.pos, b->in + b->in_pos, copy_size);
b->in_pos += copy_size;
s->temp.pos += copy_size;
if (s->temp.pos == s->temp.size) {
s->temp.pos = 0;
return 1;
}
return 0;
}
/*
* Decode a variable-length integer (VLI). XZ VLIs use 7 bits per byte
* with the high bit as a continuation flag. Returns XZ_STREAM_END when
* the VLI is complete, XZ_OK when more bytes are needed.
*/
static enum xz_ret dec_vli(struct xz_dec *s, const uint8_t *in,
size_t *in_pos, size_t in_size)
{
uint8_t byte;
if (s->vli_count == 0) {
s->vli = 0;
s->vli_count = 1;
}
while (*in_pos < in_size) {
byte = in[*in_pos];
++(*in_pos);
s->vli |= (uint64_t)(byte & 0x7F) << ((s->vli_count - 1) * 7);
if ((byte & 0x80) == 0) {
/* Reject non-minimal encodings */
if (byte == 0 && s->vli_count > 1)
return XZ_DATA_ERROR;
s->vli_count = 0;
return XZ_STREAM_END;
}
if (s->vli_count >= VLI_BYTES_MAX)
return XZ_DATA_ERROR;
++s->vli_count;
}
return XZ_OK;
}
/*
* Decode the stream header. It is 12 bytes:
* 6 bytes magic, 2 bytes stream flags, 4 bytes CRC32 of flags.
*/
static enum xz_ret dec_stream_header(struct xz_dec *s)
{
uint32_t crc;
if (memcmp(s->temp.buf, XZ_HEADER_MAGIC, XZ_HEADER_MAGIC_SIZE) != 0)
return XZ_FORMAT_ERROR;
/* Stream flags: first byte must be 0, second byte holds check type */
if (s->temp.buf[6] != 0)
return XZ_OPTIONS_ERROR;
s->stream_flags = s->temp.buf[7];
s->block.check_type = s->stream_flags & 0x0F;
s->block.check_size = check_sizes[s->block.check_type];
/* Verify CRC32 of the two stream flag bytes */
crc = xz_crc32(s->temp.buf + 6, 2, 0);
if (crc != get_le32(s->temp.buf + 8))
return XZ_DATA_ERROR;
return XZ_OK;
}
/*
* Decode a block header. The first byte gives the header size
* (encoded as (real_size / 4) - 1). The header contains filter
* flags; we only support a single LZMA2 filter (ID 0x21).
*/
static enum xz_ret dec_block_header(struct xz_dec *s)
{
uint32_t crc;
size_t header_size;
uint8_t bflags;
size_t pos;
uint8_t filter_id;
uint8_t props_size;
uint8_t lzma2_props;
enum xz_ret ret;
header_size = ((uint32_t)s->temp.buf[0] + 1) * 4;
/* The CRC32 covers everything except the CRC32 field itself */
crc = xz_crc32(s->temp.buf, (size_t)(header_size - 4), 0);
if (crc != get_le32(s->temp.buf + header_size - 4))
return XZ_DATA_ERROR;
bflags = s->temp.buf[1];
/*
* Bits 0-1: number of filters minus 1 (we require exactly 1).
* Bit 6: compressed size present.
* Bit 7: uncompressed size present.
* Other bits must be 0.
*/
if ((bflags & 0x03) != 0)
return XZ_OPTIONS_ERROR; /* more than one filter */
if (bflags & 0x3C)
return XZ_OPTIONS_ERROR; /* reserved bits set */
pos = 2;
/* Compressed size (optional) */
if (bflags & 0x40) {
s->vli_count = 0;
ret = dec_vli(s, s->temp.buf, &pos, header_size);
if (ret != XZ_STREAM_END)
return ret == XZ_OK ? XZ_DATA_ERROR : ret;
s->block.compressed = s->vli;
} else {
s->block.compressed = VLI_UNKNOWN;
}
/* Uncompressed size (optional) */
if (bflags & 0x80) {
s->vli_count = 0;
ret = dec_vli(s, s->temp.buf, &pos, header_size);
if (ret != XZ_STREAM_END)
return ret == XZ_OK ? XZ_DATA_ERROR : ret;
s->block.uncompressed = s->vli;
} else {
s->block.uncompressed = VLI_UNKNOWN;
}
/* Filter flags */
if (pos >= header_size - 4)
return XZ_DATA_ERROR;
filter_id = s->temp.buf[pos++];
if (filter_id != 0x21)
return XZ_OPTIONS_ERROR; /* only LZMA2 supported */
if (pos >= header_size - 4)
return XZ_DATA_ERROR;
props_size = s->temp.buf[pos++];
if (props_size != 1)
return XZ_OPTIONS_ERROR;
if (pos >= header_size - 4)
return XZ_DATA_ERROR;
lzma2_props = s->temp.buf[pos++];
/* Remaining bytes before CRC must be zero (padding) */
while (pos < header_size - 4) {
if (s->temp.buf[pos++] != 0)
return XZ_OPTIONS_ERROR;
}
/* Reset LZMA2 decoder with the new properties */
ret = xz_dec_lzma2_reset(s->lzma2, lzma2_props);
if (ret != XZ_OK)
return ret;
/* Reset block counters */
s->block.count_compressed = 0;
s->block.count_uncompressed = 0;
return XZ_OK;
}
/*
* Validate the stream footer. The footer is 12 bytes:
* 4 bytes CRC32, 4 bytes backward size, 2 bytes stream flags, 2 bytes magic.
*/
static enum xz_ret dec_stream_footer(struct xz_dec *s)
{
uint32_t crc;
/* Check footer magic bytes */
if (s->temp.buf[10] != 'Y' || s->temp.buf[11] != 'Z')
return XZ_DATA_ERROR;
/* CRC32 covers backward size and stream flags (bytes 4..9) */
crc = xz_crc32(s->temp.buf + 4, 6, 0);
if (crc != get_le32(s->temp.buf))
return XZ_DATA_ERROR;
/*
* Stream flags in the footer must match the header.
* Byte 8 must be 0, byte 9 holds check type.
*/
if (s->temp.buf[8] != 0)
return XZ_OPTIONS_ERROR;
if (s->temp.buf[9] != (uint8_t)s->stream_flags)
return XZ_DATA_ERROR;
/*
* Backward size indicates the size of the Index field.
* We don't verify it here in this minimal implementation.
*/
return XZ_STREAM_END;
}
/*
* Main decoder loop.
*/
enum xz_ret xz_dec_run(struct xz_dec *s, struct xz_buf *b)
{
enum xz_ret ret;
size_t copy_size;
/* Avoid infinite loops: if we can't make progress, return XZ_BUF_ERROR */
size_t in_start;
size_t out_start;
for (;;) {
in_start = b->in_pos;
out_start = b->out_pos;
switch (s->sequence) {
case SEQ_STREAM_HEADER:
s->temp.size = STREAM_HEADER_SIZE;
if (!fill_temp(s, b))
return XZ_OK;
ret = dec_stream_header(s);
if (ret != XZ_OK)
return ret;
s->sequence = SEQ_BLOCK_START;
break;
case SEQ_BLOCK_START:
/*
* The first byte of a block header gives its size.
* A zero byte indicates the start of the Index.
*/
if (b->in_pos >= b->in_size)
return XZ_OK;
/* Peek at the first byte */
if (b->in[b->in_pos] == 0x00) {
/* Index indicator */
++b->in_pos;
s->vli_count = 0;
s->index.count = 0;
s->index.hash_compressed = 0;
s->index.hash_uncompressed = 0;
s->index.hash_count = 0;
s->index.size = 1; /* counting the 0x00 byte */
s->crc32_context = xz_crc32((const uint8_t *)"\0", 1, 0);
s->sequence = SEQ_INDEX;
break;
}
/* Read the full block header into temp buffer */
s->temp.size = ((uint32_t)b->in[b->in_pos] + 1) * 4;
s->temp.pos = 0;
s->sequence = SEQ_BLOCK_HEADER;
break;
case SEQ_BLOCK_HEADER:
if (!fill_temp(s, b))
return XZ_OK;
ret = dec_block_header(s);
if (ret != XZ_OK)
return ret;
s->sequence = SEQ_BLOCK_UNCOMPRESS;
break;
case SEQ_BLOCK_UNCOMPRESS: {
size_t in_before = b->in_pos;
size_t out_before = b->out_pos;
ret = xz_dec_lzma2_run(s->lzma2, b);
s->block.count_compressed += b->in_pos - in_before;
s->block.count_uncompressed += b->out_pos - out_before;
if (ret == XZ_STREAM_END) {
/* Verify sizes if they were specified in the block header */
if (s->block.compressed != VLI_UNKNOWN &&
s->block.count_compressed != s->block.compressed)
return XZ_DATA_ERROR;
if (s->block.uncompressed != VLI_UNKNOWN &&
s->block.count_uncompressed != s->block.uncompressed)
return XZ_DATA_ERROR;
/* Accumulate for index verification */
s->block.hash_compressed += s->block.count_compressed;
s->block.hash_uncompressed += s->block.count_uncompressed;
++s->block.hash_count;
s->pos = 0;
s->sequence = SEQ_BLOCK_PADDING;
break;
}
if (ret != XZ_OK)
return ret;
/* Check for limit violations */
if (s->block.compressed != VLI_UNKNOWN &&
s->block.count_compressed > s->block.compressed)
return XZ_DATA_ERROR;
if (s->block.uncompressed != VLI_UNKNOWN &&
s->block.count_uncompressed > s->block.uncompressed)
return XZ_DATA_ERROR;
return XZ_OK;
}
case SEQ_BLOCK_PADDING:
/*
* Compressed data is padded to a multiple of 4 bytes.
* The padding bytes must be zero.
*/
while ((s->block.count_compressed + s->pos) & 3) {
if (b->in_pos >= b->in_size)
return XZ_OK;
if (b->in[b->in_pos++] != 0)
return XZ_DATA_ERROR;
++s->pos;
}
s->check.pos = 0;
s->sequence = SEQ_BLOCK_CHECK;
break;
case SEQ_BLOCK_CHECK:
/*
* We consume the check value but don't verify it
* (a full implementation would verify CRC32/CRC64/SHA-256).
* For CRC32 and CRC64 we could verify, but for simplicity
* and size we skip it. The stream/index CRC32s are verified.
*/
copy_size = min_t(size_t,
s->block.check_size - s->check.pos,
b->in_size - b->in_pos);
b->in_pos += copy_size;
s->check.pos += (uint32_t)copy_size;
if (s->check.pos < s->block.check_size)
return XZ_OK;
s->sequence = SEQ_BLOCK_START;
break;
case SEQ_INDEX: {
/*
* Parse the Index. Format:
* - Number of Records (VLI)
* - For each record: Unpadded Size (VLI), Uncompressed Size (VLI)
* - Padding to 4-byte boundary
* - CRC32 of everything from the Index Indicator to padding
*
* We do a simplified parse: consume VLIs for count then
* skip through the records.
*/
/* First VLI is the number of records */
if (s->index.count == 0 && s->vli_count == 0) {
/* Decode number-of-records VLI */
ret = dec_vli(s, b->in, &b->in_pos, b->in_size);
if (ret == XZ_OK)
return XZ_OK;
if (ret != XZ_STREAM_END)
return ret;
s->index.count = (uint32_t)s->vli;
s->index.hash_count = 0;
s->pos = 0; /* sub-state: 0 = compressed VLI, 1 = uncompressed VLI */
}
/* Consume records */
while (s->index.hash_count < s->index.count) {
s->vli_count = 0;
ret = dec_vli(s, b->in, &b->in_pos, b->in_size);
if (ret == XZ_OK)
return XZ_OK;
if (ret != XZ_STREAM_END)
return ret;
if (s->pos == 0) {
s->pos = 1;
} else {
s->pos = 0;
++s->index.hash_count;
}
}
s->sequence = SEQ_INDEX_PADDING;
s->pos = 0;
break;
}
case SEQ_INDEX_PADDING:
/*
* Skip padding. We need to figure out how many bytes the
* index consumed; for simplicity we just skip 0-3 zero bytes.
*/
while (b->in_pos < b->in_size && s->pos < 3) {
if (b->in[b->in_pos] != 0)
break;
++b->in_pos;
++s->pos;
}
/* Now read the 4-byte CRC32 */
s->temp.size = 4;
s->temp.pos = 0;
s->sequence = SEQ_INDEX_CRC32;
break;
case SEQ_INDEX_CRC32:
if (!fill_temp(s, b))
return XZ_OK;
/*
* In a full implementation we'd verify the CRC32 of the
* entire index. We skip that for size, but we do consume it.
*/
s->temp.size = 12; /* stream footer size */
s->temp.pos = 0;
s->sequence = SEQ_STREAM_FOOTER;
break;
case SEQ_STREAM_FOOTER:
if (!fill_temp(s, b))
return XZ_OK;
return dec_stream_footer(s);
default:
return XZ_DATA_ERROR;
}
/* Detect lack of progress */
if (b->in_pos == in_start && b->out_pos == out_start) {
/*
* If we switched states without consuming anything,
* that's fine -- we'll try again. But if we've been
* through the loop already without progress, avoid
* spinning. The state change itself counts as progress
* the first time through.
*/
if (s->allow_buf_error)
return XZ_BUF_ERROR;
s->allow_buf_error = 1;
} else {
s->allow_buf_error = 0;
}
}
}
struct xz_dec *xz_dec_init(enum xz_mode mode, uint32_t dict_max)
{
struct xz_dec *s;
s = (struct xz_dec *)malloc(sizeof(*s));
if (s == NULL)
return NULL;
memset(s, 0, sizeof(*s));
s->mode = mode;
s->lzma2 = xz_dec_lzma2_create(mode, dict_max);
if (s->lzma2 == NULL) {
free(s);
return NULL;
}
xz_dec_reset(s);
return s;
}
void xz_dec_reset(struct xz_dec *s)
{
s->sequence = SEQ_STREAM_HEADER;
s->allow_buf_error = 0;
s->pos = 0;
s->vli_count = 0;
memset(&s->block, 0, sizeof(s->block));
memset(&s->index, 0, sizeof(s->index));
memset(&s->check, 0, sizeof(s->check));
memset(&s->temp, 0, sizeof(s->temp));
s->stream_flags = 0;
s->crc32_context = 0;
}
void xz_dec_end(struct xz_dec *s)
{
if (s != NULL) {
xz_dec_lzma2_end(s->lzma2);
free(s);
}
}

116
third_party/xz-embedded/xz_lzma2.h vendored Normal file
View File

@@ -0,0 +1,116 @@
/*
* LZMA2 decoder - Internal constants and structures
*
* Based on xz-embedded by Lasse Collin (public domain).
*/
#ifndef XZ_LZMA2_H
#define XZ_LZMA2_H
#include "xz_private.h"
/* Range coder constants */
#define RC_SHIFT_BITS 8
#define RC_TOP_BITS 24
#define RC_TOP_VALUE (1U << RC_TOP_BITS)
#define RC_BIT_MODEL_TOTAL_BITS 11
#define RC_BIT_MODEL_TOTAL (1 << RC_BIT_MODEL_TOTAL_BITS)
#define RC_MOVE_BITS 5
/*
* Maximum number of position bits (lp + pb combined).
* LZMA uses up to 4 position bits.
*/
#define POS_STATES_MAX (1 << 4)
/*
* The LZMA state machine has 12 states. States 0-6 indicate that the
* previous output was a literal; states 7-11 indicate a match/rep.
*/
#define STATES 12
/* Special state values */
#define LIT_STATES 7
/* Match length constants */
#define MATCH_LEN_MIN 2
#define LEN_LOW_BITS 3
#define LEN_LOW_SYMBOLS (1 << LEN_LOW_BITS)
#define LEN_MID_BITS 3
#define LEN_MID_SYMBOLS (1 << LEN_MID_BITS)
#define LEN_HIGH_BITS 8
#define LEN_HIGH_SYMBOLS (1 << LEN_HIGH_BITS)
/* Total number of match length probabilities */
#define LEN_SYMBOLS (LEN_LOW_SYMBOLS + LEN_MID_SYMBOLS + LEN_HIGH_SYMBOLS)
/* Distance slots */
#define DIST_STATES 4
#define DIST_SLOT_BITS 6
#define DIST_SLOTS (1 << DIST_SLOT_BITS)
#define DIST_MODEL_START 4
#define DIST_MODEL_END 14
#define FULL_DISTANCES (1 << (DIST_MODEL_END / 2))
/* Alignment bits for distance decoding */
#define ALIGN_BITS 4
#define ALIGN_SIZE (1 << ALIGN_BITS)
#define ALIGN_MASK (ALIGN_SIZE - 1)
/* Total number of LZMA probability variables */
#define PROBS_SIZE_LIT(lc) (3U << (lc))
/* Literal coder */
#define LITERAL_CODERS_MAX (1 << 4) /* max lc = 4 */
/* LZMA2 control byte values */
#define LZMA2_CONTROL_LZMA 0x80
#define LZMA2_CONTROL_COPY_NO_RESET 0x01
#define LZMA2_CONTROL_COPY_RESET 0x02
/*
* lzma_state_is_literal: Returns true if the given LZMA state indicates
* that the previous symbol was a literal.
*/
static inline int lzma_state_is_literal(uint32_t state)
{
return state < LIT_STATES;
}
/*
* State transitions after different symbol types.
*/
static inline uint32_t lzma_state_literal(uint32_t state)
{
if (state <= 3)
return 0;
if (state <= 9)
return state - 3;
return state - 6;
}
static inline uint32_t lzma_state_match(uint32_t state)
{
return state < LIT_STATES ? 7 : 10;
}
static inline uint32_t lzma_state_long_rep(uint32_t state)
{
return state < LIT_STATES ? 8 : 11;
}
static inline uint32_t lzma_state_short_rep(uint32_t state)
{
return state < LIT_STATES ? 9 : 11;
}
/*
* Get the distance state (0..3) from the match length.
*/
static inline uint32_t lzma_get_dist_state(uint32_t len)
{
return len < DIST_STATES + MATCH_LEN_MIN
? len - MATCH_LEN_MIN : DIST_STATES - 1;
}
#endif /* XZ_LZMA2_H */

55
third_party/xz-embedded/xz_private.h vendored Normal file
View File

@@ -0,0 +1,55 @@
/*
* XZ decompressor - Private/internal header
*
* Based on xz-embedded by Lasse Collin (public domain).
*/
#ifndef XZ_PRIVATE_H
#define XZ_PRIVATE_H
#include "xz.h"
#include <stdlib.h>
#include <string.h>
/* Six-byte magic at the start of every .xz stream: 0xFD, '7', 'z', 'X', 'Z', 0x00 */
#define XZ_HEADER_MAGIC "\3757zXZ\0"
#define XZ_HEADER_MAGIC_SIZE 6
/* Two-byte magic in stream footer: 'Y', 'Z' */
#define XZ_FOOTER_MAGIC "YZ"
#define XZ_FOOTER_MAGIC_SIZE 2
#ifndef min_t
#define min_t(type, a, b) ((type)(a) < (type)(b) ? (type)(a) : (type)(b))
#endif
#ifndef max_t
#define max_t(type, a, b) ((type)(a) > (type)(b) ? (type)(a) : (type)(b))
#endif
/* Get an unaligned 32-bit little-endian value */
static inline uint32_t get_le32(const uint8_t *buf)
{
return (uint32_t)buf[0]
| ((uint32_t)buf[1] << 8)
| ((uint32_t)buf[2] << 16)
| ((uint32_t)buf[3] << 24);
}
/* CRC lookup tables */
extern uint32_t xz_crc32_table[256];
extern uint64_t xz_crc64_table[256];
/* CRC functions */
uint32_t xz_crc32(const uint8_t *buf, size_t size, uint32_t crc);
uint64_t xz_crc64(const uint8_t *buf, size_t size, uint64_t crc);
/* LZMA2 decoder */
struct xz_dec_lzma2;
struct xz_dec_lzma2 *xz_dec_lzma2_create(enum xz_mode mode, uint32_t dict_max);
enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2 *s, struct xz_buf *b);
enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2 *s, uint8_t props);
void xz_dec_lzma2_end(struct xz_dec_lzma2 *s);
#endif /* XZ_PRIVATE_H */

33
tools/generate_lic.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
Generate the .key unlock file for Setec Partition Wizard.
Usage:
python generate_lic.py
Then use the resulting unlock.key in Tools > Unlock Features
"""
import base64
import hashlib
import os
UNLOCK_TEXT = "Snake Says Unlock!"
EXPECTED_HASH = "f2cd6920ba4b09c79c105810f9eff9d73beb1f689b8f67099c1a39e5634059c5"
script_dir = os.path.dirname(os.path.abspath(__file__))
key_path = os.path.join(script_dir, "unlock.key")
encoded = base64.b64encode(UNLOCK_TEXT.encode("utf-8"))
sha = hashlib.sha256(encoded + b"\n").hexdigest()
print(f"SHA-256: {sha}")
print(f"Expected: {EXPECTED_HASH}")
if sha == EXPECTED_HASH:
print("Hash matches!")
else:
print("WARNING: Hash mismatch.")
with open(key_path, "wb") as f:
f.write(encoded + b"\n")
print(f"Written: {key_path} ({len(encoded) + 1} bytes)")

1
tools/unlock.key Normal file
View File

@@ -0,0 +1 @@
U25ha2UgU2F5cyBVbmxvY2tcIQ==