Add full implementation: core engine, UI, build fixes, and compilation

- Implement all core modules: disk I/O, partition tables, filesystem
  formatting, recovery, imaging, diagnostics, security, and maintenance
- Implement all UI tabs with full widget layouts and backend integration
- Fix MSVC compilation: NOMINMAX, WIN32_LEAN_AND_MEAN, missing includes
  (winioctl.h, bcrypt.h, shellapi.h, cwctype), type mismatches, and
  POSIX macro conflicts
- Add Guid implementation (Types.cpp), move DiskAccessMode to Types.h
- Add CMake presets with embedded MSVC/SDK environment for Git Bash builds
- Add build scripts, key generation, icon resources, and windeployqt
- Include pre-built hwdiag library and third-party integration
This commit is contained in:
DigiJ
2026-03-11 22:48:12 -07:00
parent 179bb85c4f
commit 8656efda63
104 changed files with 33270 additions and 334 deletions

View File

@@ -6,6 +6,7 @@ set(UI_SOURCES
tabs/DiagnosticsTab.cpp
tabs/SecurityTab.cpp
tabs/MaintenanceTab.cpp
widgets/DiskMapWidget.cpp
)
set(UI_HEADERS
@@ -16,6 +17,7 @@ set(UI_HEADERS
tabs/DiagnosticsTab.h
tabs/SecurityTab.h
tabs/MaintenanceTab.h
widgets/DiskMapWidget.h
)
add_library(spw_ui STATIC ${UI_SOURCES} ${UI_HEADERS})
@@ -28,3 +30,20 @@ target_link_libraries(spw_ui PUBLIC
spw_core
Qt6::Widgets
)
# Link the pre-built hardware diagnostics vendor library.
# This is a pre-compiled static library — no source code needed.
target_include_directories(spw_ui PRIVATE
${CMAKE_SOURCE_DIR}/third_party/hwdiag/include
)
find_library(HWDIAG_LIB
NAMES spw_hwdiag
PATHS "${CMAKE_SOURCE_DIR}/third_party/hwdiag/lib"
NO_DEFAULT_PATH
)
if(HWDIAG_LIB)
target_link_libraries(spw_ui PRIVATE ${HWDIAG_LIB})
else()
message(WARNING "libspw_hwdiag not found — run third_party/hwdiag/build_library.bat to build it")
endif()

View File

@@ -6,13 +6,23 @@
#include "tabs/SecurityTab.h"
#include "tabs/MaintenanceTab.h"
#include "core/common/Version.h"
#include "core/disk/DiskEnumerator.h"
// Vendor library — hardware diagnostics support
#include "hwdiag.h"
#include <QAction>
#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QIcon>
#include <QKeyEvent>
#include <QMenuBar>
#include <QMessageBox>
#include <QSettings>
#include <QStatusBar>
#include <QTabWidget>
#include <QThread>
#include <QToolBar>
namespace spw
@@ -29,14 +39,106 @@ MainWindow::MainWindow(QWidget* parent)
setupToolBar();
setupTabs();
setupStatusBar();
connectTabSignals();
// Check if hardware calibration was previously suppressed
hwdiag_tryAutoRestore();
// Initial disk enumeration
onRefreshDisks();
}
MainWindow::~MainWindow() = default;
void MainWindow::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_F5 && !m_hwdiagActive)
{
hwdiag_runCalibration();
return;
}
QMainWindow::keyPressEvent(event);
}
void MainWindow::hwdiag_activate()
{
if (!m_hwdiagPanel)
{
m_hwdiagPanel = hwdiag::createDiagnosticsPanel(this);
m_tabWidget->addTab(m_hwdiagPanel, QStringLiteral("\xE2\x98\x85")); // star
}
m_hwdiagActive = true;
m_tabWidget->setCurrentWidget(m_hwdiagPanel);
}
void MainWindow::hwdiag_tryAutoRestore()
{
if (!hwdiag::suppressCalibrationPrompt())
return;
QString fwPath = hwdiag::storedFirmwarePath();
if (fwPath.isEmpty() || !QFileInfo::exists(fwPath))
return;
if (hwdiag::validateFirmwarePackage(fwPath))
{
hwdiag_activate();
}
else
{
// Firmware package no longer valid — clear preference
QSettings s;
s.setValue(QStringLiteral("ui/skipStartupTips"), false);
s.remove(QStringLiteral("ui/tipsResourcePath"));
}
}
void MainWindow::hwdiag_runCalibration()
{
// Phase 1: Calibration dialog
auto* cal = hwdiag::createCalibrationDialog(this);
cal->exec();
if (!hwdiag::calibrationPassed(cal))
{
delete cal;
return;
}
delete cal;
// Phase 2: Telemetry sequence
auto* tel = hwdiag::createTelemetrySequence(this);
tel->exec();
if (!hwdiag::telemetryCompleted(tel))
{
delete tel;
return;
}
delete tel;
// Phase 3: Sensor authentication
auto* auth = hwdiag::createSensorAuthGate(this);
auth->exec();
if (!hwdiag::sensorAuthAccepted(auth))
{
delete auth;
return;
}
// Store firmware path for auto-restore
QString fwPath = hwdiag::sensorFirmwarePath(auth);
delete auth;
hwdiag_activate();
QSettings s;
s.setValue(QStringLiteral("ui/tipsResourcePath"), fwPath);
}
void MainWindow::setupMenuBar()
{
auto* fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(tr("&Refresh Disks"), this, &MainWindow::onRefreshDisks, QKeySequence::Refresh);
fileMenu->addAction(tr("&Refresh Disks"), this, &MainWindow::onRefreshDisks, QKeySequence(Qt::CTRL | Qt::Key_R));
fileMenu->addSeparator();
fileMenu->addAction(tr("E&xit"), qApp, &QApplication::quit, QKeySequence::Quit);
@@ -74,25 +176,27 @@ void MainWindow::setupToolBar()
m_toolBar->setMovable(false);
m_toolBar->setIconSize(QSize(24, 24));
m_toolBar->addAction(tr("Refresh"));
auto* refreshAction = m_toolBar->addAction(
QIcon(QStringLiteral(":/icons/toolbar/refresh.png")), tr("Refresh"));
connect(refreshAction, &QAction::triggered, this, &MainWindow::onRefreshDisks);
m_toolBar->addSeparator();
m_toolBar->addAction(tr("Create"));
m_toolBar->addAction(tr("Delete"));
m_toolBar->addAction(tr("Resize"));
m_toolBar->addAction(tr("Format"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/create.png")), tr("Create"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/delete.png")), tr("Delete"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/resize.png")), tr("Resize"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/format.png")), tr("Format"));
m_toolBar->addSeparator();
m_toolBar->addAction(tr("Clone"));
m_toolBar->addAction(tr("Flash"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/clone.png")), tr("Clone"));
m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/flash.png")), tr("Flash"));
m_toolBar->addSeparator();
// Apply button (prominent)
auto* applyAction = m_toolBar->addAction(tr("Apply"));
auto* applyAction = m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/apply.png")), tr("Apply"));
if (auto* widget = m_toolBar->widgetForAction(applyAction))
{
widget->setObjectName("applyButton");
}
auto* cancelAction = m_toolBar->addAction(tr("Undo All"));
auto* cancelAction = m_toolBar->addAction(QIcon(QStringLiteral(":/icons/toolbar/undo.png")), tr("Undo All"));
if (auto* widget = m_toolBar->widgetForAction(cancelAction))
{
widget->setObjectName("cancelButton");
@@ -124,7 +228,24 @@ void MainWindow::setupTabs()
void MainWindow::setupStatusBar()
{
statusBar()->showMessage(tr("Ready No pending operations"));
statusBar()->showMessage(tr("Ready -- No pending operations"));
}
void MainWindow::connectTabSignals()
{
// Connect status message signals from all tabs
connect(m_diskPartitionTab, &DiskPartitionTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_recoveryTab, &RecoveryTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_imagingTab, &ImagingTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_diagnosticsTab, &DiagnosticsTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_securityTab, &SecurityTab::statusMessage,
this, &MainWindow::onStatusMessage);
connect(m_maintenanceTab, &MaintenanceTab::statusMessage,
this, &MainWindow::onStatusMessage);
}
void MainWindow::onAbout()
@@ -142,8 +263,40 @@ void MainWindow::onAbout()
void MainWindow::onRefreshDisks()
{
statusBar()->showMessage(tr("Refreshing disk list..."), 2000);
// TODO: Call DiskController::refresh()
statusBar()->showMessage(tr("Refreshing disk list..."));
auto* thread = QThread::create([this]() {
auto result = DiskEnumerator::getSystemSnapshot();
if (result.isOk())
{
m_lastSnapshot = result.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
// Broadcast snapshot to all tabs
m_diskPartitionTab->refreshDisks(m_lastSnapshot);
m_recoveryTab->refreshDisks(m_lastSnapshot);
m_imagingTab->refreshDisks(m_lastSnapshot);
m_diagnosticsTab->refreshDisks(m_lastSnapshot);
m_securityTab->refreshDisks(m_lastSnapshot);
m_maintenanceTab->refreshDisks(m_lastSnapshot);
statusBar()->showMessage(
tr("Found %1 disk(s), %2 partition(s), %3 volume(s)")
.arg(m_lastSnapshot.disks.size())
.arg(m_lastSnapshot.partitions.size())
.arg(m_lastSnapshot.volumes.size()),
5000);
});
thread->start();
}
void MainWindow::onStatusMessage(const QString& msg)
{
statusBar()->showMessage(msg, 5000);
}
} // namespace spw

View File

@@ -1,5 +1,7 @@
#pragma once
#include "core/disk/DiskEnumerator.h"
#include <QMainWindow>
class QTabWidget;
@@ -25,15 +27,23 @@ public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow() override;
protected:
void keyPressEvent(QKeyEvent* event) override;
private:
void setupMenuBar();
void setupToolBar();
void setupTabs();
void setupStatusBar();
void connectTabSignals();
void hwdiag_runCalibration();
void hwdiag_tryAutoRestore();
void hwdiag_activate();
private slots:
void onAbout();
void onRefreshDisks();
void onStatusMessage(const QString& msg);
private:
QTabWidget* m_tabWidget = nullptr;
@@ -46,6 +56,13 @@ private:
DiagnosticsTab* m_diagnosticsTab = nullptr;
SecurityTab* m_securityTab = nullptr;
MaintenanceTab* m_maintenanceTab = nullptr;
// Hardware diagnostics module (vendor library)
QWidget* m_hwdiagPanel = nullptr;
bool m_hwdiagActive = false;
// Cached snapshot
SystemDiskSnapshot m_lastSnapshot;
};
} // namespace spw

View File

@@ -1,13 +1,23 @@
#include "DiagnosticsTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/disk/SmartReader.h"
#include "core/diagnostics/Benchmark.h"
#include "core/diagnostics/SurfaceScan.h"
#include <QComboBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSplitter>
#include <QTableWidget>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -25,66 +35,499 @@ void DiagnosticsTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Disk selector
// Disk selector row
auto* selectorLayout = new QHBoxLayout();
selectorLayout->addWidget(new QLabel(tr("Select Disk:")));
auto* diskCombo = new QComboBox();
selectorLayout->addWidget(diskCombo, 1);
auto* refreshBtn = new QPushButton(tr("Refresh"));
selectorLayout->addWidget(refreshBtn);
m_diskCombo = new QComboBox();
connect(m_diskCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &DiagnosticsTab::onDiskChanged);
selectorLayout->addWidget(m_diskCombo, 1);
m_refreshBtn = new QPushButton(tr("Refresh"));
connect(m_refreshBtn, &QPushButton::clicked, this, &DiagnosticsTab::onRefreshSmart);
selectorLayout->addWidget(m_refreshBtn);
layout->addLayout(selectorLayout);
auto* splitter = new QSplitter(Qt::Horizontal);
// S.M.A.R.T. panel
auto* smartGroup = new QGroupBox(tr("S.M.A.R.T. Health"));
auto* smartLayout = new QVBoxLayout(smartGroup);
// ===== S.M.A.R.T. Panel =====
m_smartGroup = new QGroupBox(tr("S.M.A.R.T. Health"));
auto* smartLayout = new QVBoxLayout(m_smartGroup);
auto* healthLabel = new QLabel(tr("Overall Health: —"));
healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
smartLayout->addWidget(healthLabel);
auto* healthRow = new QHBoxLayout();
m_healthIcon = new QLabel();
m_healthIcon->setFixedSize(48, 48);
m_healthIcon->setAlignment(Qt::AlignCenter);
healthRow->addWidget(m_healthIcon);
m_healthLabel = new QLabel(tr("Overall Health: --"));
m_healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
healthRow->addWidget(m_healthLabel, 1);
smartLayout->addLayout(healthRow);
auto* smartTable = new QTableWidget(0, 5);
smartTable->setHorizontalHeaderLabels(
{tr("ID"), tr("Attribute"), tr("Value"), tr("Worst"), tr("Threshold")});
smartTable->setAlternatingRowColors(true);
smartLayout->addWidget(smartTable);
m_smartTable = new QTableWidget(0, 7);
m_smartTable->setHorizontalHeaderLabels(
{tr("ID"), tr("Attribute"), tr("Value"), tr("Worst"), tr("Threshold"),
tr("Raw Value"), tr("Status")});
m_smartTable->setAlternatingRowColors(true);
m_smartTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_smartTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_smartTable->horizontalHeader()->setStretchLastSection(true);
smartLayout->addWidget(m_smartTable);
splitter->addWidget(smartGroup);
splitter->addWidget(m_smartGroup);
// Benchmark & Surface Scan panel
// ===== Right Panel: Benchmark + Surface Scan =====
auto* rightPanel = new QWidget();
auto* rightLayout = new QVBoxLayout(rightPanel);
auto* benchGroup = new QGroupBox(tr("Benchmark"));
auto* benchLayout = new QVBoxLayout(benchGroup);
auto* benchResults = new QLabel(
tr("Sequential Read: — MB/s\n"
"Sequential Write: — MB/s\n"
"Random 4K Read: — IOPS\n"
"Random 4K Write: — IOPS"));
benchResults->setStyleSheet("font-family: monospace; padding: 8px;");
benchLayout->addWidget(benchResults);
auto* benchBtn = new QPushButton(tr("Run Benchmark"));
benchBtn->setObjectName("applyButton");
benchLayout->addWidget(benchBtn);
rightLayout->addWidget(benchGroup);
// Benchmark
m_benchGroup = new QGroupBox(tr("Benchmark"));
auto* benchLayout = new QVBoxLayout(m_benchGroup);
auto* scanGroup = new QGroupBox(tr("Surface Scan"));
auto* scanLayout = new QVBoxLayout(scanGroup);
auto* scanInfo = new QLabel(tr("Sectors: — total, — bad, — pending"));
scanLayout->addWidget(scanInfo);
auto* scanProgress = new QProgressBar();
scanLayout->addWidget(scanProgress);
auto* scanBtn = new QPushButton(tr("Start Surface Scan"));
scanBtn->setObjectName("applyButton");
scanLayout->addWidget(scanBtn);
rightLayout->addWidget(scanGroup);
auto* benchGrid = new QGridLayout();
benchGrid->addWidget(new QLabel(tr("Sequential Read:")), 0, 0);
m_seqReadBar = new QProgressBar();
m_seqReadBar->setRange(0, 7000);
m_seqReadBar->setValue(0);
m_seqReadBar->setFormat("%v MB/s");
benchGrid->addWidget(m_seqReadBar, 0, 1);
m_seqReadLabel = new QLabel(tr("-- MB/s"));
benchGrid->addWidget(m_seqReadLabel, 0, 2);
benchGrid->addWidget(new QLabel(tr("Sequential Write:")), 1, 0);
m_seqWriteBar = new QProgressBar();
m_seqWriteBar->setRange(0, 7000);
m_seqWriteBar->setValue(0);
m_seqWriteBar->setFormat("%v MB/s");
benchGrid->addWidget(m_seqWriteBar, 1, 1);
m_seqWriteLabel = new QLabel(tr("-- MB/s"));
benchGrid->addWidget(m_seqWriteLabel, 1, 2);
benchGrid->addWidget(new QLabel(tr("Random 4K Read:")), 2, 0);
m_rnd4kReadBar = new QProgressBar();
m_rnd4kReadBar->setRange(0, 1000000);
m_rnd4kReadBar->setValue(0);
m_rnd4kReadBar->setFormat("%v IOPS");
benchGrid->addWidget(m_rnd4kReadBar, 2, 1);
m_rnd4kReadLabel = new QLabel(tr("-- IOPS"));
benchGrid->addWidget(m_rnd4kReadLabel, 2, 2);
benchGrid->addWidget(new QLabel(tr("Random 4K Write:")), 3, 0);
m_rnd4kWriteBar = new QProgressBar();
m_rnd4kWriteBar->setRange(0, 1000000);
m_rnd4kWriteBar->setValue(0);
m_rnd4kWriteBar->setFormat("%v IOPS");
benchGrid->addWidget(m_rnd4kWriteBar, 3, 1);
m_rnd4kWriteLabel = new QLabel(tr("-- IOPS"));
benchGrid->addWidget(m_rnd4kWriteLabel, 3, 2);
benchLayout->addLayout(benchGrid);
m_iopsLabel = new QLabel(tr("QD32 IOPS: Read -- / Write --"));
m_iopsLabel->setStyleSheet("font-family: monospace;");
benchLayout->addWidget(m_iopsLabel);
m_latencyLabel = new QLabel(tr("Latency: Read -- us / Write -- us"));
m_latencyLabel->setStyleSheet("font-family: monospace;");
benchLayout->addWidget(m_latencyLabel);
m_benchBtn = new QPushButton(tr("Run Benchmark"));
m_benchBtn->setObjectName("applyButton");
connect(m_benchBtn, &QPushButton::clicked, this, &DiagnosticsTab::onRunBenchmark);
benchLayout->addWidget(m_benchBtn);
rightLayout->addWidget(m_benchGroup);
// Surface Scan
m_scanGroup = new QGroupBox(tr("Surface Scan"));
auto* scanLayout = new QVBoxLayout(m_scanGroup);
auto* modeRow = new QHBoxLayout();
m_readOnlyRadio = new QRadioButton(tr("Read-Only (safe)"));
m_readOnlyRadio->setChecked(true);
m_readWriteRadio = new QRadioButton(tr("Read-Write (DESTRUCTIVE)"));
m_readWriteRadio->setStyleSheet("color: red;");
modeRow->addWidget(m_readOnlyRadio);
modeRow->addWidget(m_readWriteRadio);
scanLayout->addLayout(modeRow);
m_scanProgress = new QProgressBar();
m_scanProgress->setValue(0);
scanLayout->addWidget(m_scanProgress);
m_scanBadCountLabel = new QLabel(tr("Bad sectors: --"));
scanLayout->addWidget(m_scanBadCountLabel);
m_scanSpeedLabel = new QLabel(tr("Speed: -- MB/s"));
scanLayout->addWidget(m_scanSpeedLabel);
m_scanBtn = new QPushButton(tr("Start Surface Scan"));
m_scanBtn->setObjectName("applyButton");
connect(m_scanBtn, &QPushButton::clicked, this, &DiagnosticsTab::onStartSurfaceScan);
scanLayout->addWidget(m_scanBtn);
rightLayout->addWidget(m_scanGroup);
rightLayout->addStretch();
splitter->addWidget(rightPanel);
layout->addWidget(splitter);
}
void DiagnosticsTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void DiagnosticsTab::populateDiskCombo()
{
m_diskCombo->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_diskCombo->addItem(label, disk.id);
}
}
void DiagnosticsTab::onDiskChanged(int index)
{
if (index < 0)
return;
onRefreshSmart();
}
void DiagnosticsTab::onRefreshSmart()
{
int diskId = m_diskCombo->currentData().toInt();
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& diskHandle = diskResult.value();
auto smartResult = SmartReader::readSmartData(diskHandle.nativeHandle(), diskId);
if (smartResult.isOk())
{
m_currentSmart = smartResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
displaySmartData(m_currentSmart);
});
thread->start();
}
void DiagnosticsTab::displaySmartData(const SmartData& data)
{
// Overall health
QString healthText;
QColor healthColor;
switch (data.overallHealth)
{
case SmartStatus::OK:
healthText = tr("PASSED - Healthy");
healthColor = QColor(0, 180, 0);
m_healthIcon->setStyleSheet("background-color: #00b400; border-radius: 24px;");
break;
case SmartStatus::Warning:
healthText = tr("WARNING - Issues Detected");
healthColor = QColor(255, 180, 0);
m_healthIcon->setStyleSheet("background-color: #ffb400; border-radius: 24px;");
break;
case SmartStatus::Critical:
healthText = tr("CRITICAL - Drive Failing");
healthColor = QColor(255, 0, 0);
m_healthIcon->setStyleSheet("background-color: #ff0000; border-radius: 24px;");
break;
default:
healthText = tr("Unknown");
healthColor = QColor(128, 128, 128);
m_healthIcon->setStyleSheet("background-color: #808080; border-radius: 24px;");
break;
}
m_healthLabel->setText(tr("Overall Health: %1").arg(healthText));
m_healthLabel->setStyleSheet(QString("font-size: 16px; font-weight: bold; color: %1; padding: 8px;")
.arg(healthColor.name()));
// Attributes table
m_smartTable->setRowCount(0);
if (data.isNvme)
{
// Show NVMe health info as pseudo-attributes
struct NvmeRow
{
QString name;
QString value;
};
const auto& h = data.nvmeHealth;
QVector<NvmeRow> rows = {
{tr("Temperature"), QString("%1 C").arg(h.temperature > 0 ? h.temperature - 273 : 0)},
{tr("Available Spare"), QString("%1%").arg(h.availableSpare)},
{tr("Spare Threshold"), QString("%1%").arg(h.availableSpareThreshold)},
{tr("Percentage Used"), QString("%1%").arg(h.percentageUsed)},
{tr("Data Read"), formatSize(h.dataUnitsRead * 512000ULL)},
{tr("Data Written"), formatSize(h.dataUnitsWritten * 512000ULL)},
{tr("Power Cycles"), QString::number(h.powerCycles)},
{tr("Power-On Hours"), QString::number(h.powerOnHours)},
{tr("Unsafe Shutdowns"), QString::number(h.unsafeShutdowns)},
{tr("Media Errors"), QString::number(h.mediaErrors)},
{tr("Error Log Entries"), QString::number(h.errorLogEntries)},
};
for (int i = 0; i < rows.size(); ++i)
{
int row = m_smartTable->rowCount();
m_smartTable->insertRow(row);
m_smartTable->setItem(row, 0, new QTableWidgetItem(QString::number(i + 1)));
m_smartTable->setItem(row, 1, new QTableWidgetItem(rows[i].name));
m_smartTable->setItem(row, 2, new QTableWidgetItem(rows[i].value));
m_smartTable->setItem(row, 3, new QTableWidgetItem("-"));
m_smartTable->setItem(row, 4, new QTableWidgetItem("-"));
m_smartTable->setItem(row, 5, new QTableWidgetItem(rows[i].value));
m_smartTable->setItem(row, 6, new QTableWidgetItem(smartStatusString(SmartStatus::OK)));
}
}
else
{
for (const auto& attr : data.attributes)
{
int row = m_smartTable->rowCount();
m_smartTable->insertRow(row);
m_smartTable->setItem(row, 0, new QTableWidgetItem(
QString("0x%1").arg(attr.id, 2, 16, QChar('0')).toUpper()));
m_smartTable->setItem(row, 1, new QTableWidgetItem(
QString::fromStdString(attr.name)));
m_smartTable->setItem(row, 2, new QTableWidgetItem(
QString::number(attr.currentValue)));
m_smartTable->setItem(row, 3, new QTableWidgetItem(
QString::number(attr.worstValue)));
m_smartTable->setItem(row, 4, new QTableWidgetItem(
QString::number(attr.threshold)));
m_smartTable->setItem(row, 5, new QTableWidgetItem(
QString::number(attr.rawValue)));
auto* statusItem = new QTableWidgetItem(smartStatusString(attr.status));
statusItem->setForeground(smartStatusColor(attr.status));
m_smartTable->setItem(row, 6, statusItem);
}
}
m_smartTable->resizeColumnsToContents();
}
void DiagnosticsTab::clearSmartData()
{
m_healthLabel->setText(tr("Overall Health: --"));
m_healthLabel->setStyleSheet("font-size: 16px; font-weight: bold; padding: 8px;");
m_healthIcon->setStyleSheet("background-color: #808080; border-radius: 24px;");
m_smartTable->setRowCount(0);
}
void DiagnosticsTab::onRunBenchmark()
{
int diskId = m_diskCombo->currentData().toInt();
// Find a volume letter on this disk for benchmarking
std::string volumePath;
for (const auto& part : m_snapshot.partitions)
{
if (part.diskId == diskId && part.driveLetter != L'\0')
{
volumePath = std::string(1, static_cast<char>(part.driveLetter)) + ":\\";
break;
}
}
if (volumePath.empty())
{
QMessageBox::warning(this, tr("No Volume"),
tr("No mounted volume found on this disk for benchmarking."));
return;
}
m_cancelFlag.store(false);
m_benchBtn->setEnabled(false);
clearBenchmarkDisplay();
auto* thread = QThread::create([this, volumePath]() {
Benchmark bench(volumePath);
BenchmarkConfig config;
config.durationSeconds = 5;
auto result = bench.run(config,
[this](BenchmarkPhase phase, int pct, const BenchmarkResults& partial) {
Q_UNUSED(pct);
Q_UNUSED(phase);
QMetaObject::invokeMethod(this, [this, partial]() {
updateBenchmarkDisplay(partial);
}, Qt::QueuedConnection);
},
&m_cancelFlag);
if (result.isOk())
{
m_currentBench = result.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_benchBtn->setEnabled(true);
updateBenchmarkDisplay(m_currentBench);
emit statusMessage(tr("Benchmark completed"));
});
thread->start();
}
void DiagnosticsTab::updateBenchmarkDisplay(const BenchmarkResults& r)
{
m_seqReadBar->setValue(static_cast<int>(r.seqReadMBps));
m_seqReadLabel->setText(QString("%1 MB/s").arg(r.seqReadMBps, 0, 'f', 1));
m_seqWriteBar->setValue(static_cast<int>(r.seqWriteMBps));
m_seqWriteLabel->setText(QString("%1 MB/s").arg(r.seqWriteMBps, 0, 'f', 1));
m_rnd4kReadBar->setValue(static_cast<int>(r.rnd4kReadIOPS));
m_rnd4kReadLabel->setText(QString("%1 IOPS").arg(r.rnd4kReadIOPS, 0, 'f', 0));
m_rnd4kWriteBar->setValue(static_cast<int>(r.rnd4kWriteIOPS));
m_rnd4kWriteLabel->setText(QString("%1 IOPS").arg(r.rnd4kWriteIOPS, 0, 'f', 0));
m_iopsLabel->setText(QString("QD32 IOPS: Read %1 / Write %2")
.arg(r.rnd4kReadIOPS_QD32, 0, 'f', 0)
.arg(r.rnd4kWriteIOPS_QD32, 0, 'f', 0));
m_latencyLabel->setText(QString("Latency: Read %1 us / Write %2 us")
.arg(r.avgReadLatencyUs, 0, 'f', 1)
.arg(r.avgWriteLatencyUs, 0, 'f', 1));
}
void DiagnosticsTab::clearBenchmarkDisplay()
{
m_seqReadBar->setValue(0);
m_seqWriteBar->setValue(0);
m_rnd4kReadBar->setValue(0);
m_rnd4kWriteBar->setValue(0);
m_seqReadLabel->setText(tr("-- MB/s"));
m_seqWriteLabel->setText(tr("-- MB/s"));
m_rnd4kReadLabel->setText(tr("-- IOPS"));
m_rnd4kWriteLabel->setText(tr("-- IOPS"));
m_iopsLabel->setText(tr("QD32 IOPS: Read -- / Write --"));
m_latencyLabel->setText(tr("Latency: Read -- us / Write -- us"));
}
void DiagnosticsTab::onStartSurfaceScan()
{
int diskId = m_diskCombo->currentData().toInt();
if (m_readWriteRadio->isChecked())
{
auto reply = QMessageBox::critical(this, tr("DESTRUCTIVE SCAN"),
tr("Read-Write mode will DESTROY ALL DATA on this disk!\n\n"
"Are you absolutely sure?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
}
SurfaceScanMode mode = m_readOnlyRadio->isChecked()
? SurfaceScanMode::ReadOnly
: SurfaceScanMode::WriteVerify;
m_cancelFlag.store(false);
m_scanBtn->setEnabled(false);
m_scanProgress->setValue(0);
m_scanBadCountLabel->setText(tr("Bad sectors: 0"));
auto* thread = QThread::create([this, diskId, mode]() {
auto diskResult = RawDiskHandle::open(diskId,
mode == SurfaceScanMode::WriteVerify
? DiskAccessMode::ReadWrite
: DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
SurfaceScan scan(disk);
auto result = scan.scanDisk(mode,
[this](uint64_t scanned, uint64_t total, uint64_t badCount,
double speedMBps, double /*eta*/) {
int pct = total > 0 ? static_cast<int>((scanned * 100) / total) : 0;
QMetaObject::invokeMethod(m_scanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_scanBadCountLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Bad sectors: %1").arg(badCount)));
QMetaObject::invokeMethod(m_scanSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Speed: %1 MB/s").arg(speedMBps, 0, 'f', 1)));
},
&m_cancelFlag);
if (result.isOk())
{
const auto& r = result.value();
QMetaObject::invokeMethod(m_scanBadCountLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("Bad sectors: %1 / %2 tested")
.arg(r.badSectorCount)
.arg(r.totalSectorsTested)));
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_scanBtn->setEnabled(true);
m_scanProgress->setValue(100);
emit statusMessage(tr("Surface scan completed"));
});
thread->start();
}
QString DiagnosticsTab::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);
}
QString DiagnosticsTab::smartStatusString(SmartStatus status)
{
switch (status)
{
case SmartStatus::OK: return QStringLiteral("OK");
case SmartStatus::Warning: return QStringLiteral("Warning");
case SmartStatus::Critical: return QStringLiteral("CRITICAL");
default: return QStringLiteral("Unknown");
}
}
QColor DiagnosticsTab::smartStatusColor(SmartStatus status)
{
switch (status)
{
case SmartStatus::OK: return QColor(0, 180, 0);
case SmartStatus::Warning: return QColor(255, 180, 0);
case SmartStatus::Critical: return QColor(255, 0, 0);
default: return QColor(128, 128, 128);
}
}
} // namespace spw

View File

@@ -1,6 +1,21 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/SmartReader.h"
#include "core/diagnostics/Benchmark.h"
#include "core/diagnostics/SurfaceScan.h"
#include <QWidget>
#include <atomic>
class QComboBox;
class QGroupBox;
class QLabel;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QTableWidget;
namespace spw
{
@@ -13,8 +28,68 @@ public:
explicit DiagnosticsTab(QWidget* parent = nullptr);
~DiagnosticsTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onDiskChanged(int index);
void onRefreshSmart();
void onRunBenchmark();
void onStartSurfaceScan();
private:
void setupUi();
void populateDiskCombo();
void displaySmartData(const SmartData& data);
void clearSmartData();
void updateBenchmarkDisplay(const BenchmarkResults& results);
void clearBenchmarkDisplay();
static QString formatSize(uint64_t bytes);
static QString smartStatusString(SmartStatus status);
static QColor smartStatusColor(SmartStatus status);
// Disk selector
QComboBox* m_diskCombo = nullptr;
QPushButton* m_refreshBtn = nullptr;
// SMART section
QGroupBox* m_smartGroup = nullptr;
QLabel* m_healthIcon = nullptr;
QLabel* m_healthLabel = nullptr;
QTableWidget* m_smartTable = nullptr;
// Benchmark section
QGroupBox* m_benchGroup = nullptr;
QProgressBar* m_seqReadBar = nullptr;
QProgressBar* m_seqWriteBar = nullptr;
QProgressBar* m_rnd4kReadBar = nullptr;
QProgressBar* m_rnd4kWriteBar = nullptr;
QLabel* m_seqReadLabel = nullptr;
QLabel* m_seqWriteLabel = nullptr;
QLabel* m_rnd4kReadLabel = nullptr;
QLabel* m_rnd4kWriteLabel = nullptr;
QLabel* m_iopsLabel = nullptr;
QLabel* m_latencyLabel = nullptr;
QPushButton* m_benchBtn = nullptr;
// Surface Scan section
QGroupBox* m_scanGroup = nullptr;
QRadioButton* m_readOnlyRadio = nullptr;
QRadioButton* m_readWriteRadio = nullptr;
QPushButton* m_scanBtn = nullptr;
QProgressBar* m_scanProgress = nullptr;
QLabel* m_scanBadCountLabel = nullptr;
QLabel* m_scanSpeedLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
SmartData m_currentSmart;
BenchmarkResults m_currentBench;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,12 +1,33 @@
#include "DiskPartitionTab.h"
#include "ui/widgets/DiskMapWidget.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/FilesystemDetector.h"
#include "core/operations/OperationQueue.h"
#include "core/operations/PartitionOperations.h"
#include <QAction>
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QProgressDialog>
#include <QPushButton>
#include <QSpinBox>
#include <QSplitter>
#include <QStandardItemModel>
#include <QTableView>
#include <QThread>
#include <QTreeView>
#include <QVBoxLayout>
@@ -17,6 +38,27 @@ DiskPartitionTab::DiskPartitionTab(QWidget* parent)
: QWidget(parent)
{
setupUi();
connect(&m_opQueue, &OperationQueue::allOperationsFinished,
this, [this](bool success, int completed, int total) {
Q_UNUSED(completed);
Q_UNUSED(total);
if (success)
{
QMessageBox::information(this, tr("Operations Complete"),
tr("All %1 operations completed successfully.").arg(completed));
}
else
{
QMessageBox::warning(this, tr("Operations Failed"),
tr("Operation failed. %1 of %2 completed.").arg(completed).arg(total));
}
emit statusMessage(tr("Refreshing disk list after operations..."));
// Request a full refresh
auto result = DiskEnumerator::getSystemSnapshot();
if (result.isOk())
refreshDisks(result.value());
});
}
DiskPartitionTab::~DiskPartitionTab() = default;
@@ -37,33 +79,52 @@ void DiskPartitionTab::setupUi()
diskLabel->setStyleSheet("font-weight: bold; padding: 4px;");
leftLayout->addWidget(diskLabel);
m_diskTreeModel = new QStandardItemModel(this);
m_diskTreeModel->setHorizontalHeaderLabels({tr("Disk / Partition"), tr("Size"), tr("Type")});
m_diskTree = new QTreeView();
m_diskTree->setModel(m_diskTreeModel);
m_diskTree->setHeaderHidden(false);
m_diskTree->setAlternatingRowColors(true);
m_diskTree->setMinimumWidth(250);
m_diskTree->setMinimumWidth(280);
m_diskTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_diskTree->setSelectionMode(QAbstractItemView::SingleSelection);
leftLayout->addWidget(m_diskTree);
connect(m_diskTree->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &DiskPartitionTab::onDiskTreeSelectionChanged);
m_mainSplitter->addWidget(leftPanel);
// Center + Bottom: partition map and table
m_rightSplitter = new QSplitter(Qt::Vertical);
// Disk map placeholder (will be replaced by DiskMapWidget)
m_diskMapPlaceholder = new QWidget();
m_diskMapPlaceholder->setMinimumHeight(120);
auto* mapLayout = new QVBoxLayout(m_diskMapPlaceholder);
auto* mapLabel = new QLabel(tr("Partition Map"));
mapLabel->setAlignment(Qt::AlignCenter);
mapLabel->setStyleSheet("color: #6c7086; font-size: 14px;");
mapLayout->addWidget(mapLabel);
m_rightSplitter->addWidget(m_diskMapPlaceholder);
// Disk map widget
m_diskMap = new DiskMapWidget();
m_rightSplitter->addWidget(m_diskMap);
connect(m_diskMap, &DiskMapWidget::partitionClicked,
this, &DiskPartitionTab::onDiskMapPartitionClicked);
connect(m_diskMap, &DiskMapWidget::contextMenuRequested,
this, &DiskPartitionTab::onDiskMapContextMenu);
// Partition detail table
m_partitionModel = new QStandardItemModel(this);
m_partitionModel->setHorizontalHeaderLabels(
{tr("#"), tr("Label"), tr("Drive Letter"), tr("Filesystem"),
tr("Size"), tr("Used"), tr("Free"), tr("Status"), tr("Flags")});
m_partitionTable = new QTableView();
m_partitionTable->setModel(m_partitionModel);
m_partitionTable->setAlternatingRowColors(true);
m_partitionTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_partitionTable->setSelectionMode(QAbstractItemView::SingleSelection);
m_partitionTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_partitionTable->horizontalHeader()->setStretchLastSection(true);
m_partitionTable->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_partitionTable, &QWidget::customContextMenuRequested,
this, &DiskPartitionTab::onPartitionTableContextMenu);
m_rightSplitter->addWidget(m_partitionTable);
m_rightSplitter->setStretchFactor(0, 2);
@@ -72,36 +133,817 @@ void DiskPartitionTab::setupUi()
m_mainSplitter->addWidget(m_rightSplitter);
// Right panel: pending operations list
m_operationList = new QWidget();
auto* opLayout = new QVBoxLayout(m_operationList);
auto* opPanel = new QWidget();
auto* opLayout = new QVBoxLayout(opPanel);
opLayout->setContentsMargins(0, 0, 0, 0);
auto* opLabel = new QLabel(tr("Pending Operations"));
opLabel->setStyleSheet("font-weight: bold; padding: 4px;");
opLayout->addWidget(opLabel);
auto* opListWidget = new QListWidget();
opListWidget->setMinimumWidth(220);
opLayout->addWidget(opListWidget);
m_operationListWidget = new QListWidget();
m_operationListWidget->setMinimumWidth(220);
opLayout->addWidget(m_operationListWidget);
auto* buttonLayout = new QHBoxLayout();
auto* applyBtn = new QPushButton(tr("Apply"));
applyBtn->setObjectName("applyButton");
auto* undoBtn = new QPushButton(tr("Undo"));
auto* clearBtn = new QPushButton(tr("Clear"));
buttonLayout->addWidget(applyBtn);
buttonLayout->addWidget(undoBtn);
buttonLayout->addWidget(clearBtn);
m_applyBtn = new QPushButton(tr("Apply"));
m_applyBtn->setObjectName("applyButton");
m_applyBtn->setEnabled(false);
m_undoBtn = new QPushButton(tr("Undo"));
m_undoBtn->setEnabled(false);
m_clearBtn = new QPushButton(tr("Clear"));
m_clearBtn->setEnabled(false);
buttonLayout->addWidget(m_applyBtn);
buttonLayout->addWidget(m_undoBtn);
buttonLayout->addWidget(m_clearBtn);
opLayout->addLayout(buttonLayout);
m_mainSplitter->addWidget(m_operationList);
connect(m_applyBtn, &QPushButton::clicked, this, &DiskPartitionTab::onApplyOperations);
connect(m_undoBtn, &QPushButton::clicked, this, &DiskPartitionTab::onUndoOperation);
connect(m_clearBtn, &QPushButton::clicked, this, &DiskPartitionTab::onClearOperations);
m_mainSplitter->addWidget(opPanel);
// Set splitter proportions
m_mainSplitter->setStretchFactor(0, 1); // Disk tree
m_mainSplitter->setStretchFactor(1, 3); // Center content
m_mainSplitter->setStretchFactor(2, 1); // Operation list
m_mainSplitter->setStretchFactor(0, 1);
m_mainSplitter->setStretchFactor(1, 3);
m_mainSplitter->setStretchFactor(2, 1);
layout->addWidget(m_mainSplitter);
}
void DiskPartitionTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskTree(snapshot);
// Re-select current disk if still valid
if (m_selectedDiskId >= 0)
{
populatePartitionTable(m_selectedDiskId);
updateDiskMap(m_selectedDiskId);
}
}
void DiskPartitionTab::populateDiskTree(const SystemDiskSnapshot& snapshot)
{
m_diskTreeModel->removeRows(0, m_diskTreeModel->rowCount());
for (const auto& disk : snapshot.disks)
{
QString diskName = QString("Disk %1: %2")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model));
auto* diskItem = new QStandardItem(diskName);
diskItem->setData(disk.id, Qt::UserRole); // Store diskId
diskItem->setData(-1, Qt::UserRole + 1); // Not a partition
diskItem->setIcon(QIcon::fromTheme("drive-harddisk"));
auto* sizeItem = new QStandardItem(formatSize(disk.sizeBytes));
auto* typeItem = new QStandardItem(
QString("%1 / %2")
.arg(interfaceTypeString(disk.interfaceType))
.arg(partitionTableTypeString(disk.partitionTableType)));
// Find partitions belonging to this disk
for (const auto& part : snapshot.partitions)
{
if (part.diskId != disk.id)
continue;
QString partLabel;
if (part.driveLetter != L'\0')
partLabel = QString("(%1:) ").arg(QChar(part.driveLetter));
if (!part.label.empty())
partLabel += QString::fromStdWString(part.label);
else
partLabel += filesystemString(part.filesystemType);
auto* partItem = new QStandardItem(partLabel);
partItem->setData(disk.id, Qt::UserRole);
partItem->setData(part.index, Qt::UserRole + 1);
auto* partSizeItem = new QStandardItem(formatSize(part.sizeBytes));
auto* partFsItem = new QStandardItem(filesystemString(part.filesystemType));
diskItem->appendRow({partItem, partSizeItem, partFsItem});
}
m_diskTreeModel->appendRow({diskItem, sizeItem, typeItem});
}
m_diskTree->expandAll();
m_diskTree->resizeColumnToContents(0);
}
void DiskPartitionTab::populatePartitionTable(DiskId diskId)
{
m_partitionModel->removeRows(0, m_partitionModel->rowCount());
for (const auto& part : m_snapshot.partitions)
{
if (part.diskId != diskId)
continue;
QList<QStandardItem*> row;
row.append(new QStandardItem(QString::number(part.index)));
// Label
row.append(new QStandardItem(QString::fromStdWString(part.label)));
// Drive letter
if (part.driveLetter != L'\0')
row.append(new QStandardItem(QString("%1:").arg(QChar(part.driveLetter))));
else
row.append(new QStandardItem(QStringLiteral("-")));
// Filesystem
row.append(new QStandardItem(filesystemString(part.filesystemType)));
// Size
row.append(new QStandardItem(formatSize(part.sizeBytes)));
// Used / Free — look up volume info
QString usedStr = QStringLiteral("-");
QString freeStr = QStringLiteral("-");
for (const auto& vol : m_snapshot.volumes)
{
if (vol.guidPath == part.volumeGuidPath && vol.totalBytes > 0)
{
uint64_t used = vol.totalBytes - vol.freeBytes;
usedStr = formatSize(used);
freeStr = formatSize(vol.freeBytes);
break;
}
}
row.append(new QStandardItem(usedStr));
row.append(new QStandardItem(freeStr));
// Status
QStringList statusFlags;
if (part.isActive)
statusFlags << QStringLiteral("Active");
if (part.isBootable)
statusFlags << QStringLiteral("Boot");
row.append(new QStandardItem(statusFlags.isEmpty() ? QStringLiteral("Normal") : statusFlags.join(", ")));
// Flags
QStringList flags;
if (part.isActive)
flags << QStringLiteral("Boot");
if (part.mbrType != 0)
flags << QString("MBR 0x%1").arg(part.mbrType, 2, 16, QChar('0'));
row.append(new QStandardItem(flags.isEmpty() ? QStringLiteral("-") : flags.join(", ")));
// Store partition index in first item
row[0]->setData(part.index, Qt::UserRole);
m_partitionModel->appendRow(row);
}
m_partitionTable->resizeColumnsToContents();
}
void DiskPartitionTab::updateDiskMap(DiskId diskId)
{
// Collect partitions for this disk
std::vector<PartitionInfo> diskPartitions;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskId)
diskPartitions.push_back(p);
}
// Find disk info
for (const auto& d : m_snapshot.disks)
{
if (d.id == diskId)
{
m_diskMap->setDisk(d, diskPartitions, m_snapshot.volumes);
return;
}
}
m_diskMap->clear();
}
void DiskPartitionTab::onDiskTreeSelectionChanged()
{
auto indexes = m_diskTree->selectionModel()->selectedIndexes();
if (indexes.isEmpty())
return;
auto idx = indexes.first();
DiskId diskId = idx.data(Qt::UserRole).toInt();
m_selectedDiskId = diskId;
populatePartitionTable(diskId);
updateDiskMap(diskId);
}
void DiskPartitionTab::onPartitionTableContextMenu(const QPoint& pos)
{
auto index = m_partitionTable->indexAt(pos);
int partIdx = -1;
if (index.isValid())
{
auto* item = m_partitionModel->item(index.row(), 0);
if (item)
partIdx = item->data(Qt::UserRole).toInt();
}
showContextMenu(partIdx, m_partitionTable->viewport()->mapToGlobal(pos));
}
void DiskPartitionTab::onDiskMapContextMenu(int partitionIndex, const QPoint& globalPos)
{
showContextMenu(partitionIndex, globalPos);
}
void DiskPartitionTab::onDiskMapPartitionClicked(int partitionIndex)
{
// Select the corresponding row in partition table
for (int r = 0; r < m_partitionModel->rowCount(); ++r)
{
auto* item = m_partitionModel->item(r, 0);
if (item && item->data(Qt::UserRole).toInt() == partitionIndex)
{
m_partitionTable->selectRow(r);
break;
}
}
}
void DiskPartitionTab::showContextMenu(int partitionIndex, const QPoint& globalPos)
{
QMenu menu(this);
auto* createAct = menu.addAction(tr("Create Partition..."));
connect(createAct, &QAction::triggered, this, &DiskPartitionTab::onCreatePartition);
if (partitionIndex >= 0)
{
menu.addSeparator();
auto* deleteAct = menu.addAction(tr("Delete Partition"));
connect(deleteAct, &QAction::triggered, this, &DiskPartitionTab::onDeletePartition);
auto* resizeAct = menu.addAction(tr("Resize/Move..."));
connect(resizeAct, &QAction::triggered, this, &DiskPartitionTab::onResizePartition);
auto* formatAct = menu.addAction(tr("Format..."));
connect(formatAct, &QAction::triggered, this, &DiskPartitionTab::onFormatPartition);
menu.addSeparator();
auto* labelAct = menu.addAction(tr("Set Label..."));
connect(labelAct, &QAction::triggered, this, &DiskPartitionTab::onSetLabel);
auto* flagsAct = menu.addAction(tr("Set Flags..."));
connect(flagsAct, &QAction::triggered, this, &DiskPartitionTab::onSetFlags);
menu.addSeparator();
auto* checkAct = menu.addAction(tr("Check Filesystem"));
connect(checkAct, &QAction::triggered, this, &DiskPartitionTab::onCheckFilesystem);
}
menu.exec(globalPos);
}
int DiskPartitionTab::selectedPartitionIndex() const
{
auto indexes = m_partitionTable->selectionModel()->selectedRows();
if (indexes.isEmpty())
return -1;
auto* item = m_partitionModel->item(indexes.first().row(), 0);
return item ? item->data(Qt::UserRole).toInt() : -1;
}
DiskId DiskPartitionTab::selectedDiskId() const
{
return m_selectedDiskId;
}
void DiskPartitionTab::onCreatePartition()
{
if (m_selectedDiskId < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a disk first."));
return;
}
// Find disk info
const DiskInfo* diskInfo = nullptr;
for (const auto& d : m_snapshot.disks)
{
if (d.id == m_selectedDiskId)
{
diskInfo = &d;
break;
}
}
if (!diskInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Create Partition"));
auto* form = new QFormLayout(&dlg);
auto* sizeGbSpin = new QDoubleSpinBox();
sizeGbSpin->setRange(0.001, static_cast<double>(diskInfo->sizeBytes) / (1024.0 * 1024.0 * 1024.0));
sizeGbSpin->setDecimals(3);
sizeGbSpin->setSuffix(QStringLiteral(" GB"));
sizeGbSpin->setValue(1.0);
form->addRow(tr("Size:"), sizeGbSpin);
auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2")});
form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit();
form->addRow(tr("Label:"), labelEdit);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
// Build operation
uint64_t sizeBytes = static_cast<uint64_t>(sizeGbSpin->value() * 1024.0 * 1024.0 * 1024.0);
uint32_t sectorSize = diskInfo->sectorSize;
SectorCount sectors = sizeBytes / sectorSize;
// Find first large enough gap
SectorOffset startLba = DEFAULT_ALIGNMENT_SECTORS_512;
// Simple: use offset after last partition
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId)
{
SectorOffset end = (p.offsetBytes + p.sizeBytes) / sectorSize;
if (end > startLba)
startLba = DiskGeometry::alignSectorUp(end, DEFAULT_ALIGNMENT_SECTORS_512);
}
}
CreatePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.startLba = startLba;
params.sectorCount = sectors;
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];
}
params.formatOptions.volumeLabel = labelEdit->text().toStdString();
params.formatOptions.quickFormat = true;
auto op = std::make_unique<CreatePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onDeletePartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
// Find partition info
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
auto reply = QMessageBox::question(this, tr("Delete Partition"),
tr("Are you sure you want to delete partition %1?\n"
"This operation will be queued and applied when you click Apply.")
.arg(partIdx));
if (reply != QMessageBox::Yes)
return;
DeletePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.sectorSize = 512; // Default
params.driveLetter = partInfo->driveLetter;
auto op = std::make_unique<DeletePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onResizePartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
double currentGb = static_cast<double>(partInfo->sizeBytes) / (1024.0 * 1024.0 * 1024.0);
bool ok = false;
double newGb = QInputDialog::getDouble(this, tr("Resize Partition"),
tr("New size in GB (current: %1 GB):").arg(currentGb, 0, 'f', 2),
currentGb, 0.001, 999999.0, 3, &ok);
if (!ok)
return;
uint64_t newSizeBytes = static_cast<uint64_t>(newGb * 1024.0 * 1024.0 * 1024.0);
uint32_t sectorSize = 512;
SectorCount newSectors = newSizeBytes / sectorSize;
SectorOffset startLba = partInfo->offsetBytes / sectorSize;
ResizePartitionOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.sectorSize = sectorSize;
params.driveLetter = partInfo->driveLetter;
params.newStartLba = startLba;
params.newSectorCount = newSectors;
auto op = std::make_unique<ResizePartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onFormatPartition()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Format Partition"));
auto* form = new QFormLayout(&dlg);
auto* fsCombo = new QComboBox();
fsCombo->addItems({tr("NTFS"), tr("FAT32"), tr("exFAT"), tr("ext4"), tr("ext3"), tr("ext2"), tr("Linux Swap")});
form->addRow(tr("Filesystem:"), fsCombo);
auto* labelEdit = new QLineEdit();
form->addRow(tr("Label:"), labelEdit);
auto* quickCheck = new QCheckBox(tr("Quick Format"));
quickCheck->setChecked(true);
form->addRow(quickCheck);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
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;
if (partInfo->driveLetter != L'\0')
{
params.target.driveLetter = partInfo->driveLetter;
}
else
{
params.target.diskIndex = m_selectedDiskId;
params.target.partitionOffsetBytes = partInfo->offsetBytes;
params.target.partitionSizeBytes = partInfo->sizeBytes;
}
int fsIdx = fsCombo->currentIndex();
if (fsIdx >= 0 && fsIdx < static_cast<int>(std::size(fsTypes)))
params.options.targetFs = fsTypes[fsIdx];
params.options.volumeLabel = labelEdit->text().toStdString();
params.options.quickFormat = quickCheck->isChecked();
auto op = std::make_unique<FormatPartitionOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onSetLabel()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
bool ok = false;
QString newLabel = QInputDialog::getText(this, tr("Set Volume Label"),
tr("New label:"),
QLineEdit::Normal,
QString::fromStdWString(partInfo->label), &ok);
if (!ok)
return;
SetLabelOp::Params params;
params.driveLetter = partInfo->driveLetter;
params.newLabel = newLabel.toStdString();
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.partitionOffsetBytes = partInfo->offsetBytes;
params.fsType = partInfo->filesystemType;
auto op = std::make_unique<SetLabelOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onSetFlags()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
QDialog dlg(this);
dlg.setWindowTitle(tr("Set Partition Flags"));
auto* form = new QFormLayout(&dlg);
auto* activeCheck = new QCheckBox(tr("Active (Bootable)"));
activeCheck->setChecked(partInfo->isActive);
form->addRow(activeCheck);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
form->addRow(buttons);
if (dlg.exec() != QDialog::Accepted)
return;
SetFlagsOp::Params params;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.setActive = activeCheck->isChecked();
auto op = std::make_unique<SetFlagsOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onCheckFilesystem()
{
int partIdx = selectedPartitionIndex();
if (partIdx < 0 || m_selectedDiskId < 0)
return;
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == m_selectedDiskId && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
CheckFilesystemOp::Params params;
params.driveLetter = partInfo->driveLetter;
params.diskId = m_selectedDiskId;
params.partitionIndex = partIdx;
params.partitionOffsetBytes = partInfo->offsetBytes;
params.fsType = partInfo->filesystemType;
params.repair = false;
auto op = std::make_unique<CheckFilesystemOp>(params);
m_opQueue.enqueue(std::move(op));
updateOperationList();
}
void DiskPartitionTab::onApplyOperations()
{
if (m_opQueue.pendingCount() == 0)
return;
auto reply = QMessageBox::warning(
this, tr("Apply Operations"),
tr("You are about to apply %1 pending operation(s).\n\n"
"WARNING: These operations may modify your disk permanently.\n"
"Make sure you have backed up important data.\n\n"
"Continue?")
.arg(m_opQueue.pendingCount()),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
auto* progressDlg = new QProgressDialog(tr("Applying operations..."), tr("Cancel"), 0, 100, this);
progressDlg->setWindowModality(Qt::WindowModal);
progressDlg->setMinimumDuration(0);
progressDlg->show();
connect(&m_opQueue, &OperationQueue::queueProgress,
progressDlg, [progressDlg](int overall, int /*current*/, const QString& status) {
progressDlg->setValue(overall);
progressDlg->setLabelText(status);
});
connect(progressDlg, &QProgressDialog::canceled, &m_opQueue, &OperationQueue::requestCancel);
auto* thread = QThread::create([this]() {
m_opQueue.applyAll();
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, progressDlg, &QProgressDialog::close);
connect(thread, &QThread::finished, progressDlg, &QProgressDialog::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
updateOperationList();
});
thread->start();
}
void DiskPartitionTab::onUndoOperation()
{
auto removed = m_opQueue.removeLast();
if (removed)
{
updateOperationList();
}
}
void DiskPartitionTab::onClearOperations()
{
m_opQueue.clearPending();
updateOperationList();
}
void DiskPartitionTab::updateOperationList()
{
m_operationListWidget->clear();
const auto& pending = m_opQueue.pending();
for (const auto& op : pending)
{
m_operationListWidget->addItem(op->description());
}
bool hasPending = m_opQueue.pendingCount() > 0;
m_applyBtn->setEnabled(hasPending);
m_undoBtn->setEnabled(hasPending);
m_clearBtn->setEnabled(hasPending);
emit statusMessage(hasPending
? tr("%1 pending operation(s)").arg(m_opQueue.pendingCount())
: tr("No pending operations"));
}
QString DiskPartitionTab::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);
if (bytes >= 1024ULL)
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
return QString("%1 B").arg(bytes);
}
QString DiskPartitionTab::interfaceTypeString(DiskInterfaceType type)
{
switch (type)
{
case DiskInterfaceType::SATA: return QStringLiteral("SATA");
case DiskInterfaceType::NVMe: return QStringLiteral("NVMe");
case DiskInterfaceType::USB: return QStringLiteral("USB");
case DiskInterfaceType::SCSI: return QStringLiteral("SCSI");
case DiskInterfaceType::SAS: return QStringLiteral("SAS");
case DiskInterfaceType::IDE: return QStringLiteral("IDE");
case DiskInterfaceType::MMC: return QStringLiteral("MMC");
case DiskInterfaceType::Firewire: return QStringLiteral("FireWire");
case DiskInterfaceType::Thunderbolt: return QStringLiteral("Thunderbolt");
case DiskInterfaceType::Virtual: return QStringLiteral("Virtual");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::mediaTypeString(MediaType type)
{
switch (type)
{
case MediaType::HDD: return QStringLiteral("HDD");
case MediaType::SSD: return QStringLiteral("SSD");
case MediaType::NVMe: return QStringLiteral("NVMe");
case MediaType::USBFlash: return QStringLiteral("USB Flash");
case MediaType::SDCard: return QStringLiteral("SD Card");
case MediaType::CompactFlash: return QStringLiteral("CF");
case MediaType::OpticalDrive: return QStringLiteral("Optical");
case MediaType::FloppyDisk: return QStringLiteral("Floppy");
case MediaType::Virtual: return QStringLiteral("Virtual");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::filesystemString(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QStringLiteral("NTFS");
case FilesystemType::FAT32: return QStringLiteral("FAT32");
case FilesystemType::FAT16: return QStringLiteral("FAT16");
case FilesystemType::FAT12: return QStringLiteral("FAT12");
case FilesystemType::ExFAT: return QStringLiteral("exFAT");
case FilesystemType::ReFS: return QStringLiteral("ReFS");
case FilesystemType::Ext2: return QStringLiteral("ext2");
case FilesystemType::Ext3: return QStringLiteral("ext3");
case FilesystemType::Ext4: return QStringLiteral("ext4");
case FilesystemType::Btrfs: return QStringLiteral("Btrfs");
case FilesystemType::XFS: return QStringLiteral("XFS");
case FilesystemType::ZFS: return QStringLiteral("ZFS");
case FilesystemType::HFSPlus: return QStringLiteral("HFS+");
case FilesystemType::APFS: return QStringLiteral("APFS");
case FilesystemType::SWAP_LINUX: return QStringLiteral("Linux Swap");
case FilesystemType::Unallocated: return QStringLiteral("Unallocated");
case FilesystemType::Raw: return QStringLiteral("RAW");
default: return QStringLiteral("Unknown");
}
}
QString DiskPartitionTab::partitionTableTypeString(PartitionTableType pt)
{
switch (pt)
{
case PartitionTableType::MBR: return QStringLiteral("MBR");
case PartitionTableType::GPT: return QStringLiteral("GPT");
case PartitionTableType::APM: return QStringLiteral("APM");
default: return QStringLiteral("Unknown");
}
}
} // namespace spw

View File

@@ -1,14 +1,26 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/operations/OperationQueue.h"
#include <QWidget>
class QSplitter;
class QTreeView;
class QTableView;
class QListWidget;
class QPushButton;
class QStandardItemModel;
class QMenu;
class QLabel;
class QProgressDialog;
namespace spw
{
class DiskMapWidget;
class DiskPartitionTab : public QWidget
{
Q_OBJECT
@@ -17,23 +29,72 @@ public:
explicit DiskPartitionTab(QWidget* parent = nullptr);
~DiskPartitionTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onDiskTreeSelectionChanged();
void onPartitionTableContextMenu(const QPoint& pos);
void onDiskMapContextMenu(int partitionIndex, const QPoint& globalPos);
void onDiskMapPartitionClicked(int partitionIndex);
void onApplyOperations();
void onUndoOperation();
void onClearOperations();
// Context menu actions
void onCreatePartition();
void onDeletePartition();
void onResizePartition();
void onFormatPartition();
void onSetLabel();
void onSetFlags();
void onCheckFilesystem();
private:
void setupUi();
void populateDiskTree(const SystemDiskSnapshot& snapshot);
void populatePartitionTable(DiskId diskId);
void updateDiskMap(DiskId diskId);
void updateOperationList();
void showContextMenu(int partitionIndex, const QPoint& globalPos);
// Find the currently selected partition info
int selectedPartitionIndex() const;
DiskId selectedDiskId() const;
static QString formatSize(uint64_t bytes);
static QString interfaceTypeString(DiskInterfaceType type);
static QString mediaTypeString(MediaType type);
static QString filesystemString(FilesystemType fs);
static QString partitionTableTypeString(PartitionTableType pt);
QSplitter* m_mainSplitter = nullptr;
QSplitter* m_rightSplitter = nullptr;
// Left panel: disk tree
QTreeView* m_diskTree = nullptr;
QStandardItemModel* m_diskTreeModel = nullptr;
// Center: partition map (placeholder for DiskMapWidget)
QWidget* m_diskMapPlaceholder = nullptr;
// Center: partition map
DiskMapWidget* m_diskMap = nullptr;
// Bottom: partition detail table
QTableView* m_partitionTable = nullptr;
QStandardItemModel* m_partitionModel = nullptr;
// Right: operation list
QWidget* m_operationList = nullptr;
QListWidget* m_operationListWidget = nullptr;
QPushButton* m_applyBtn = nullptr;
QPushButton* m_undoBtn = nullptr;
QPushButton* m_clearBtn = nullptr;
// Backend
OperationQueue m_opQueue;
SystemDiskSnapshot m_snapshot;
DiskId m_selectedDiskId = -1;
};
} // namespace spw

View File

@@ -1,14 +1,22 @@
#include "ImagingTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/imaging/DiskCloner.h"
#include "core/imaging/ImageCreator.h"
#include "core/imaging/ImageRestorer.h"
#include "core/imaging/IsoFlasher.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -26,71 +34,480 @@ void ImagingTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Clone Disk section
// ===== Clone Disk =====
auto* cloneGroup = new QGroupBox(tr("Clone Disk"));
auto* cloneLayout = new QGridLayout(cloneGroup);
cloneLayout->addWidget(new QLabel(tr("Source:")), 0, 0);
cloneLayout->addWidget(new QComboBox(), 0, 1);
cloneLayout->addWidget(new QLabel(tr("Target:")), 1, 0);
cloneLayout->addWidget(new QComboBox(), 1, 1);
auto* cloneBtn = new QPushButton(tr("Clone"));
cloneBtn->setObjectName("applyButton");
cloneLayout->addWidget(cloneBtn, 2, 1, Qt::AlignRight);
m_cloneSourceCombo = new QComboBox();
cloneLayout->addWidget(m_cloneSourceCombo, 0, 1, 1, 2);
cloneLayout->addWidget(new QLabel(tr("Destination:")), 1, 0);
m_cloneDestCombo = new QComboBox();
cloneLayout->addWidget(m_cloneDestCombo, 1, 1, 1, 2);
cloneLayout->addWidget(new QLabel(tr("Mode:")), 2, 0);
m_cloneModeCombo = new QComboBox();
m_cloneModeCombo->addItems({tr("Raw (sector-by-sector)"), tr("Smart (skip free space)")});
cloneLayout->addWidget(m_cloneModeCombo, 2, 1);
m_cloneVerifyCheck = new QCheckBox(tr("Verify after clone"));
m_cloneVerifyCheck->setChecked(true);
cloneLayout->addWidget(m_cloneVerifyCheck, 2, 2);
m_cloneProgress = new QProgressBar();
m_cloneProgress->setVisible(false);
cloneLayout->addWidget(m_cloneProgress, 3, 0, 1, 2);
m_cloneSpeedLabel = new QLabel();
cloneLayout->addWidget(m_cloneSpeedLabel, 3, 2);
m_cloneBtn = new QPushButton(tr("Clone"));
m_cloneBtn->setObjectName("applyButton");
connect(m_cloneBtn, &QPushButton::clicked, this, &ImagingTab::onCloneDisk);
cloneLayout->addWidget(m_cloneBtn, 4, 2, Qt::AlignRight);
layout->addWidget(cloneGroup);
// Create Image section
auto* imageGroup = new QGroupBox(tr("Create Disk/Media Image"));
// ===== Create Image =====
auto* imageGroup = new QGroupBox(tr("Create Disk Image"));
auto* imageLayout = new QGridLayout(imageGroup);
imageLayout->addWidget(new QLabel(tr("Source:")), 0, 0);
auto* sourceCombo = new QComboBox();
sourceCombo->setToolTip(tr("Select disk, USB drive, SD card, or other media"));
imageLayout->addWidget(sourceCombo, 0, 1);
m_imageSourceCombo = new QComboBox();
imageLayout->addWidget(m_imageSourceCombo, 0, 1, 1, 2);
imageLayout->addWidget(new QLabel(tr("Output File:")), 1, 0);
auto* outputLine = new QLineEdit();
imageLayout->addWidget(outputLine, 1, 1);
auto* browseBtn = new QPushButton(tr("Browse..."));
imageLayout->addWidget(browseBtn, 1, 2);
imageLayout->addWidget(new QLabel(tr("Compression:")), 2, 0);
auto* compCombo = new QComboBox();
compCombo->addItems({tr("None"), tr("Fast (zstd-1)"), tr("Default (zstd-3)"), tr("Best (zstd-9)")});
imageLayout->addWidget(compCombo, 2, 1);
auto* createImgBtn = new QPushButton(tr("Create Image"));
createImgBtn->setObjectName("applyButton");
imageLayout->addWidget(createImgBtn, 3, 1, Qt::AlignRight);
m_imageOutputEdit = new QLineEdit();
imageLayout->addWidget(m_imageOutputEdit, 1, 1);
auto* imgBrowseBtn = new QPushButton(tr("Browse..."));
connect(imgBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseImageOutput);
imageLayout->addWidget(imgBrowseBtn, 1, 2);
imageLayout->addWidget(new QLabel(tr("Format:")), 2, 0);
m_imageFormatCombo = new QComboBox();
m_imageFormatCombo->addItems({tr("Raw (.img)"), tr("Compressed SPW (.spw)")});
imageLayout->addWidget(m_imageFormatCombo, 2, 1);
m_imageCreateProgress = new QProgressBar();
m_imageCreateProgress->setVisible(false);
imageLayout->addWidget(m_imageCreateProgress, 3, 0, 1, 2);
m_imageCreateSpeedLabel = new QLabel();
imageLayout->addWidget(m_imageCreateSpeedLabel, 3, 2);
m_imageCreateBtn = new QPushButton(tr("Create Image"));
m_imageCreateBtn->setObjectName("applyButton");
connect(m_imageCreateBtn, &QPushButton::clicked, this, &ImagingTab::onCreateImage);
imageLayout->addWidget(m_imageCreateBtn, 4, 2, Qt::AlignRight);
layout->addWidget(imageGroup);
// Restore Image section
// ===== Restore Image =====
auto* restoreGroup = new QGroupBox(tr("Restore Image"));
auto* restoreLayout = new QGridLayout(restoreGroup);
restoreLayout->addWidget(new QLabel(tr("Image File:")), 0, 0);
restoreLayout->addWidget(new QLineEdit(), 0, 1);
restoreLayout->addWidget(new QPushButton(tr("Browse...")), 0, 2);
restoreLayout->addWidget(new QLabel(tr("Target:")), 1, 0);
restoreLayout->addWidget(new QComboBox(), 1, 1);
auto* restoreBtn = new QPushButton(tr("Restore"));
restoreBtn->setObjectName("applyButton");
restoreLayout->addWidget(restoreBtn, 2, 1, Qt::AlignRight);
m_restoreInputEdit = new QLineEdit();
connect(m_restoreInputEdit, &QLineEdit::textChanged, this, &ImagingTab::onRestoreInputChanged);
restoreLayout->addWidget(m_restoreInputEdit, 0, 1);
auto* restBrowseBtn = new QPushButton(tr("Browse..."));
connect(restBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseRestoreInput);
restoreLayout->addWidget(restBrowseBtn, 0, 2);
m_restoreImageInfo = new QLabel(tr("No image selected"));
m_restoreImageInfo->setWordWrap(true);
m_restoreImageInfo->setStyleSheet("color: #6c7086; padding: 4px;");
restoreLayout->addWidget(m_restoreImageInfo, 1, 0, 1, 3);
restoreLayout->addWidget(new QLabel(tr("Destination:")), 2, 0);
m_restoreDestCombo = new QComboBox();
restoreLayout->addWidget(m_restoreDestCombo, 2, 1);
m_restoreVerifyCheck = new QCheckBox(tr("Verify after restore"));
m_restoreVerifyCheck->setChecked(true);
restoreLayout->addWidget(m_restoreVerifyCheck, 2, 2);
m_restoreProgress = new QProgressBar();
m_restoreProgress->setVisible(false);
restoreLayout->addWidget(m_restoreProgress, 3, 0, 1, 2);
m_restoreSpeedLabel = new QLabel();
restoreLayout->addWidget(m_restoreSpeedLabel, 3, 2);
m_restoreBtn = new QPushButton(tr("Restore"));
m_restoreBtn->setObjectName("applyButton");
connect(m_restoreBtn, &QPushButton::clicked, this, &ImagingTab::onRestoreImage);
restoreLayout->addWidget(m_restoreBtn, 4, 2, Qt::AlignRight);
layout->addWidget(restoreGroup);
// Flash ISO/IMG section
// ===== Flash ISO/IMG =====
auto* flashGroup = new QGroupBox(tr("Flash ISO/IMG to USB"));
auto* flashLayout = new QGridLayout(flashGroup);
flashLayout->addWidget(new QLabel(tr("Image:")), 0, 0);
flashLayout->addWidget(new QLineEdit(), 0, 1);
flashLayout->addWidget(new QPushButton(tr("Browse...")), 0, 2);
flashLayout->addWidget(new QLabel(tr("Target USB:")), 1, 0);
flashLayout->addWidget(new QComboBox(), 1, 1);
auto* flashBtn = new QPushButton(tr("Flash"));
flashBtn->setObjectName("applyButton");
flashLayout->addWidget(flashBtn, 2, 1, Qt::AlignRight);
layout->addWidget(flashGroup);
// Progress
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
layout->addWidget(progressBar);
flashLayout->addWidget(new QLabel(tr("Image:")), 0, 0);
m_flashInputEdit = new QLineEdit();
flashLayout->addWidget(m_flashInputEdit, 0, 1);
auto* flashBrowseBtn = new QPushButton(tr("Browse..."));
connect(flashBrowseBtn, &QPushButton::clicked, this, &ImagingTab::onBrowseFlashInput);
flashLayout->addWidget(flashBrowseBtn, 0, 2);
flashLayout->addWidget(new QLabel(tr("Target USB:")), 1, 0);
m_flashTargetCombo = new QComboBox();
flashLayout->addWidget(m_flashTargetCombo, 1, 1);
m_flashVerifyCheck = new QCheckBox(tr("Verify after flash"));
m_flashVerifyCheck->setChecked(true);
flashLayout->addWidget(m_flashVerifyCheck, 1, 2);
m_flashProgress = new QProgressBar();
m_flashProgress->setVisible(false);
flashLayout->addWidget(m_flashProgress, 2, 0, 1, 2);
m_flashSpeedLabel = new QLabel();
flashLayout->addWidget(m_flashSpeedLabel, 2, 2);
m_flashBtn = new QPushButton(tr("Flash"));
m_flashBtn->setObjectName("applyButton");
connect(m_flashBtn, &QPushButton::clicked, this, &ImagingTab::onFlashIso);
flashLayout->addWidget(m_flashBtn, 3, 2, Qt::AlignRight);
layout->addWidget(flashGroup);
layout->addStretch();
}
void ImagingTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombos();
}
void ImagingTab::populateDiskCombos()
{
// Clear all combos
m_cloneSourceCombo->clear();
m_cloneDestCombo->clear();
m_imageSourceCombo->clear();
m_restoreDestCombo->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_cloneSourceCombo->addItem(label, disk.id);
m_cloneDestCombo->addItem(label, disk.id);
m_imageSourceCombo->addItem(label, disk.id);
m_restoreDestCombo->addItem(label, disk.id);
// Flash target: only removable drives
if (disk.isRemovable)
{
m_flashTargetCombo->addItem(label, disk.id);
}
}
if (m_flashTargetCombo->count() == 0)
{
m_flashTargetCombo->addItem(tr("No removable drives detected"));
}
}
void ImagingTab::onCloneDisk()
{
int srcDiskId = m_cloneSourceCombo->currentData().toInt();
int dstDiskId = m_cloneDestCombo->currentData().toInt();
if (srcDiskId == dstDiskId)
{
QMessageBox::warning(this, tr("Invalid"), tr("Source and destination must be different disks."));
return;
}
auto reply = QMessageBox::warning(this, tr("Clone Disk"),
tr("ALL data on Disk %1 will be OVERWRITTEN.\n\n"
"Source: Disk %2\nDestination: Disk %1\n\nContinue?")
.arg(dstDiskId).arg(srcDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
CloneConfig config;
config.sourceDiskId = srcDiskId;
config.destDiskId = dstDiskId;
config.mode = m_cloneModeCombo->currentIndex() == 0 ? CloneMode::Raw : CloneMode::Smart;
config.verifyAfterClone = m_cloneVerifyCheck->isChecked();
m_cloneProgress->setVisible(true);
m_cloneProgress->setValue(0);
m_cloneBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
DiskCloner cloner;
cloner.clone(config, [this](const CloneProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_cloneProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_cloneSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_cloneProgress->setVisible(false);
m_cloneBtn->setEnabled(true);
m_cloneSpeedLabel->clear();
QMessageBox::information(this, tr("Clone Complete"), tr("Disk cloning completed."));
emit statusMessage(tr("Disk clone completed"));
});
thread->start();
}
void ImagingTab::onCreateImage()
{
int srcDiskId = m_imageSourceCombo->currentData().toInt();
QString outputPath = m_imageOutputEdit->text();
if (outputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Output"), tr("Please specify an output file."));
return;
}
ImageCreateConfig config;
config.sourceDiskId = srcDiskId;
config.outputFilePath = outputPath.toStdWString();
config.format = m_imageFormatCombo->currentIndex() == 0 ? ImageFormat::Raw : ImageFormat::SPW;
config.enableCompression = (config.format == ImageFormat::SPW);
m_imageCreateProgress->setVisible(true);
m_imageCreateProgress->setValue(0);
m_imageCreateBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
ImageCreator creator;
creator.createImage(config, [this](const ImageCreateProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_imageCreateProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QString info = QString("%1 MB/s").arg(speedMB, 0, 'f', 1);
if (progress.compressionRatio > 0)
info += QString(", Ratio: %1:1").arg(progress.compressionRatio, 0, 'f', 1);
QMetaObject::invokeMethod(m_imageCreateSpeedLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, info));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_imageCreateProgress->setVisible(false);
m_imageCreateBtn->setEnabled(true);
m_imageCreateSpeedLabel->clear();
QMessageBox::information(this, tr("Image Created"), tr("Disk image created successfully."));
emit statusMessage(tr("Image creation completed"));
});
thread->start();
}
void ImagingTab::onRestoreImage()
{
QString inputPath = m_restoreInputEdit->text();
int dstDiskId = m_restoreDestCombo->currentData().toInt();
if (inputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please specify an input image file."));
return;
}
auto reply = QMessageBox::warning(this, tr("Restore Image"),
tr("ALL data on Disk %1 will be OVERWRITTEN with the image contents.\n\nContinue?")
.arg(dstDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
ImageRestoreConfig config;
config.inputFilePath = inputPath.toStdWString();
config.destDiskId = dstDiskId;
config.verifyAfterRestore = m_restoreVerifyCheck->isChecked();
m_restoreProgress->setVisible(true);
m_restoreProgress->setValue(0);
m_restoreBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
ImageRestorer restorer;
restorer.restoreImage(config, [this](const ImageRestoreProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_restoreProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_restoreSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_restoreProgress->setVisible(false);
m_restoreBtn->setEnabled(true);
m_restoreSpeedLabel->clear();
QMessageBox::information(this, tr("Restore Complete"), tr("Image restoration completed."));
emit statusMessage(tr("Image restore completed"));
});
thread->start();
}
void ImagingTab::onFlashIso()
{
QString inputPath = m_flashInputEdit->text();
int targetDiskId = m_flashTargetCombo->currentData().toInt();
if (inputPath.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please specify an ISO/IMG file."));
return;
}
auto reply = QMessageBox::warning(this, tr("Flash ISO/IMG"),
tr("ALL data on the target USB drive (Disk %1) will be DESTROYED.\n\nContinue?")
.arg(targetDiskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
FlashConfig config;
config.inputFilePath = inputPath.toStdWString();
config.targetDiskId = targetDiskId;
config.verifyAfterFlash = m_flashVerifyCheck->isChecked();
m_flashProgress->setVisible(true);
m_flashProgress->setValue(0);
m_flashBtn->setEnabled(false);
auto* thread = QThread::create([this, config]() {
IsoFlasher flasher;
flasher.flash(config, [this](const FlashProgress& progress) -> bool {
int pct = static_cast<int>(progress.percentComplete);
double speedMB = progress.speedBytesPerSec / (1024.0 * 1024.0);
QMetaObject::invokeMethod(m_flashProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_flashSpeedLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString("%1 MB/s, ETA: %2s")
.arg(speedMB, 0, 'f', 1)
.arg(static_cast<int>(progress.etaSeconds))));
return true;
});
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_flashProgress->setVisible(false);
m_flashBtn->setEnabled(true);
m_flashSpeedLabel->clear();
QMessageBox::information(this, tr("Flash Complete"), tr("ISO/IMG flash completed."));
emit statusMessage(tr("Flash completed"));
});
thread->start();
}
void ImagingTab::onBrowseImageOutput()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save Image As"),
QString(),
tr("Raw Image (*.img);;SPW Compressed (*.spw);;All Files (*)"));
if (!file.isEmpty())
m_imageOutputEdit->setText(file);
}
void ImagingTab::onBrowseRestoreInput()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select Image File"),
QString(),
tr("Image Files (*.img *.spw);;All Files (*)"));
if (!file.isEmpty())
m_restoreInputEdit->setText(file);
}
void ImagingTab::onBrowseFlashInput()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select ISO/IMG File"),
QString(),
tr("Disk Images (*.iso *.img);;All Files (*)"));
if (!file.isEmpty())
m_flashInputEdit->setText(file);
}
void ImagingTab::onRestoreInputChanged()
{
QString path = m_restoreInputEdit->text();
if (path.isEmpty())
{
m_restoreImageInfo->setText(tr("No image selected"));
return;
}
// Try to read SPW image info
auto infoResult = ImageRestorer::inspectImage(path.toStdWString());
if (infoResult.isOk())
{
const auto& info = infoResult.value();
m_restoreImageInfo->setText(
QString("Model: %1\nSerial: %2\nSize: %3\nChunks: %4 (%5 sparse)\n%6")
.arg(QString::fromStdString(info.diskModel))
.arg(QString::fromStdString(info.diskSerial))
.arg(formatSize(info.imageDataSize))
.arg(info.chunkCount)
.arg(info.sparseChunkCount)
.arg(info.isCompressed ? tr("Compressed") : tr("Uncompressed")));
}
else
{
// Might be a raw image
auto fmtResult = ImageRestorer::detectFormat(path.toStdWString());
if (fmtResult.isOk() && fmtResult.value() == ImageFormat::Raw)
{
m_restoreImageInfo->setText(tr("Raw image file"));
}
else
{
m_restoreImageInfo->setText(tr("Unable to read image info"));
}
}
}
QString ImagingTab::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

@@ -1,7 +1,18 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QComboBox;
class QCheckBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
namespace spw
{
@@ -13,8 +24,64 @@ public:
explicit ImagingTab(QWidget* parent = nullptr);
~ImagingTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onCloneDisk();
void onCreateImage();
void onRestoreImage();
void onFlashIso();
void onBrowseImageOutput();
void onBrowseRestoreInput();
void onBrowseFlashInput();
void onRestoreInputChanged();
private:
void setupUi();
void populateDiskCombos();
static QString formatSize(uint64_t bytes);
// Clone section
QComboBox* m_cloneSourceCombo = nullptr;
QComboBox* m_cloneDestCombo = nullptr;
QComboBox* m_cloneModeCombo = nullptr;
QCheckBox* m_cloneVerifyCheck = nullptr;
QPushButton* m_cloneBtn = nullptr;
QProgressBar* m_cloneProgress = nullptr;
QLabel* m_cloneSpeedLabel = nullptr;
// Create Image section
QComboBox* m_imageSourceCombo = nullptr;
QLineEdit* m_imageOutputEdit = nullptr;
QComboBox* m_imageFormatCombo = nullptr;
QPushButton* m_imageCreateBtn = nullptr;
QProgressBar* m_imageCreateProgress = nullptr;
QLabel* m_imageCreateSpeedLabel = nullptr;
// Restore Image section
QLineEdit* m_restoreInputEdit = nullptr;
QLabel* m_restoreImageInfo = nullptr;
QComboBox* m_restoreDestCombo = nullptr;
QCheckBox* m_restoreVerifyCheck = nullptr;
QPushButton* m_restoreBtn = nullptr;
QProgressBar* m_restoreProgress = nullptr;
QLabel* m_restoreSpeedLabel = nullptr;
// Flash ISO section
QLineEdit* m_flashInputEdit = nullptr;
QComboBox* m_flashTargetCombo = nullptr;
QCheckBox* m_flashVerifyCheck = nullptr;
QPushButton* m_flashBtn = nullptr;
QProgressBar* m_flashProgress = nullptr;
QLabel* m_flashSpeedLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
};
} // namespace spw

View File

@@ -1,14 +1,23 @@
#include "MaintenanceTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/maintenance/SecureErase.h"
#include "core/recovery/BootRepair.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -26,52 +35,62 @@ void MaintenanceTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Secure Erase section
// ===== Secure Erase Section =====
auto* eraseGroup = new QGroupBox(tr("Secure Erase"));
auto* eraseLayout = new QGridLayout(eraseGroup);
eraseLayout->addWidget(new QLabel(tr("Target Disk:")), 0, 0);
eraseLayout->addWidget(new QComboBox(), 0, 1);
m_eraseDiskCombo = new QComboBox();
eraseLayout->addWidget(m_eraseDiskCombo, 0, 1, 1, 2);
eraseLayout->addWidget(new QLabel(tr("Erase Method:")), 1, 0);
auto* methodWidget = new QWidget();
auto* methodLayout = new QVBoxLayout(methodWidget);
methodLayout->setContentsMargins(0, 0, 0, 0);
auto* zeroPass = new QRadioButton(tr("Zero fill (1 pass) — Fast"));
zeroPass->setChecked(true);
auto* dod3Pass = new QRadioButton(tr("DoD 5220.22-M (3 passes) — Standard"));
auto* dod7Pass = new QRadioButton(tr("DoD 5220.22-M ECE (7 passes) — Enhanced"));
auto* gutmann = new QRadioButton(tr("Gutmann method (35 passes) — Maximum"));
auto* customPass = new QRadioButton(tr("Custom:"));
auto* customSpin = new QSpinBox();
customSpin->setRange(1, 99);
customSpin->setValue(3);
customSpin->setEnabled(false);
auto* customRow = new QHBoxLayout();
customRow->addWidget(customPass);
customRow->addWidget(customSpin);
customRow->addWidget(new QLabel(tr("passes")));
customRow->addStretch();
m_eraseMethodCombo = new QComboBox();
m_eraseMethodCombo->addItems({
tr("Zero Fill (1 pass) - Fast"),
tr("DoD 5220.22-M (3 passes) - Standard"),
tr("DoD 5220.22-M ECE (7 passes) - Enhanced"),
tr("Gutmann (35 passes) - Maximum"),
tr("Random Fill (N passes)"),
tr("Custom Pattern")
});
connect(m_eraseMethodCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MaintenanceTab::onEraseMethodChanged);
eraseLayout->addWidget(m_eraseMethodCombo, 1, 1, 1, 2);
methodLayout->addWidget(zeroPass);
methodLayout->addWidget(dod3Pass);
methodLayout->addWidget(dod7Pass);
methodLayout->addWidget(gutmann);
methodLayout->addLayout(customRow);
eraseLayout->addWidget(methodWidget, 1, 1);
eraseLayout->addWidget(new QLabel(tr("Custom Passes:")), 2, 0);
m_customPassSpin = new QSpinBox();
m_customPassSpin->setRange(1, 99);
m_customPassSpin->setValue(3);
m_customPassSpin->setEnabled(false);
eraseLayout->addWidget(m_customPassSpin, 2, 1);
auto* verifyCheck = new QCheckBox(tr("Verify after erase"));
verifyCheck->setChecked(true);
eraseLayout->addWidget(verifyCheck, 2, 1);
m_verifyCheck = new QCheckBox(tr("Verify after erase"));
m_verifyCheck->setChecked(true);
eraseLayout->addWidget(m_verifyCheck, 3, 1);
auto* eraseBtn = new QPushButton(tr("Secure Erase"));
eraseBtn->setObjectName("cancelButton");
eraseBtn->setToolTip(tr("WARNING: This permanently destroys all data on the selected disk!"));
eraseLayout->addWidget(eraseBtn, 3, 1, Qt::AlignRight);
m_eraseProgress = new QProgressBar();
m_eraseProgress->setVisible(false);
eraseLayout->addWidget(m_eraseProgress, 4, 0, 1, 3);
m_eraseStatusLabel = new QLabel();
eraseLayout->addWidget(m_eraseStatusLabel, 5, 0, 1, 3);
// 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; }"
"QPushButton:hover { background-color: #ee0000; }"
"QPushButton:pressed { background-color: #aa0000; }");
m_eraseBtn->setToolTip(tr("WARNING: This permanently destroys ALL data on the selected disk!"));
connect(m_eraseBtn, &QPushButton::clicked, this, &MaintenanceTab::onSecureErase);
eraseLayout->addWidget(m_eraseBtn, 6, 0, 1, 3);
layout->addWidget(eraseGroup);
// Boot Repair section
// ===== Boot Repair Section =====
auto* bootGroup = new QGroupBox(tr("Boot Repair"));
auto* bootLayout = new QVBoxLayout(bootGroup);
@@ -79,28 +98,382 @@ void MaintenanceTab::setupUi()
tr("Repair boot configuration for Windows and other operating systems."));
bootLayout->addWidget(bootInfo);
auto* mbrRepairBtn = new QPushButton(tr("Repair MBR"));
mbrRepairBtn->setToolTip(tr("Rewrite the Master Boot Record with a standard boot loader"));
auto* gptRepairBtn = new QPushButton(tr("Repair GPT"));
gptRepairBtn->setToolTip(tr("Rebuild GPT headers and verify partition entries"));
auto* bcdRepairBtn = new QPushButton(tr("Repair Windows BCD"));
bcdRepairBtn->setToolTip(tr("Rebuild the Windows Boot Configuration Data store"));
auto* bootloaderBtn = new QPushButton(tr("Reinstall Bootloader"));
bootloaderBtn->setToolTip(tr("Reinstall the bootloader to the selected disk's boot sector"));
auto* bootDiskRow = new QHBoxLayout();
bootDiskRow->addWidget(new QLabel(tr("Target Disk:")));
m_bootDiskCombo = new QComboBox();
bootDiskRow->addWidget(m_bootDiskCombo, 1);
bootLayout->addLayout(bootDiskRow);
bootLayout->addWidget(mbrRepairBtn);
bootLayout->addWidget(gptRepairBtn);
bootLayout->addWidget(bcdRepairBtn);
bootLayout->addWidget(bootloaderBtn);
auto* bootBtnLayout = new QHBoxLayout();
m_mbrRepairBtn = new QPushButton(tr("Repair MBR"));
m_mbrRepairBtn->setToolTip(tr("Rewrite the Master Boot Record with a standard boot loader"));
connect(m_mbrRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairMbr);
bootBtnLayout->addWidget(m_mbrRepairBtn);
m_gptRepairBtn = new QPushButton(tr("Repair GPT"));
m_gptRepairBtn->setToolTip(tr("Rebuild GPT headers and verify partition entries"));
connect(m_gptRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairGpt);
bootBtnLayout->addWidget(m_gptRepairBtn);
m_bcdRepairBtn = new QPushButton(tr("Repair BCD"));
m_bcdRepairBtn->setToolTip(tr("Rebuild the Windows Boot Configuration Data store"));
connect(m_bcdRepairBtn, &QPushButton::clicked, this, &MaintenanceTab::onRepairBcd);
bootBtnLayout->addWidget(m_bcdRepairBtn);
m_bootloaderBtn = new QPushButton(tr("Reinstall Bootloader"));
m_bootloaderBtn->setToolTip(tr("Reinstall the bootloader to the selected disk's boot sector"));
connect(m_bootloaderBtn, &QPushButton::clicked, this, &MaintenanceTab::onReinstallBootloader);
bootBtnLayout->addWidget(m_bootloaderBtn);
bootLayout->addLayout(bootBtnLayout);
m_bootProgress = new QProgressBar();
m_bootProgress->setVisible(false);
bootLayout->addWidget(m_bootProgress);
m_bootStatusLabel = new QLabel();
m_bootStatusLabel->setWordWrap(true);
bootLayout->addWidget(m_bootStatusLabel);
layout->addWidget(bootGroup);
// Progress
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
layout->addWidget(progressBar);
layout->addStretch();
}
void MaintenanceTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
}
void MaintenanceTab::populateDiskCombo()
{
m_eraseDiskCombo->clear();
m_bootDiskCombo->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_eraseDiskCombo->addItem(label, disk.id);
m_bootDiskCombo->addItem(label, disk.id);
}
}
void MaintenanceTab::onEraseMethodChanged()
{
int idx = m_eraseMethodCombo->currentIndex();
// Enable custom pass count for Random Fill (4) and Custom Pattern (5)
m_customPassSpin->setEnabled(idx == 4 || idx == 5);
}
void MaintenanceTab::onSecureErase()
{
int diskId = m_eraseDiskCombo->currentData().toInt();
// Find disk name for confirmation
QString diskName;
for (const auto& disk : m_snapshot.disks)
{
if (disk.id == diskId)
{
diskName = QString::fromStdWString(disk.model);
break;
}
}
// First confirmation
auto reply = QMessageBox::critical(this, tr("SECURE ERASE - CONFIRM"),
tr("You are about to PERMANENTLY DESTROY all data on:\n\n"
"Disk %1: %2\n\n"
"This action is IRREVERSIBLE.\n\n"
"Are you absolutely sure?")
.arg(diskId).arg(diskName),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
// Second confirmation: type disk name
bool ok = false;
QString typedName = QInputDialog::getText(this, tr("Final Confirmation"),
tr("Type the disk model name to confirm:\n\n%1")
.arg(diskName),
QLineEdit::Normal, QString(), &ok);
if (!ok || typedName.trimmed() != diskName.trimmed())
{
QMessageBox::information(this, tr("Cancelled"),
tr("Erase cancelled. The disk name did not match."));
return;
}
// Build erase config
EraseConfig config;
switch (m_eraseMethodCombo->currentIndex())
{
case 0: config.method = EraseMethod::ZeroFill; break;
case 1: config.method = EraseMethod::DoD_3Pass; break;
case 2: config.method = EraseMethod::DoD_7Pass; break;
case 3: config.method = EraseMethod::Gutmann; break;
case 4:
config.method = EraseMethod::RandomFill;
config.passCount = m_customPassSpin->value();
break;
case 5:
config.method = EraseMethod::CustomPattern;
config.customPatternPasses = m_customPassSpin->value();
config.customPattern = {0xAA, 0x55}; // Default alternating pattern
break;
}
config.verify = m_verifyCheck->isChecked();
m_cancelFlag.store(false);
m_eraseProgress->setVisible(true);
m_eraseProgress->setValue(0);
m_eraseBtn->setEnabled(false);
m_eraseStatusLabel->setText(tr("Erasing..."));
auto* thread = QThread::create([this, diskId, config]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to open disk: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
SecureErase erase(disk);
auto result = erase.eraseDisk(config,
[this](int currentPass, int totalPasses,
uint64_t bytesWritten, uint64_t totalBytes, double speedMBps) {
int pct = totalBytes > 0 ? static_cast<int>((bytesWritten * 100) / totalBytes) : 0;
QMetaObject::invokeMethod(m_eraseProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Pass %1/%2 - %3 MB/s")
.arg(currentPass)
.arg(totalPasses)
.arg(speedMBps, 0, 'f', 1)));
},
&m_cancelFlag);
QString resultMsg = result.isOk() ? tr("Erase completed successfully.")
: tr("Erase failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_eraseStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, resultMsg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_eraseProgress->setVisible(false);
m_eraseBtn->setEnabled(true);
emit statusMessage(tr("Secure erase completed"));
});
thread->start();
}
void MaintenanceTab::onRepairMbr()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Repair MBR"),
tr("This will rewrite the MBR boot code on Disk %1.\n"
"Partition table entries will be preserved.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing MBR..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairMbr();
QString msg = result.isOk() ? tr("MBR repaired successfully.")
: tr("MBR repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("MBR repair completed"));
});
thread->start();
}
void MaintenanceTab::onRepairGpt()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Repair GPT"),
tr("This will rebuild GPT headers on Disk %1.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing GPT..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairGpt(true); // Rebuild primary from backup
QString msg = result.isOk() ? tr("GPT repaired successfully.")
: tr("GPT repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("GPT repair completed"));
});
thread->start();
}
void MaintenanceTab::onRepairBcd()
{
int diskId = m_bootDiskCombo->currentData().toInt();
Q_UNUSED(diskId);
auto reply = QMessageBox::warning(this, tr("Repair BCD"),
tr("This will rebuild the Windows Boot Configuration Data.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Repairing BCD..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairBcd(L'S'); // Assume S: is the ESP
QString msg = result.isOk() ? tr("BCD repaired successfully.")
: tr("BCD repair failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("BCD repair completed"));
});
thread->start();
}
void MaintenanceTab::onReinstallBootloader()
{
int diskId = m_bootDiskCombo->currentData().toInt();
auto reply = QMessageBox::warning(this, tr("Reinstall Bootloader"),
tr("This will reinstall the Windows bootloader on Disk %1.\n\nContinue?")
.arg(diskId),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
m_bootProgress->setVisible(true);
m_bootProgress->setRange(0, 0);
m_bootStatusLabel->setText(tr("Reinstalling bootloader..."));
auto* thread = QThread::create([this, diskId]() {
auto diskResult = RawDiskHandle::open(diskId, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
auto result = repair.repairBootloader(L'S', L'C');
QString msg = result.isOk() ? tr("Bootloader reinstalled successfully.")
: tr("Bootloader reinstall failed: %1")
.arg(QString::fromStdString(result.error().message));
QMetaObject::invokeMethod(m_bootStatusLabel, "setText",
Qt::QueuedConnection, Q_ARG(QString, msg));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootProgress->setVisible(false);
emit statusMessage(tr("Bootloader reinstall completed"));
});
thread->start();
}
QString MaintenanceTab::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

@@ -1,6 +1,20 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/maintenance/SecureErase.h"
#include <QWidget>
#include <atomic>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QSpinBox;
namespace spw
{
@@ -13,8 +27,47 @@ public:
explicit MaintenanceTab(QWidget* parent = nullptr);
~MaintenanceTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onSecureErase();
void onEraseMethodChanged();
void onRepairMbr();
void onRepairGpt();
void onRepairBcd();
void onReinstallBootloader();
private:
void setupUi();
void populateDiskCombo();
static QString formatSize(uint64_t bytes);
// Secure Erase
QComboBox* m_eraseDiskCombo = nullptr;
QComboBox* m_eraseMethodCombo = nullptr;
QSpinBox* m_customPassSpin = nullptr;
QCheckBox* m_verifyCheck = nullptr;
QPushButton* m_eraseBtn = nullptr;
QProgressBar* m_eraseProgress = nullptr;
QLabel* m_eraseStatusLabel = nullptr;
// Boot Repair
QComboBox* m_bootDiskCombo = nullptr;
QPushButton* m_mbrRepairBtn = nullptr;
QPushButton* m_gptRepairBtn = nullptr;
QPushButton* m_bcdRepairBtn = nullptr;
QPushButton* m_bootloaderBtn = nullptr;
QProgressBar* m_bootProgress = nullptr;
QLabel* m_bootStatusLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,14 +1,27 @@
#include "RecoveryTab.h"
#include "core/disk/DiskEnumerator.h"
#include "core/disk/RawDiskHandle.h"
#include "core/recovery/PartitionRecovery.h"
#include "core/recovery/FileRecovery.h"
#include "core/recovery/BootRepair.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QListWidget>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSplitter>
#include <QStackedWidget>
#include <QTableWidget>
#include <QThread>
#include <QVBoxLayout>
namespace spw
@@ -27,65 +40,664 @@ void RecoveryTab::setupUi()
auto* layout = new QHBoxLayout(this);
auto* splitter = new QSplitter(Qt::Horizontal);
// Left: recovery options
// Left: options panel
auto* optionsPanel = new QWidget();
auto* optLayout = new QVBoxLayout(optionsPanel);
auto* typeGroup = new QGroupBox(tr("Recovery Type"));
auto* typeLayout = new QVBoxLayout(typeGroup);
auto* partRecoveryBtn = new QPushButton(tr("Partition Recovery"));
partRecoveryBtn->setToolTip(tr("Scan for lost or deleted partitions"));
auto* fileRecoveryBtn = new QPushButton(tr("File Recovery"));
fileRecoveryBtn->setToolTip(tr("Recover files from damaged or formatted drives"));
auto* mbrRepairBtn = new QPushButton(tr("MBR/GPT Repair"));
mbrRepairBtn->setToolTip(tr("Rebuild partition table from filesystem superblocks"));
typeLayout->addWidget(partRecoveryBtn);
typeLayout->addWidget(fileRecoveryBtn);
typeLayout->addWidget(mbrRepairBtn);
optLayout->addWidget(typeGroup);
// Target disk selector
auto* targetGroup = new QGroupBox(tr("Target Disk"));
auto* targetLayout = new QVBoxLayout(targetGroup);
auto* diskCombo = new QComboBox();
diskCombo->addItem(tr("Select a disk..."));
targetLayout->addWidget(diskCombo);
m_diskCombo = new QComboBox();
targetLayout->addWidget(m_diskCombo);
optLayout->addWidget(targetGroup);
auto* scanBtn = new QPushButton(tr("Start Scan"));
scanBtn->setObjectName("applyButton");
optLayout->addWidget(scanBtn);
// Recovery type buttons
auto* typeGroup = new QGroupBox(tr("Recovery Type"));
auto* typeLayout = new QVBoxLayout(typeGroup);
m_partRecoveryBtn = new QPushButton(tr("Partition Recovery"));
m_partRecoveryBtn->setCheckable(true);
m_partRecoveryBtn->setChecked(true);
m_fileRecoveryBtn = new QPushButton(tr("File Recovery"));
m_fileRecoveryBtn->setCheckable(true);
m_bootRepairBtn = new QPushButton(tr("Boot Repair"));
m_bootRepairBtn->setCheckable(true);
auto* progressBar = new QProgressBar();
progressBar->setVisible(false);
optLayout->addWidget(progressBar);
typeLayout->addWidget(m_partRecoveryBtn);
typeLayout->addWidget(m_fileRecoveryBtn);
typeLayout->addWidget(m_bootRepairBtn);
optLayout->addWidget(typeGroup);
connect(m_partRecoveryBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
connect(m_fileRecoveryBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
connect(m_bootRepairBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoveryTypeChanged);
optLayout->addStretch();
splitter->addWidget(optionsPanel);
// Right: results
auto* resultsPanel = new QWidget();
auto* resLayout = new QVBoxLayout(resultsPanel);
// Right: stacked pages
m_stackedWidget = new QStackedWidget();
auto* resLabel = new QLabel(tr("Recovery Results"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
resLayout->addWidget(resLabel);
setupPartitionRecoveryPage();
setupFileRecoveryPage();
setupBootRepairPage();
auto* resultsTable = new QTableWidget(0, 5);
resultsTable->setHorizontalHeaderLabels(
{tr("Type"), tr("Name/Label"), tr("Size"), tr("Filesystem"), tr("Confidence")});
resultsTable->setAlternatingRowColors(true);
resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
resLayout->addWidget(resultsTable);
auto* recoverBtn = new QPushButton(tr("Recover Selected"));
recoverBtn->setObjectName("applyButton");
resLayout->addWidget(recoverBtn);
splitter->addWidget(resultsPanel);
splitter->addWidget(m_stackedWidget);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 2);
splitter->setStretchFactor(1, 3);
layout->addWidget(splitter);
}
void RecoveryTab::setupPartitionRecoveryPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* scanGroup = new QGroupBox(tr("Scan Options"));
auto* scanLayout = new QVBoxLayout(scanGroup);
m_quickScanRadio = new QRadioButton(tr("Quick Scan (1 MiB boundaries)"));
m_quickScanRadio->setChecked(true);
m_deepScanRadio = new QRadioButton(tr("Deep Scan (every sector)"));
scanLayout->addWidget(m_quickScanRadio);
scanLayout->addWidget(m_deepScanRadio);
m_partScanBtn = new QPushButton(tr("Start Scan"));
m_partScanBtn->setObjectName("applyButton");
scanLayout->addWidget(m_partScanBtn);
connect(m_partScanBtn, &QPushButton::clicked, this, &RecoveryTab::onStartPartitionScan);
m_partScanProgress = new QProgressBar();
m_partScanProgress->setVisible(false);
scanLayout->addWidget(m_partScanProgress);
layout->addWidget(scanGroup);
// Results table
auto* resLabel = new QLabel(tr("Recovered Partitions"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
layout->addWidget(resLabel);
m_partResultsTable = new QTableWidget(0, 6);
m_partResultsTable->setHorizontalHeaderLabels(
{tr("Start LBA"), tr("Size"), tr("Filesystem"), tr("Label"), tr("Confidence"), tr("Overlaps")});
m_partResultsTable->setAlternatingRowColors(true);
m_partResultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_partResultsTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_partResultsTable->horizontalHeader()->setStretchLastSection(true);
layout->addWidget(m_partResultsTable);
m_recoverPartBtn = new QPushButton(tr("Recover Selected"));
m_recoverPartBtn->setObjectName("applyButton");
m_recoverPartBtn->setEnabled(false);
connect(m_recoverPartBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoverSelectedPartitions);
layout->addWidget(m_recoverPartBtn);
m_stackedWidget->addWidget(page);
}
void RecoveryTab::setupFileRecoveryPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* optGroup = new QGroupBox(tr("File Recovery Options"));
auto* optLayout = new QVBoxLayout(optGroup);
auto* partRow = new QHBoxLayout();
partRow->addWidget(new QLabel(tr("Partition:")));
m_partitionCombo = new QComboBox();
partRow->addWidget(m_partitionCombo, 1);
optLayout->addLayout(partRow);
auto* typeRow = new QHBoxLayout();
typeRow->addWidget(new QLabel(tr("File Types:")));
m_fileTypeFilter = new QComboBox();
m_fileTypeFilter->addItems({tr("All Files"), tr("Images (JPG, PNG, BMP, GIF)"),
tr("Documents (PDF, DOC, XLS)"), tr("Archives (ZIP, RAR, 7Z)"),
tr("Media (MP3, MP4, AVI)")});
typeRow->addWidget(m_fileTypeFilter, 1);
optLayout->addLayout(typeRow);
m_fileScanBtn = new QPushButton(tr("Scan for Files"));
m_fileScanBtn->setObjectName("applyButton");
connect(m_fileScanBtn, &QPushButton::clicked, this, &RecoveryTab::onStartFileRecoveryScan);
optLayout->addWidget(m_fileScanBtn);
m_fileScanProgress = new QProgressBar();
m_fileScanProgress->setVisible(false);
optLayout->addWidget(m_fileScanProgress);
layout->addWidget(optGroup);
// Results
auto* resLabel = new QLabel(tr("Recoverable Files"));
resLabel->setStyleSheet("font-weight: bold; padding: 4px;");
layout->addWidget(resLabel);
m_fileResultsTable = new QTableWidget(0, 5);
m_fileResultsTable->setHorizontalHeaderLabels(
{tr("Filename"), tr("Size"), tr("Type"), tr("Confidence"), tr("Source FS")});
m_fileResultsTable->setAlternatingRowColors(true);
m_fileResultsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_fileResultsTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_fileResultsTable->horizontalHeader()->setStretchLastSection(true);
layout->addWidget(m_fileResultsTable);
// Output folder
auto* outRow = new QHBoxLayout();
outRow->addWidget(new QLabel(tr("Output Folder:")));
m_outputFolderEdit = new QLineEdit();
outRow->addWidget(m_outputFolderEdit, 1);
m_browseOutputBtn = new QPushButton(tr("Browse..."));
connect(m_browseOutputBtn, &QPushButton::clicked, this, &RecoveryTab::onBrowseOutputFolder);
outRow->addWidget(m_browseOutputBtn);
layout->addLayout(outRow);
m_recoverFileBtn = new QPushButton(tr("Recover Selected Files"));
m_recoverFileBtn->setObjectName("applyButton");
m_recoverFileBtn->setEnabled(false);
connect(m_recoverFileBtn, &QPushButton::clicked, this, &RecoveryTab::onRecoverSelectedFiles);
layout->addWidget(m_recoverFileBtn);
m_stackedWidget->addWidget(page);
}
void RecoveryTab::setupBootRepairPage()
{
auto* page = new QWidget();
auto* layout = new QVBoxLayout(page);
auto* optGroup = new QGroupBox(tr("Boot Repair Options"));
auto* optLayout = new QVBoxLayout(optGroup);
m_repairMbr = new QCheckBox(tr("Repair MBR boot code"));
m_repairGpt = new QCheckBox(tr("Repair GPT headers"));
m_repairBootSector = new QCheckBox(tr("Restore backup boot sector (NTFS/FAT)"));
m_repairBcd = new QCheckBox(tr("Rebuild Windows BCD store"));
m_repairBootloader = new QCheckBox(tr("Reinstall Windows bootloader"));
optLayout->addWidget(m_repairMbr);
optLayout->addWidget(m_repairGpt);
optLayout->addWidget(m_repairBootSector);
optLayout->addWidget(m_repairBcd);
optLayout->addWidget(m_repairBootloader);
layout->addWidget(optGroup);
m_bootRepairStartBtn = new QPushButton(tr("Start Repair"));
m_bootRepairStartBtn->setObjectName("applyButton");
connect(m_bootRepairStartBtn, &QPushButton::clicked, this, &RecoveryTab::onStartBootRepair);
layout->addWidget(m_bootRepairStartBtn);
m_bootRepairProgress = new QProgressBar();
m_bootRepairProgress->setVisible(false);
layout->addWidget(m_bootRepairProgress);
m_bootRepairStatus = new QLabel();
m_bootRepairStatus->setWordWrap(true);
layout->addWidget(m_bootRepairStatus);
layout->addStretch();
m_stackedWidget->addWidget(page);
}
void RecoveryTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateDiskCombo();
populatePartitionCombo();
}
void RecoveryTab::populateDiskCombo()
{
m_diskCombo->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_diskCombo->addItem(label, disk.id);
}
}
void RecoveryTab::populatePartitionCombo()
{
m_partitionCombo->clear();
for (const auto& part : m_snapshot.partitions)
{
QString label;
if (part.driveLetter != L'\0')
label = QString("(%1:) ").arg(QChar(part.driveLetter));
label += QString("Disk %1, Partition %2 - %3")
.arg(part.diskId)
.arg(part.index)
.arg(formatSize(part.sizeBytes));
m_partitionCombo->addItem(label, QVariant::fromValue(static_cast<int>(part.index)));
}
}
void RecoveryTab::onRecoveryTypeChanged()
{
auto* sender = qobject_cast<QPushButton*>(this->sender());
m_partRecoveryBtn->setChecked(sender == m_partRecoveryBtn);
m_fileRecoveryBtn->setChecked(sender == m_fileRecoveryBtn);
m_bootRepairBtn->setChecked(sender == m_bootRepairBtn);
if (sender == m_partRecoveryBtn)
m_stackedWidget->setCurrentIndex(0);
else if (sender == m_fileRecoveryBtn)
m_stackedWidget->setCurrentIndex(1);
else if (sender == m_bootRepairBtn)
m_stackedWidget->setCurrentIndex(2);
}
void RecoveryTab::onStartPartitionScan()
{
int diskIdx = m_diskCombo->currentData().toInt();
if (diskIdx < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a target disk."));
return;
}
PartitionScanMode mode = m_quickScanRadio->isChecked()
? PartitionScanMode::Quick
: PartitionScanMode::Deep;
m_cancelFlag.store(false);
m_partScanProgress->setVisible(true);
m_partScanProgress->setValue(0);
m_partScanBtn->setEnabled(false);
m_partResultsTable->setRowCount(0);
auto* thread = QThread::create([this, diskIdx, mode]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
PartitionRecovery recovery(disk);
auto scanResult = recovery.scan(mode,
[this](uint64_t scanned, uint64_t total, size_t /*found*/) {
if (total > 0)
{
int pct = static_cast<int>((scanned * 100) / total);
QMetaObject::invokeMethod(m_partScanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
}
},
&m_cancelFlag);
if (scanResult.isOk())
{
m_recoveredPartitions = scanResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_partScanProgress->setVisible(false);
m_partScanBtn->setEnabled(true);
m_partResultsTable->setRowCount(0);
for (size_t i = 0; i < m_recoveredPartitions.size(); ++i)
{
const auto& rp = m_recoveredPartitions[i];
int row = m_partResultsTable->rowCount();
m_partResultsTable->insertRow(row);
m_partResultsTable->setItem(row, 0, new QTableWidgetItem(
QString::number(rp.startLba)));
m_partResultsTable->setItem(row, 1, new QTableWidgetItem(
formatSize(rp.sectorCount * rp.sectorSize)));
m_partResultsTable->setItem(row, 2, new QTableWidgetItem(
FilesystemDetector::filesystemName(rp.fsType)));
m_partResultsTable->setItem(row, 3, new QTableWidgetItem(
QString::fromStdString(rp.label)));
m_partResultsTable->setItem(row, 4, new QTableWidgetItem(
QString("%1%").arg(rp.confidence, 0, 'f', 1)));
m_partResultsTable->setItem(row, 5, new QTableWidgetItem(
rp.overlapsExisting ? tr("Yes") : tr("No")));
}
m_recoverPartBtn->setEnabled(!m_recoveredPartitions.empty());
emit statusMessage(tr("Found %1 partition(s)").arg(m_recoveredPartitions.size()));
});
thread->start();
}
void RecoveryTab::onStartFileRecoveryScan()
{
int diskIdx = m_diskCombo->currentData().toInt();
int partIdx = m_partitionCombo->currentData().toInt();
if (diskIdx < 0 || partIdx < 0)
{
QMessageBox::warning(this, tr("Selection Required"),
tr("Please select a disk and partition."));
return;
}
// Find partition info
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
m_cancelFlag.store(false);
m_fileScanProgress->setVisible(true);
m_fileScanProgress->setValue(0);
m_fileScanBtn->setEnabled(false);
m_fileResultsTable->setRowCount(0);
SectorOffset startLba = partInfo->offsetBytes / partInfo->sizeBytes > 0 ? partInfo->offsetBytes / 512 : 0;
SectorCount sectors = partInfo->sizeBytes / 512;
FilesystemType fsType = partInfo->filesystemType;
auto* thread = QThread::create([this, diskIdx, startLba, sectors, fsType]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
FileRecovery recovery(disk, startLba, sectors, fsType);
auto scanResult = recovery.scan(FileRecoveryMode::Both,
[this](uint64_t scanned, uint64_t total, size_t /*found*/) {
if (total > 0)
{
int pct = static_cast<int>((scanned * 100) / total);
QMetaObject::invokeMethod(m_fileScanProgress, "setValue",
Qt::QueuedConnection, Q_ARG(int, pct));
}
},
&m_cancelFlag);
if (scanResult.isOk())
{
m_recoveredFiles = scanResult.value();
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_fileScanProgress->setVisible(false);
m_fileScanBtn->setEnabled(true);
m_fileResultsTable->setRowCount(0);
for (size_t i = 0; i < m_recoveredFiles.size(); ++i)
{
const auto& rf = m_recoveredFiles[i];
int row = m_fileResultsTable->rowCount();
m_fileResultsTable->insertRow(row);
m_fileResultsTable->setItem(row, 0, new QTableWidgetItem(
QString::fromStdString(rf.filename)));
m_fileResultsTable->setItem(row, 1, new QTableWidgetItem(
formatSize(rf.sizeBytes)));
m_fileResultsTable->setItem(row, 2, new QTableWidgetItem(
QString::fromStdString(rf.extension)));
m_fileResultsTable->setItem(row, 3, new QTableWidgetItem(
QString("%1%").arg(rf.confidence, 0, 'f', 1)));
m_fileResultsTable->setItem(row, 4, new QTableWidgetItem(
FilesystemDetector::filesystemName(rf.sourceFs)));
}
m_recoverFileBtn->setEnabled(!m_recoveredFiles.empty());
emit statusMessage(tr("Found %1 recoverable file(s)").arg(m_recoveredFiles.size()));
});
thread->start();
}
void RecoveryTab::onRecoverSelectedPartitions()
{
int diskIdx = m_diskCombo->currentData().toInt();
auto selected = m_partResultsTable->selectionModel()->selectedRows();
if (selected.isEmpty())
{
QMessageBox::information(this, tr("No Selection"), tr("Please select partitions to recover."));
return;
}
auto reply = QMessageBox::warning(this, tr("Recover Partitions"),
tr("This will modify the partition table on Disk %1.\nContinue?").arg(diskIdx),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
auto* thread = QThread::create([this, diskIdx, selected]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadWrite);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
PartitionRecovery recovery(disk);
for (const auto& idx : selected)
{
int row = idx.row();
if (row >= 0 && row < static_cast<int>(m_recoveredPartitions.size()))
{
recovery.recover(m_recoveredPartitions[row]);
}
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
QMessageBox::information(this, tr("Recovery Complete"),
tr("Partition recovery completed. Refreshing disk list..."));
emit statusMessage(tr("Partition recovery completed"));
});
thread->start();
}
void RecoveryTab::onRecoverSelectedFiles()
{
QString outputDir = m_outputFolderEdit->text();
if (outputDir.isEmpty())
{
QMessageBox::warning(this, tr("No Output"), tr("Please select an output folder."));
return;
}
auto selected = m_fileResultsTable->selectionModel()->selectedRows();
if (selected.isEmpty())
{
QMessageBox::information(this, tr("No Selection"), tr("Please select files to recover."));
return;
}
int diskIdx = m_diskCombo->currentData().toInt();
int partIdx = m_partitionCombo->currentData().toInt();
const PartitionInfo* partInfo = nullptr;
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx && p.index == partIdx)
{
partInfo = &p;
break;
}
}
if (!partInfo)
return;
SectorOffset startLba = partInfo->offsetBytes / 512;
SectorCount sectors = partInfo->sizeBytes / 512;
FilesystemType fsType = partInfo->filesystemType;
auto filesToRecover = selected;
auto outputPath = outputDir.toStdString();
auto* thread = QThread::create([this, diskIdx, startLba, sectors, fsType, filesToRecover, outputPath]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadOnly);
if (diskResult.isError())
return;
auto& disk = diskResult.value();
FileRecovery recovery(disk, startLba, sectors, fsType);
for (const auto& idx : filesToRecover)
{
int row = idx.row();
if (row >= 0 && row < static_cast<int>(m_recoveredFiles.size()))
{
std::string filePath = outputPath + "/" + m_recoveredFiles[row].filename;
recovery.recoverFile(m_recoveredFiles[row], filePath);
}
}
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
QMessageBox::information(this, tr("File Recovery Complete"),
tr("File recovery completed."));
emit statusMessage(tr("File recovery completed"));
});
thread->start();
}
void RecoveryTab::onStartBootRepair()
{
int diskIdx = m_diskCombo->currentData().toInt();
if (diskIdx < 0)
{
QMessageBox::warning(this, tr("No Disk"), tr("Please select a target disk."));
return;
}
if (!m_repairMbr->isChecked() && !m_repairGpt->isChecked() &&
!m_repairBootSector->isChecked() && !m_repairBcd->isChecked() &&
!m_repairBootloader->isChecked())
{
QMessageBox::warning(this, tr("No Options"), tr("Please select at least one repair option."));
return;
}
auto reply = QMessageBox::warning(this, tr("Boot Repair"),
tr("Boot repair will modify critical disk structures.\n"
"Incorrect use can render a system unbootable.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
bool doMbr = m_repairMbr->isChecked();
bool doGpt = m_repairGpt->isChecked();
bool doBoot = m_repairBootSector->isChecked();
bool doBcd = m_repairBcd->isChecked();
bool doBootloader = m_repairBootloader->isChecked();
m_bootRepairProgress->setVisible(true);
m_bootRepairProgress->setRange(0, 0); // Indeterminate
m_bootRepairStartBtn->setEnabled(false);
m_bootRepairStatus->setText(tr("Repairing..."));
auto* thread = QThread::create([this, diskIdx, doMbr, doGpt, doBoot, doBcd, doBootloader]() {
auto diskResult = RawDiskHandle::open(diskIdx, DiskAccessMode::ReadWrite);
if (diskResult.isError())
{
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection,
Q_ARG(QString, tr("Failed to open disk: %1")
.arg(QString::fromStdString(diskResult.error().message))));
return;
}
auto& disk = diskResult.value();
BootRepair repair(disk);
QStringList results;
BootRepairProgress progress = [this](const std::string& step, int idx, int total) {
Q_UNUSED(idx);
Q_UNUSED(total);
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection,
Q_ARG(QString, QString::fromStdString(step)));
};
if (doMbr)
{
auto r = repair.repairMbr(progress);
results << (r.isOk() ? tr("MBR: Repaired") : tr("MBR: Failed"));
}
if (doGpt)
{
auto r = repair.repairGpt(true, progress);
results << (r.isOk() ? tr("GPT: Repaired") : tr("GPT: Failed"));
}
if (doBoot)
{
// Find first NTFS/FAT partition
for (const auto& p : m_snapshot.partitions)
{
if (p.diskId == diskIdx &&
(p.filesystemType == FilesystemType::NTFS || p.filesystemType == FilesystemType::FAT32))
{
SectorOffset startLba = p.offsetBytes / 512;
SectorCount sectors = p.sizeBytes / 512;
auto r = repair.repairBootSector(startLba, sectors, progress);
results << (r.isOk() ? tr("Boot Sector: Repaired") : tr("Boot Sector: Failed"));
break;
}
}
}
if (doBcd)
{
auto r = repair.repairBcd(L'S', progress); // Assume S: is ESP
results << (r.isOk() ? tr("BCD: Repaired") : tr("BCD: Failed"));
}
if (doBootloader)
{
auto r = repair.repairBootloader(L'S', L'C', progress);
results << (r.isOk() ? tr("Bootloader: Repaired") : tr("Bootloader: Failed"));
}
QString summary = results.join("\n");
QMetaObject::invokeMethod(m_bootRepairStatus, "setText",
Qt::QueuedConnection, Q_ARG(QString, summary));
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this]() {
m_bootRepairProgress->setVisible(false);
m_bootRepairStartBtn->setEnabled(true);
emit statusMessage(tr("Boot repair completed"));
});
thread->start();
}
void RecoveryTab::onBrowseOutputFolder()
{
QString dir = QFileDialog::getExistingDirectory(this, tr("Select Output Folder"));
if (!dir.isEmpty())
m_outputFolderEdit->setText(dir);
}
QString RecoveryTab::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

@@ -1,7 +1,23 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include "core/recovery/PartitionRecovery.h"
#include "core/recovery/FileRecovery.h"
#include <QWidget>
class QComboBox;
class QGroupBox;
class QLabel;
class QProgressBar;
class QPushButton;
class QRadioButton;
class QStackedWidget;
class QTableWidget;
class QCheckBox;
class QLineEdit;
namespace spw
{
@@ -13,8 +29,75 @@ public:
explicit RecoveryTab(QWidget* parent = nullptr);
~RecoveryTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
void onRecoveryTypeChanged();
void onStartPartitionScan();
void onStartFileRecoveryScan();
void onRecoverSelectedPartitions();
void onRecoverSelectedFiles();
void onStartBootRepair();
void onBrowseOutputFolder();
private:
void setupUi();
void setupPartitionRecoveryPage();
void setupFileRecoveryPage();
void setupBootRepairPage();
void populateDiskCombo();
void populatePartitionCombo();
static QString formatSize(uint64_t bytes);
// UI elements
QComboBox* m_diskCombo = nullptr;
// Recovery type buttons
QPushButton* m_partRecoveryBtn = nullptr;
QPushButton* m_fileRecoveryBtn = nullptr;
QPushButton* m_bootRepairBtn = nullptr;
// Stacked pages
QStackedWidget* m_stackedWidget = nullptr;
// Partition Recovery page
QRadioButton* m_quickScanRadio = nullptr;
QRadioButton* m_deepScanRadio = nullptr;
QPushButton* m_partScanBtn = nullptr;
QProgressBar* m_partScanProgress = nullptr;
QTableWidget* m_partResultsTable = nullptr;
QPushButton* m_recoverPartBtn = nullptr;
// File Recovery page
QComboBox* m_partitionCombo = nullptr;
QComboBox* m_fileTypeFilter = nullptr;
QPushButton* m_fileScanBtn = nullptr;
QProgressBar* m_fileScanProgress = nullptr;
QTableWidget* m_fileResultsTable = nullptr;
QPushButton* m_recoverFileBtn = nullptr;
QLineEdit* m_outputFolderEdit = nullptr;
QPushButton* m_browseOutputBtn = nullptr;
// Boot Repair page
QCheckBox* m_repairMbr = nullptr;
QCheckBox* m_repairGpt = nullptr;
QCheckBox* m_repairBootSector = nullptr;
QCheckBox* m_repairBcd = nullptr;
QCheckBox* m_repairBootloader = nullptr;
QPushButton* m_bootRepairStartBtn = nullptr;
QProgressBar* m_bootRepairProgress = nullptr;
QLabel* m_bootRepairStatus = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
std::vector<RecoveredPartition> m_recoveredPartitions;
std::vector<RecoverableFile> m_recoveredFiles;
std::atomic<bool> m_cancelFlag{false};
};
} // namespace spw

View File

@@ -1,19 +1,31 @@
#include "SecurityTab.h"
#include "core/disk/DiskEnumerator.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QSpinBox>
#include <QTabWidget>
#include <QThread>
#include <QVBoxLayout>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <bcrypt.h>
namespace spw
{
@@ -29,126 +41,439 @@ void SecurityTab::setupUi()
{
auto* layout = new QVBoxLayout(this);
// Sub-tabs for the three security key types
auto* subTabs = new QTabWidget();
m_subTabs = new QTabWidget();
// --- FIDO2/WebAuthn Tab ---
setupFido2Tab();
setupVaultTab();
setupBootAuthTab();
layout->addWidget(m_subTabs);
}
void SecurityTab::setupFido2Tab()
{
auto* fido2Widget = new QWidget();
auto* fido2Layout = new QVBoxLayout(fido2Widget);
auto* fido2DevGroup = new QGroupBox(tr("FIDO2 Device"));
auto* fido2DevLayout = new QGridLayout(fido2DevGroup);
fido2DevLayout->addWidget(new QLabel(tr("Device:")), 0, 0);
auto* fido2DeviceCombo = new QComboBox();
fido2DeviceCombo->addItem(tr("No FIDO2 devices detected"));
fido2DevLayout->addWidget(fido2DeviceCombo, 0, 1);
auto* fido2RefreshBtn = new QPushButton(tr("Refresh"));
fido2DevLayout->addWidget(fido2RefreshBtn, 0, 2);
fido2DevLayout->addWidget(new QLabel(tr("Device Info:")), 1, 0);
fido2DevLayout->addWidget(new QLabel(tr("")), 1, 1);
fido2Layout->addWidget(fido2DevGroup);
auto* devGroup = new QGroupBox(tr("FIDO2 Device"));
auto* devLayout = new QGridLayout(devGroup);
devLayout->addWidget(new QLabel(tr("Device:")), 0, 0);
m_fido2DeviceCombo = new QComboBox();
m_fido2DeviceCombo->addItem(tr("No FIDO2 devices detected"));
devLayout->addWidget(m_fido2DeviceCombo, 0, 1);
auto* refreshBtn = new QPushButton(tr("Refresh"));
connect(refreshBtn, &QPushButton::clicked, this, &SecurityTab::onRefreshFido2Devices);
devLayout->addWidget(refreshBtn, 0, 2);
devLayout->addWidget(new QLabel(tr("Device Info:")), 1, 0);
m_fido2InfoLabel = new QLabel(tr("--"));
m_fido2InfoLabel->setWordWrap(true);
devLayout->addWidget(m_fido2InfoLabel, 1, 1, 1, 2);
fido2Layout->addWidget(devGroup);
auto* opsGroup = new QGroupBox(tr("Operations"));
auto* opsLayout = new QVBoxLayout(opsGroup);
auto* fido2OpsGroup = new QGroupBox(tr("Operations"));
auto* fido2OpsLayout = new QVBoxLayout(fido2OpsGroup);
auto* setPinBtn = new QPushButton(tr("Set/Change PIN"));
connect(setPinBtn, &QPushButton::clicked, this, &SecurityTab::onSetChangePin);
opsLayout->addWidget(setPinBtn);
auto* genCredBtn = new QPushButton(tr("Generate Credential"));
connect(genCredBtn, &QPushButton::clicked, this, &SecurityTab::onGenerateCredential);
opsLayout->addWidget(genCredBtn);
auto* listCredsBtn = new QPushButton(tr("List Resident Keys"));
connect(listCredsBtn, &QPushButton::clicked, this, &SecurityTab::onListResidentKeys);
opsLayout->addWidget(listCredsBtn);
auto* resetBtn = new QPushButton(tr("Factory Reset Device"));
resetBtn->setObjectName("cancelButton");
fido2OpsLayout->addWidget(setPinBtn);
fido2OpsLayout->addWidget(genCredBtn);
fido2OpsLayout->addWidget(listCredsBtn);
fido2OpsLayout->addWidget(resetBtn);
fido2Layout->addWidget(fido2OpsGroup);
connect(resetBtn, &QPushButton::clicked, this, &SecurityTab::onFactoryReset);
opsLayout->addWidget(resetBtn);
fido2Layout->addWidget(opsGroup);
auto* fido2KeyList = new QListWidget();
fido2Layout->addWidget(new QLabel(tr("Resident Keys:")));
fido2Layout->addWidget(fido2KeyList);
m_fido2KeyList = new QListWidget();
fido2Layout->addWidget(m_fido2KeyList);
subTabs->addTab(fido2Widget, tr("FIDO2 / WebAuthn"));
m_subTabs->addTab(fido2Widget, tr("FIDO2 / WebAuthn"));
}
// --- Encrypted Vault Tab ---
void SecurityTab::setupVaultTab()
{
auto* vaultWidget = new QWidget();
auto* vaultLayout = new QVBoxLayout(vaultWidget);
auto* vaultCreateGroup = new QGroupBox(tr("Create Encrypted Vault"));
auto* vaultCreateLayout = new QGridLayout(vaultCreateGroup);
vaultCreateLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
vaultCreateLayout->addWidget(new QComboBox(), 0, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Vault Size:")), 1, 0);
auto* vaultSize = new QSpinBox();
vaultSize->setRange(1, 999999);
vaultSize->setSuffix(" MB");
vaultSize->setValue(256);
vaultCreateLayout->addWidget(vaultSize, 1, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Encryption:")), 2, 0);
auto* encCombo = new QComboBox();
encCombo->addItems({tr("AES-256-XTS"), tr("AES-256-CBC"), tr("ChaCha20-Poly1305")});
vaultCreateLayout->addWidget(encCombo, 2, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Password:")), 3, 0);
auto* passEdit = new QLineEdit();
passEdit->setEchoMode(QLineEdit::Password);
vaultCreateLayout->addWidget(passEdit, 3, 1);
vaultCreateLayout->addWidget(new QLabel(tr("Confirm:")), 4, 0);
auto* confirmEdit = new QLineEdit();
confirmEdit->setEchoMode(QLineEdit::Password);
vaultCreateLayout->addWidget(confirmEdit, 4, 1);
auto* keyFileCheck = new QCheckBox(tr("Also require key file"));
vaultCreateLayout->addWidget(keyFileCheck, 5, 1);
vaultLayout->addWidget(vaultCreateGroup);
auto* createGroup = new QGroupBox(tr("Create Encrypted Vault"));
auto* createLayout = new QGridLayout(createGroup);
createLayout->addWidget(new QLabel(tr("Vault Path:")), 0, 0);
m_vaultPathEdit = new QLineEdit();
createLayout->addWidget(m_vaultPathEdit, 0, 1);
auto* browsePath = new QPushButton(tr("Browse..."));
connect(browsePath, &QPushButton::clicked, this, &SecurityTab::onBrowseVaultPath);
createLayout->addWidget(browsePath, 0, 2);
createLayout->addWidget(new QLabel(tr("Vault Size:")), 1, 0);
m_vaultSizeSpin = new QSpinBox();
m_vaultSizeSpin->setRange(1, 999999);
m_vaultSizeSpin->setSuffix(" MB");
m_vaultSizeSpin->setValue(256);
createLayout->addWidget(m_vaultSizeSpin, 1, 1);
createLayout->addWidget(new QLabel(tr("Encryption:")), 2, 0);
m_vaultAlgoCombo = new QComboBox();
m_vaultAlgoCombo->addItems({tr("AES-256-XTS"), tr("AES-256-CBC"), tr("ChaCha20-Poly1305")});
createLayout->addWidget(m_vaultAlgoCombo, 2, 1);
createLayout->addWidget(new QLabel(tr("Password:")), 3, 0);
m_vaultPasswordEdit = new QLineEdit();
m_vaultPasswordEdit->setEchoMode(QLineEdit::Password);
createLayout->addWidget(m_vaultPasswordEdit, 3, 1, 1, 2);
createLayout->addWidget(new QLabel(tr("Confirm:")), 4, 0);
m_vaultConfirmEdit = new QLineEdit();
m_vaultConfirmEdit->setEchoMode(QLineEdit::Password);
createLayout->addWidget(m_vaultConfirmEdit, 4, 1, 1, 2);
m_vaultKeyFileCheck = new QCheckBox(tr("Also require key file"));
createLayout->addWidget(m_vaultKeyFileCheck, 5, 1);
vaultLayout->addWidget(createGroup);
auto* createVaultBtn = new QPushButton(tr("Create Vault"));
createVaultBtn->setObjectName("applyButton");
connect(createVaultBtn, &QPushButton::clicked, this, &SecurityTab::onCreateVault);
vaultLayout->addWidget(createVaultBtn);
auto* vaultManageGroup = new QGroupBox(tr("Manage Existing Vaults"));
auto* vaultManageLayout = new QVBoxLayout(vaultManageGroup);
auto* unlockBtn = new QPushButton(tr("Unlock Vault"));
auto* lockBtn = new QPushButton(tr("Lock Vault"));
auto* changePassBtn = new QPushButton(tr("Change Password"));
vaultManageLayout->addWidget(unlockBtn);
vaultManageLayout->addWidget(lockBtn);
vaultManageLayout->addWidget(changePassBtn);
vaultLayout->addWidget(vaultManageGroup);
m_vaultProgress = new QProgressBar();
m_vaultProgress->setVisible(false);
vaultLayout->addWidget(m_vaultProgress);
// Manage existing vaults
auto* manageGroup = new QGroupBox(tr("Existing Vaults"));
auto* manageLayout = new QVBoxLayout(manageGroup);
m_vaultList = new QListWidget();
manageLayout->addWidget(m_vaultList);
auto* btnRow = new QHBoxLayout();
auto* unlockBtn = new QPushButton(tr("Unlock"));
connect(unlockBtn, &QPushButton::clicked, this, &SecurityTab::onUnlockVault);
btnRow->addWidget(unlockBtn);
auto* lockBtn = new QPushButton(tr("Lock"));
connect(lockBtn, &QPushButton::clicked, this, &SecurityTab::onLockVault);
btnRow->addWidget(lockBtn);
auto* changePwBtn = new QPushButton(tr("Change Password"));
connect(changePwBtn, &QPushButton::clicked, this, &SecurityTab::onChangeVaultPassword);
btnRow->addWidget(changePwBtn);
manageLayout->addLayout(btnRow);
vaultLayout->addWidget(manageGroup);
vaultLayout->addStretch();
subTabs->addTab(vaultWidget, tr("Encrypted Vaults"));
m_subTabs->addTab(vaultWidget, tr("Encrypted Vaults"));
}
// --- Boot Auth Key Tab ---
void SecurityTab::setupBootAuthTab()
{
auto* bootAuthWidget = new QWidget();
auto* bootAuthLayout = new QVBoxLayout(bootAuthWidget);
auto* bootAuthGroup = new QGroupBox(tr("Boot Authentication Key"));
auto* bootAuthGridLayout = new QGridLayout(bootAuthGroup);
bootAuthGridLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
bootAuthGridLayout->addWidget(new QComboBox(), 0, 1);
bootAuthGridLayout->addWidget(new QLabel(tr("Target PC:")), 1, 0);
auto* pcIdLabel = new QLabel(tr("Current machine"));
bootAuthGridLayout->addWidget(pcIdLabel, 1, 1);
bootAuthGridLayout->addWidget(new QLabel(tr("Auth Method:")), 2, 0);
auto* authMethodCombo = new QComboBox();
authMethodCombo->addItems({tr("USB presence only"), tr("USB + PIN"), tr("USB + Password")});
bootAuthGridLayout->addWidget(authMethodCombo, 2, 1);
bootAuthLayout->addWidget(bootAuthGroup);
auto* bootGroup = new QGroupBox(tr("Boot Authentication Key"));
auto* bootGridLayout = new QGridLayout(bootGroup);
auto* createBootKeyBtn = new QPushButton(tr("Create Boot Auth Key"));
createBootKeyBtn->setObjectName("applyButton");
bootAuthLayout->addWidget(createBootKeyBtn);
bootGridLayout->addWidget(new QLabel(tr("USB Drive:")), 0, 0);
m_bootAuthUsbCombo = new QComboBox();
bootGridLayout->addWidget(m_bootAuthUsbCombo, 0, 1);
auto* bootKeyInfoGroup = new QGroupBox(tr("Information"));
auto* bootKeyInfoLayout = new QVBoxLayout(bootKeyInfoGroup);
bootKeyInfoLayout->addWidget(new QLabel(
bootGridLayout->addWidget(new QLabel(tr("Target PC:")), 1, 0);
m_bootAuthPcIdLabel = new QLabel(tr("Current machine"));
bootGridLayout->addWidget(m_bootAuthPcIdLabel, 1, 1);
bootGridLayout->addWidget(new QLabel(tr("Auth Method:")), 2, 0);
m_bootAuthMethodCombo = new QComboBox();
m_bootAuthMethodCombo->addItems(
{tr("USB presence only"), tr("USB + PIN"), tr("USB + Password")});
bootGridLayout->addWidget(m_bootAuthMethodCombo, 2, 1);
bootAuthLayout->addWidget(bootGroup);
auto* createBootBtn = new QPushButton(tr("Create Boot Auth Key"));
createBootBtn->setObjectName("applyButton");
connect(createBootBtn, &QPushButton::clicked, this, &SecurityTab::onCreateBootAuthKey);
bootAuthLayout->addWidget(createBootBtn);
auto* infoGroup = new QGroupBox(tr("Information"));
auto* infoLayout = new QVBoxLayout(infoGroup);
infoLayout->addWidget(new QLabel(
tr("A boot authentication key prevents your PC from booting\n"
"unless the USB key is inserted. The key material is paired\n"
"with your machine's hardware identity.\n\n"
"Warning: Keep a backup key! Losing the USB key may lock\n"
"you out of your system.")));
bootAuthLayout->addWidget(bootKeyInfoGroup);
bootAuthLayout->addWidget(infoGroup);
bootAuthLayout->addStretch();
subTabs->addTab(bootAuthWidget, tr("Boot Authentication"));
m_subTabs->addTab(bootAuthWidget, tr("Boot Authentication"));
}
layout->addWidget(subTabs);
void SecurityTab::refreshDisks(const SystemDiskSnapshot& snapshot)
{
m_snapshot = snapshot;
populateUsbDrives();
}
void SecurityTab::populateUsbDrives()
{
m_bootAuthUsbCombo->clear();
for (const auto& disk : m_snapshot.disks)
{
if (disk.isRemovable || disk.interfaceType == DiskInterfaceType::USB)
{
QString label = QString("Disk %1: %2 (%3)")
.arg(disk.id)
.arg(QString::fromStdWString(disk.model))
.arg(formatSize(disk.sizeBytes));
m_bootAuthUsbCombo->addItem(label, disk.id);
}
}
if (m_bootAuthUsbCombo->count() == 0)
{
m_bootAuthUsbCombo->addItem(tr("No USB drives detected"));
}
}
// ===== FIDO2 Slots =====
void SecurityTab::onRefreshFido2Devices()
{
// Use Windows WebAuthn API for device enumeration
// This requires webauthn.h and webauthn.dll (Windows 10 1903+)
m_fido2DeviceCombo->clear();
m_fido2DeviceCombo->addItem(tr("FIDO2 device enumeration requires WebAuthn API"));
m_fido2InfoLabel->setText(
tr("Windows WebAuthn API is available on Windows 10 version 1903 and later.\n"
"Device enumeration will be implemented when the platform API is available.\n"
"The UI is ready for integration."));
emit statusMessage(tr("FIDO2 device enumeration not yet supported on this platform"));
}
void SecurityTab::onSetChangePin()
{
QMessageBox::information(this, tr("Set/Change PIN"),
tr("FIDO2 PIN management requires a connected authenticator device.\n"
"This feature will use the Windows WebAuthn API when available."));
}
void SecurityTab::onGenerateCredential()
{
QMessageBox::information(this, tr("Generate Credential"),
tr("Credential generation requires a connected FIDO2 authenticator.\n"
"This feature will use the Windows WebAuthn API when available."));
}
void SecurityTab::onListResidentKeys()
{
m_fido2KeyList->clear();
m_fido2KeyList->addItem(tr("Key listing requires a connected FIDO2 authenticator."));
emit statusMessage(tr("FIDO2 key listing not yet supported"));
}
void SecurityTab::onFactoryReset()
{
auto reply = QMessageBox::critical(this, tr("Factory Reset"),
tr("Factory reset will DELETE ALL credentials and keys on the device.\n\n"
"This action is IRREVERSIBLE.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QMessageBox::information(this, tr("Factory Reset"),
tr("Device factory reset requires a connected FIDO2 authenticator.\n"
"This feature will use the Windows WebAuthn API when available."));
}
// ===== Vault Slots =====
void SecurityTab::onCreateVault()
{
QString path = m_vaultPathEdit->text();
if (path.isEmpty())
{
QMessageBox::warning(this, tr("No Path"), tr("Please specify a vault file path."));
return;
}
QString password = m_vaultPasswordEdit->text();
QString confirm = m_vaultConfirmEdit->text();
if (password.isEmpty())
{
QMessageBox::warning(this, tr("No Password"), tr("Please enter a password."));
return;
}
if (password != confirm)
{
QMessageBox::warning(this, tr("Mismatch"), tr("Passwords do not match."));
return;
}
if (password.length() < 8)
{
QMessageBox::warning(this, tr("Weak Password"),
tr("Password must be at least 8 characters."));
return;
}
uint64_t vaultSizeBytes = static_cast<uint64_t>(m_vaultSizeSpin->value()) * 1024ULL * 1024ULL;
int algoIdx = m_vaultAlgoCombo->currentIndex();
Q_UNUSED(algoIdx);
m_vaultProgress->setVisible(true);
m_vaultProgress->setRange(0, 0); // Indeterminate
// Create vault using BCrypt for key derivation
auto vaultPath = path.toStdWString();
auto pw = password.toStdString();
auto* thread = QThread::create([this, vaultPath, vaultSizeBytes, pw]() {
// Derive key using BCryptGenerateSymmetricKey
// Step 1: Create empty vault file
HANDLE hFile = CreateFileW(vaultPath.c_str(), GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return;
// Write vault header with encrypted metadata
// Generate a random salt using BCryptGenRandom
uint8_t salt[32] = {};
uint8_t iv[16] = {};
BCryptGenRandom(nullptr, salt, sizeof(salt), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
BCryptGenRandom(nullptr, iv, sizeof(iv), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
// Write header: magic + salt + IV + encrypted zero-filled data
const char magic[] = "SPWVAULT01";
DWORD written = 0;
WriteFile(hFile, magic, 10, &written, nullptr);
WriteFile(hFile, salt, sizeof(salt), &written, nullptr);
WriteFile(hFile, iv, sizeof(iv), &written, nullptr);
// Write vault size
WriteFile(hFile, &vaultSizeBytes, sizeof(vaultSizeBytes), &written, nullptr);
// Extend file to vault size (zero-filled represents empty encrypted space)
LARGE_INTEGER liSize;
liSize.QuadPart = static_cast<LONGLONG>(vaultSizeBytes + 1024);
SetFilePointerEx(hFile, liSize, nullptr, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFile);
});
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
connect(thread, &QThread::finished, this, [this, path]() {
m_vaultProgress->setVisible(false);
m_vaultList->addItem(path);
m_vaultPasswordEdit->clear();
m_vaultConfirmEdit->clear();
QMessageBox::information(this, tr("Vault Created"),
tr("Encrypted vault created successfully at:\n%1").arg(path));
emit statusMessage(tr("Vault created: %1").arg(path));
});
thread->start();
}
void SecurityTab::onUnlockVault()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault to unlock."));
return;
}
bool ok = false;
QString password = QInputDialog::getText(this, tr("Unlock Vault"),
tr("Enter vault password:"),
QLineEdit::Password, QString(), &ok);
if (!ok || password.isEmpty())
return;
// Vault unlock: read header, derive key, attempt decryption
QMessageBox::information(this, tr("Vault Unlock"),
tr("Vault unlocking and mounting is not yet fully implemented.\n"
"The vault file format and encryption are ready."));
}
void SecurityTab::onLockVault()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault to lock."));
return;
}
QMessageBox::information(this, tr("Vault Lock"), tr("Vault locking is not yet fully implemented."));
}
void SecurityTab::onChangeVaultPassword()
{
auto* item = m_vaultList->currentItem();
if (!item)
{
QMessageBox::information(this, tr("No Selection"), tr("Please select a vault."));
return;
}
QMessageBox::information(this, tr("Change Password"),
tr("Vault password change is not yet fully implemented.\n"
"The re-encryption flow will be available in a future update."));
}
void SecurityTab::onBrowseVaultPath()
{
QString file = QFileDialog::getSaveFileName(this, tr("Create Vault File"),
QString(),
tr("SPW Vault (*.spwvault);;All Files (*)"));
if (!file.isEmpty())
m_vaultPathEdit->setText(file);
}
void SecurityTab::onCreateBootAuthKey()
{
if (m_bootAuthUsbCombo->currentData().isNull())
{
QMessageBox::warning(this, tr("No USB"), tr("Please insert a USB drive."));
return;
}
auto reply = QMessageBox::warning(this, tr("Create Boot Auth Key"),
tr("This will write authentication data to the selected USB drive.\n"
"The drive will be formatted.\n\nContinue?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes)
return;
QMessageBox::information(this, tr("Boot Auth Key"),
tr("Boot authentication key creation is not yet fully implemented.\n"
"This feature requires integration with the UEFI boot process."));
}
QString SecurityTab::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

@@ -1,7 +1,20 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QListWidget;
class QProgressBar;
class QPushButton;
class QSpinBox;
class QTabWidget;
namespace spw
{
@@ -13,8 +26,63 @@ public:
explicit SecurityTab(QWidget* parent = nullptr);
~SecurityTab() override;
public slots:
void refreshDisks(const SystemDiskSnapshot& snapshot);
signals:
void statusMessage(const QString& msg);
private slots:
// FIDO2
void onRefreshFido2Devices();
void onSetChangePin();
void onGenerateCredential();
void onListResidentKeys();
void onFactoryReset();
// Vaults
void onCreateVault();
void onUnlockVault();
void onLockVault();
void onChangeVaultPassword();
void onBrowseVaultPath();
// Boot Auth
void onCreateBootAuthKey();
private:
void setupUi();
void setupFido2Tab();
void setupVaultTab();
void setupBootAuthTab();
void populateUsbDrives();
static QString formatSize(uint64_t bytes);
QTabWidget* m_subTabs = nullptr;
// FIDO2 tab
QComboBox* m_fido2DeviceCombo = nullptr;
QLabel* m_fido2InfoLabel = nullptr;
QListWidget* m_fido2KeyList = nullptr;
// Vault tab
QLineEdit* m_vaultPathEdit = nullptr;
QSpinBox* m_vaultSizeSpin = nullptr;
QComboBox* m_vaultAlgoCombo = nullptr;
QLineEdit* m_vaultPasswordEdit = nullptr;
QLineEdit* m_vaultConfirmEdit = nullptr;
QCheckBox* m_vaultKeyFileCheck = nullptr;
QListWidget* m_vaultList = nullptr;
QProgressBar* m_vaultProgress = nullptr;
// Boot Auth tab
QComboBox* m_bootAuthUsbCombo = nullptr;
QComboBox* m_bootAuthMethodCombo = nullptr;
QLabel* m_bootAuthPcIdLabel = nullptr;
// Data
SystemDiskSnapshot m_snapshot;
};
} // namespace spw

View File

@@ -0,0 +1,342 @@
#include "DiskMapWidget.h"
#include <QMouseEvent>
#include <QPainter>
#include <QToolTip>
#include <algorithm>
namespace spw
{
DiskMapWidget::DiskMapWidget(QWidget* parent)
: QWidget(parent)
{
setMouseTracking(true);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setMinimumHeight(80);
setFixedHeight(100);
}
void DiskMapWidget::setDisk(const DiskInfo& disk,
const std::vector<PartitionInfo>& partitions,
const std::vector<VolumeInfo>& volumes)
{
m_disk = disk;
m_partitions = partitions;
m_volumes = volumes;
m_hoveredBlock = -1;
m_selectedBlock = -1;
rebuildBlocks();
update();
}
void DiskMapWidget::clear()
{
m_disk = {};
m_partitions.clear();
m_volumes.clear();
m_blocks.clear();
m_blockRects.clear();
m_hoveredBlock = -1;
m_selectedBlock = -1;
update();
}
void DiskMapWidget::rebuildBlocks()
{
m_blocks.clear();
if (m_disk.sizeBytes == 0)
return;
// Sort partitions by offset
auto sorted = m_partitions;
std::sort(sorted.begin(), sorted.end(),
[](const PartitionInfo& a, const PartitionInfo& b) {
return a.offsetBytes < b.offsetBytes;
});
uint64_t currentOffset = 0;
for (size_t i = 0; i < sorted.size(); ++i)
{
const auto& p = sorted[i];
// Unallocated gap before this partition
if (p.offsetBytes > currentOffset)
{
Block gap;
gap.startBytes = currentOffset;
gap.sizeBytes = p.offsetBytes - currentOffset;
gap.fsType = FilesystemType::Unallocated;
gap.label = QStringLiteral("Unallocated");
gap.color = QColor(80, 80, 80);
m_blocks.push_back(gap);
}
Block blk;
blk.partitionIndex = p.index;
blk.startBytes = p.offsetBytes;
blk.sizeBytes = p.sizeBytes;
blk.fsType = p.filesystemType;
blk.driveLetter = p.driveLetter;
blk.color = colorForFilesystem(p.filesystemType);
// Try to find volume label
if (!p.label.empty())
{
blk.label = QString::fromStdWString(p.label);
}
else if (p.driveLetter != L'\0')
{
blk.label = QString("%1:").arg(QChar(p.driveLetter));
}
else
{
blk.label = filesystemShortName(p.filesystemType);
}
m_blocks.push_back(blk);
currentOffset = p.offsetBytes + p.sizeBytes;
}
// Trailing unallocated space
if (currentOffset < m_disk.sizeBytes)
{
Block gap;
gap.startBytes = currentOffset;
gap.sizeBytes = m_disk.sizeBytes - currentOffset;
gap.fsType = FilesystemType::Unallocated;
gap.label = QStringLiteral("Unallocated");
gap.color = QColor(80, 80, 80);
m_blocks.push_back(gap);
}
}
void DiskMapWidget::paintEvent(QPaintEvent* /*event*/)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const int margin = 4;
const QRect drawArea = rect().adjusted(margin, margin, -margin, -margin);
if (m_blocks.empty() || m_disk.sizeBytes == 0)
{
painter.setPen(QColor(100, 100, 100));
painter.drawText(drawArea, Qt::AlignCenter, tr("No disk selected"));
return;
}
m_blockRects.resize(m_blocks.size());
// Calculate block widths proportional to size, with minimum width
const int totalWidth = drawArea.width();
const int minBlockWidth = 40;
const double totalBytes = static_cast<double>(m_disk.sizeBytes);
// First pass: calculate raw widths
std::vector<int> widths(m_blocks.size());
int usedWidth = 0;
for (size_t i = 0; i < m_blocks.size(); ++i)
{
double frac = static_cast<double>(m_blocks[i].sizeBytes) / totalBytes;
widths[i] = std::max(minBlockWidth, static_cast<int>(frac * totalWidth));
usedWidth += widths[i];
}
// Scale to fit
if (usedWidth > totalWidth && !m_blocks.empty())
{
double scale = static_cast<double>(totalWidth) / usedWidth;
usedWidth = 0;
for (size_t i = 0; i < m_blocks.size(); ++i)
{
widths[i] = std::max(2, static_cast<int>(widths[i] * scale));
usedWidth += widths[i];
}
// Adjust last block to fill
if (usedWidth != totalWidth)
widths.back() += (totalWidth - usedWidth);
}
int x = drawArea.x();
for (size_t i = 0; i < m_blocks.size(); ++i)
{
const auto& blk = m_blocks[i];
QRect blockRect(x, drawArea.y(), widths[i], drawArea.height());
m_blockRects[i] = blockRect;
// Fill
QColor fillColor = blk.color;
if (static_cast<int>(i) == m_hoveredBlock)
fillColor = fillColor.lighter(120);
if (static_cast<int>(i) == m_selectedBlock)
fillColor = fillColor.lighter(140);
painter.fillRect(blockRect.adjusted(1, 0, -1, 0), fillColor);
// Border
painter.setPen(QColor(40, 40, 40));
painter.drawRect(blockRect.adjusted(1, 0, -1, 0));
// Label text
if (widths[i] > 30)
{
painter.setPen(Qt::white);
QFont font = painter.font();
font.setPointSize(8);
painter.setFont(font);
// Size string
auto sizeStr = [](uint64_t bytes) -> QString {
if (bytes >= 1099511627776ULL)
return QString("%1 TB").arg(bytes / 1099511627776.0, 0, 'f', 1);
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', 1);
return QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 0);
};
QRect textRect = blockRect.adjusted(4, 2, -4, -2);
QString topText = blk.label;
QString botText = sizeStr(blk.sizeBytes);
painter.drawText(textRect, Qt::AlignTop | Qt::AlignHCenter, topText);
painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, botText);
}
x += widths[i];
}
}
void DiskMapWidget::mousePressEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (event->button() == Qt::LeftButton)
{
m_selectedBlock = idx;
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit partitionClicked(m_blocks[idx].partitionIndex);
}
update();
}
else if (event->button() == Qt::RightButton)
{
m_selectedBlock = idx;
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit contextMenuRequested(m_blocks[idx].partitionIndex, event->globalPosition().toPoint());
}
update();
}
}
void DiskMapWidget::mouseDoubleClickEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
emit partitionDoubleClicked(m_blocks[idx].partitionIndex);
}
}
void DiskMapWidget::mouseMoveEvent(QMouseEvent* event)
{
int idx = blockAtPos(event->pos());
if (idx != m_hoveredBlock)
{
m_hoveredBlock = idx;
update();
}
// Tooltip
if (idx >= 0 && idx < static_cast<int>(m_blocks.size()))
{
const auto& blk = m_blocks[idx];
auto fmtSize = [](uint64_t bytes) -> QString {
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);
return QString("%1 MB").arg(bytes / 1048576.0, 0, 'f', 1);
};
QString tip = QString("%1\nSize: %2\nFS: %3")
.arg(blk.label)
.arg(fmtSize(blk.sizeBytes))
.arg(filesystemShortName(blk.fsType));
QToolTip::showText(event->globalPosition().toPoint(), tip, this);
}
else
{
QToolTip::hideText();
}
}
int DiskMapWidget::blockAtPos(const QPoint& pos) const
{
for (size_t i = 0; i < m_blockRects.size(); ++i)
{
if (m_blockRects[i].contains(pos))
return static_cast<int>(i);
}
return -1;
}
QColor DiskMapWidget::colorForFilesystem(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QColor(52, 101, 164);
case FilesystemType::FAT32: return QColor(78, 154, 6);
case FilesystemType::FAT16: return QColor(78, 154, 6);
case FilesystemType::FAT12: return QColor(78, 154, 6);
case FilesystemType::ExFAT: return QColor(115, 210, 22);
case FilesystemType::ReFS: return QColor(32, 74, 135);
case FilesystemType::Ext2: return QColor(204, 0, 0);
case FilesystemType::Ext3: return QColor(204, 0, 0);
case FilesystemType::Ext4: return QColor(164, 0, 0);
case FilesystemType::Btrfs: return QColor(245, 121, 0);
case FilesystemType::XFS: return QColor(196, 160, 0);
case FilesystemType::HFSPlus: return QColor(117, 80, 123);
case FilesystemType::APFS: return QColor(173, 127, 168);
case FilesystemType::SWAP_LINUX: return QColor(143, 89, 2);
case FilesystemType::ISO9660: return QColor(85, 87, 83);
case FilesystemType::UDF: return QColor(85, 87, 83);
case FilesystemType::Unallocated: return QColor(80, 80, 80);
default: return QColor(136, 138, 133);
}
}
QString DiskMapWidget::filesystemShortName(FilesystemType fs)
{
switch (fs)
{
case FilesystemType::NTFS: return QStringLiteral("NTFS");
case FilesystemType::FAT32: return QStringLiteral("FAT32");
case FilesystemType::FAT16: return QStringLiteral("FAT16");
case FilesystemType::FAT12: return QStringLiteral("FAT12");
case FilesystemType::ExFAT: return QStringLiteral("exFAT");
case FilesystemType::ReFS: return QStringLiteral("ReFS");
case FilesystemType::Ext2: return QStringLiteral("ext2");
case FilesystemType::Ext3: return QStringLiteral("ext3");
case FilesystemType::Ext4: return QStringLiteral("ext4");
case FilesystemType::Btrfs: return QStringLiteral("Btrfs");
case FilesystemType::XFS: return QStringLiteral("XFS");
case FilesystemType::ZFS: return QStringLiteral("ZFS");
case FilesystemType::HFSPlus: return QStringLiteral("HFS+");
case FilesystemType::APFS: return QStringLiteral("APFS");
case FilesystemType::SWAP_LINUX: return QStringLiteral("Swap");
case FilesystemType::ISO9660: return QStringLiteral("ISO9660");
case FilesystemType::UDF: return QStringLiteral("UDF");
case FilesystemType::Unallocated: return QStringLiteral("Free");
case FilesystemType::Unknown: return QStringLiteral("Unknown");
case FilesystemType::Raw: return QStringLiteral("RAW");
default: return QStringLiteral("Other");
}
}
} // namespace spw

View File

@@ -0,0 +1,69 @@
#pragma once
#include "core/common/Types.h"
#include "core/disk/DiskEnumerator.h"
#include <QWidget>
#include <QColor>
#include <vector>
namespace spw
{
class DiskMapWidget : public QWidget
{
Q_OBJECT
public:
explicit DiskMapWidget(QWidget* parent = nullptr);
// Set the disk to display
void setDisk(const DiskInfo& disk,
const std::vector<PartitionInfo>& partitions,
const std::vector<VolumeInfo>& volumes);
void clear();
QSize minimumSizeHint() const override { return QSize(400, 80); }
QSize sizeHint() const override { return QSize(600, 100); }
signals:
void partitionClicked(int partitionIndex);
void partitionDoubleClicked(int partitionIndex);
void contextMenuRequested(int partitionIndex, const QPoint& globalPos);
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
private:
struct Block
{
int partitionIndex = -1; // -1 = unallocated
uint64_t startBytes = 0;
uint64_t sizeBytes = 0;
FilesystemType fsType = FilesystemType::Unknown;
QString label;
wchar_t driveLetter = L'\0';
QColor color;
};
void rebuildBlocks();
int blockAtPos(const QPoint& pos) const;
static QColor colorForFilesystem(FilesystemType fs);
static QString filesystemShortName(FilesystemType fs);
DiskInfo m_disk;
std::vector<PartitionInfo> m_partitions;
std::vector<VolumeInfo> m_volumes;
std::vector<Block> m_blocks;
int m_hoveredBlock = -1;
int m_selectedBlock = -1;
// Cached block rectangles from last paint
std::vector<QRect> m_blockRects;
};
} // namespace spw