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

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

View File

@@ -5,6 +5,11 @@
#include "tabs/DiagnosticsTab.h"
#include "tabs/SecurityTab.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/disk/DiskEnumerator.h"
@@ -13,7 +18,9 @@
#include <QAction>
#include <QApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QIcon>
#include <QKeyEvent>
@@ -165,6 +172,8 @@ void MainWindow::setupMenuBar()
toolsMenu->addAction(tr("S&urface Scan..."));
toolsMenu->addSeparator();
toolsMenu->addAction(tr("&Boot Repair..."));
toolsMenu->addSeparator();
toolsMenu->addAction(tr("&Unlock Features..."), this, &MainWindow::onUnlockFeatures);
auto* helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(tr("&About..."), this, &MainWindow::onAbout);
@@ -215,6 +224,11 @@ void MainWindow::setupTabs()
m_diagnosticsTab = new DiagnosticsTab(this);
m_securityTab = new SecurityTab(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_recoveryTab, tr("Recovery"));
@@ -222,6 +236,11 @@ void MainWindow::setupTabs()
m_tabWidget->addTab(m_diagnosticsTab, tr("Diagnostics"));
m_tabWidget->addTab(m_securityTab, tr("Security Keys"));
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);
}
@@ -246,6 +265,56 @@ void MainWindow::connectTabSignals()
this, &MainWindow::onStatusMessage);
connect(m_maintenanceTab, &MaintenanceTab::statusMessage,
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()
@@ -282,6 +351,11 @@ void MainWindow::onRefreshDisks()
m_diagnosticsTab->refreshDisks(m_lastSnapshot);
m_securityTab->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(
tr("Found %1 disk(s), %2 partition(s), %3 volume(s)")

View File

@@ -18,6 +18,11 @@ class ImagingTab;
class DiagnosticsTab;
class SecurityTab;
class MaintenanceTab;
class SdCardTab;
class VirtualDiskTab;
class NonWindowsFsTab;
class LinuxFlasherTab;
class KaliCreatorTab;
class MainWindow : public QMainWindow
{
@@ -43,6 +48,7 @@ private:
private slots:
void onAbout();
void onRefreshDisks();
void onUnlockFeatures();
void onStatusMessage(const QString& msg);
private:
@@ -56,6 +62,11 @@ private:
DiagnosticsTab* m_diagnosticsTab = nullptr;
SecurityTab* m_securityTab = 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)
QWidget* m_hwdiagPanel = nullptr;

View File

@@ -86,7 +86,7 @@ void DiskPartitionTab::setupUi()
m_diskTree->setModel(m_diskTreeModel);
m_diskTree->setHeaderHidden(false);
m_diskTree->setAlternatingRowColors(true);
m_diskTree->setMinimumWidth(280);
m_diskTree->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_diskTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_diskTree->setSelectionMode(QAbstractItemView::SingleSelection);
leftLayout->addWidget(m_diskTree);
@@ -142,7 +142,7 @@ void DiskPartitionTab::setupUi()
opLayout->addWidget(opLabel);
m_operationListWidget = new QListWidget();
m_operationListWidget->setMinimumWidth(220);
m_operationListWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
opLayout->addWidget(m_operationListWidget);
auto* buttonLayout = new QHBoxLayout();
@@ -456,8 +456,56 @@ void DiskPartitionTab::onCreatePartition()
sizeGbSpin->setValue(1.0);
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();
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);
auto* labelEdit = new QLineEdit();
@@ -476,9 +524,8 @@ void DiskPartitionTab::onCreatePartition()
uint32_t sectorSize = diskInfo->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;
// Simple: use offset after last partition
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId)
@@ -496,16 +543,9 @@ void DiskPartitionTab::onCreatePartition()
params.sectorSize = sectorSize;
params.formatAfter = true;
// Map filesystem selection
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2
};
int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes)))
{
params.formatOptions.targetFs = fsTypes[fsIdx];
}
if (fsIdx >= 0 && fsIdx < kCreateFsCount)
params.formatOptions.targetFs = kCreateFsEntries[fsIdx].type;
params.formatOptions.volumeLabel = labelEdit->text().toStdString();
params.formatOptions.quickFormat = true;
@@ -618,8 +658,52 @@ void DiskPartitionTab::onFormatPartition()
dlg.setWindowTitle(tr("Format Partition"));
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();
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);
auto* labelEdit = new QLineEdit();
@@ -637,12 +721,6 @@ void DiskPartitionTab::onFormatPartition()
if (dlg.exec() != QDialog::Accepted)
return;
static const FilesystemType fsTypes[] = {
FilesystemType::NTFS, FilesystemType::FAT32, FilesystemType::ExFAT,
FilesystemType::Ext4, FilesystemType::Ext3, FilesystemType::Ext2,
FilesystemType::SWAP_LINUX
};
FormatPartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
@@ -659,8 +737,8 @@ void DiskPartitionTab::onFormatPartition()
}
int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes)))
params.options.targetFs = fsTypes[fsIdx];
if (fsIdx >= 0 && fsIdx < kFmtFsCount)
params.options.targetFs = kFmtFsEntries[fsIdx].type;
params.options.volumeLabel = labelEdit->text().toStdString();
params.options.quickFormat = quickCheck->isChecked();

View File

@@ -6,16 +6,24 @@
#include "core/imaging/ImageRestorer.h"
#include "core/imaging/IsoFlasher.h"
#ifdef _WIN32
#include <windows.h>
#include <winioctl.h>
#endif
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProcess>
#include <QProgressBar>
#include <QPushButton>
#include <QTabWidget>
#include <QThread>
#include <QVBoxLayout>
@@ -34,7 +42,12 @@ void ImagingTab::setupUi()
{
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* cloneLayout = new QGridLayout(cloneGroup);
@@ -67,9 +80,14 @@ void ImagingTab::setupUi()
connect(m_cloneBtn, &QPushButton::clicked, this, &ImagingTab::onCloneDisk);
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* imageLayout = new QGridLayout(imageGroup);
@@ -101,9 +119,14 @@ void ImagingTab::setupUi()
connect(m_imageCreateBtn, &QPushButton::clicked, this, &ImagingTab::onCreateImage);
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* restoreLayout = new QGridLayout(restoreGroup);
@@ -140,9 +163,14 @@ void ImagingTab::setupUi()
connect(m_restoreBtn, &QPushButton::clicked, this, &ImagingTab::onRestoreImage);
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* flashLayout = new QGridLayout(flashGroup);
@@ -173,9 +201,155 @@ void ImagingTab::setupUi()
connect(m_flashBtn, &QPushButton::clicked, this, &ImagingTab::onFlashIso);
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)
@@ -402,6 +576,13 @@ void ImagingTab::onFlashIso()
config.targetDiskId = targetDiskId;
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->setValue(0);
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);
}
// ============================================================================
// 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

View File

@@ -39,6 +39,12 @@ private slots:
void onBrowseRestoreInput();
void onBrowseFlashInput();
void onRestoreInputChanged();
void onOpticalRipDisc();
void onOpticalBurnImage();
void onOpticalErase();
void onOpticalBrowseBurnInput();
void onOpticalBrowseRipOutput();
void onOpticalRefreshDrives();
private:
void setupUi();
@@ -80,6 +86,30 @@ private:
QProgressBar* m_flashProgress = 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
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 <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
@@ -15,6 +17,7 @@
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProcess>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
@@ -36,7 +39,12 @@ void MaintenanceTab::setupUi()
{
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* eraseLayout = new QGridLayout(eraseGroup);
@@ -79,7 +87,6 @@ void MaintenanceTab::setupUi()
// BIG RED erase button
m_eraseBtn = new QPushButton(tr("SECURE ERASE"));
m_eraseBtn->setObjectName("cancelButton");
m_eraseBtn->setMinimumHeight(50);
m_eraseBtn->setStyleSheet(
"QPushButton { background-color: #cc0000; color: white; font-size: 16px; "
"font-weight: bold; border: 2px solid #880000; border-radius: 6px; }"
@@ -89,9 +96,14 @@ void MaintenanceTab::setupUi()
connect(m_eraseBtn, &QPushButton::clicked, this, &MaintenanceTab::onSecureErase);
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* bootLayout = new QVBoxLayout(bootGroup);
@@ -137,9 +149,91 @@ void MaintenanceTab::setupUi()
m_bootStatusLabel->setWordWrap(true);
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* sdLayout = new QGridLayout(sdGroup);
@@ -173,7 +267,6 @@ void MaintenanceTab::setupUi()
sdLayout->addWidget(m_sdLabelEdit, 3, 1, 1, 2);
m_sdFixBtn = new QPushButton(tr("Fix SD Card"));
m_sdFixBtn->setMinimumHeight(40);
m_sdFixBtn->setStyleSheet(
"QPushButton { background-color: #d4a574; color: #1e1e2e; font-size: 14px; "
"font-weight: bold; border: 2px solid #b08050; border-radius: 6px; }"
@@ -191,8 +284,11 @@ void MaintenanceTab::setupUi()
m_sdStatusLabel->setWordWrap(true);
sdLayout->addWidget(m_sdStatusLabel, 6, 0, 1, 3);
layout->addWidget(sdGroup);
layout->addStretch();
sdOuterLayout->addWidget(sdGroup);
sdOuterLayout->addStretch();
innerTabs->addTab(sdWidget, tr("SD Card Recovery"));
layout->addWidget(innerTabs);
}
void MaintenanceTab::refreshDisks(const SystemDiskSnapshot& snapshot)
@@ -205,6 +301,7 @@ void MaintenanceTab::populateDiskCombo()
{
m_eraseDiskCombo->clear();
m_bootDiskCombo->clear();
m_blDiskCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
@@ -214,6 +311,7 @@ void MaintenanceTab::populateDiskCombo()
.arg(formatSize(disk.sizeBytes));
m_eraseDiskCombo->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();
}
// ============================================================================
// 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)
{
if (bytes >= 1099511627776ULL)

View File

@@ -43,6 +43,10 @@ private slots:
void onReinstallBootloader();
void onSdScan();
void onSdFix();
void onInstallGrub2();
void onInstallWindowsBM();
void onInstallSyslinux();
void onInstallRefind();
private:
void setupUi();
@@ -68,6 +72,16 @@ private:
QProgressBar* m_bootProgress = 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
QComboBox* m_sdCardCombo = 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