/
klischa
/
AstraScanner2
Обзор
Документация
Войти
/
klischa
/
AstraScanner2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/project/ProjectManager.cpp
657 строк
23 KB
k k
fix(critical): 5 critical bugs in one batch
13 июл 2026, 23:22
13 июл 2026, 23:22
06c9bb4
Код
Авторство
О чём код?
#include "ProjectManager.h" #include <QDir> #include <QFile> #include <QFileInfo> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> #include <QDebug> #include <pcl/io/ply_io.h> #include <filesystem> namespace { constexpr const char *kMetadataFile = "project.json"; constexpr const char *kScansSubdir = "scans"; // QString → std::string для передачи в PCL/std::ofstream. // На Windows toStdString() даёт UTF-8, но std::ofstream ожидает кодировку // Возвращает путь в кодировке, понятной файловым API. // На MSVC .string() отдаёт ANSI codepage — кириллица и CJK ломаются. // Используем u8string() (UTF-8), который корректно открывает PCL и ifstream // на современных MSVC (C++17 с UTF-8 source/execution charset). inline std::string toNarrowPath(const QString &qpath) { #ifdef _WIN32 std::string result = std::filesystem::path(qpath.toStdWString()).u8string(); if (result.empty()) { qWarning() << "[Project] toNarrowPath produced empty string for" << qpath; } return result; #else return qpath.toStdString(); #endif } inline QString absoluteCleanPath(const QString &path) { return QDir::cleanPath(QFileInfo(path).absoluteFilePath()); } inline bool isPathInsideProject(const QString &projectDir, const QString &candidatePath) { const QString base = absoluteCleanPath(projectDir); const QString candidate = absoluteCleanPath(candidatePath); return candidate == base || candidate.startsWith(base + "/"); } } ProjectManager::ProjectManager(QObject *parent) : QObject(parent) {} QString ProjectManager::metadataPath() const { if (m_projectDir.isEmpty()) return {}; return QDir(m_projectDir).filePath(kMetadataFile); } QString ProjectManager::scansDirPath() const { if (m_projectDir.isEmpty()) return {}; return QDir(m_projectDir).filePath(kScansSubdir); } bool ProjectManager::isOpen() const { QMutexLocker locker(&m_mutex); return !m_projectDir.isEmpty(); } bool ProjectManager::isDirty() const { QMutexLocker locker(&m_mutex); return m_dirty; } QString ProjectManager::projectDir() const { QMutexLocker locker(&m_mutex); return m_projectDir; } QString ProjectManager::projectName() const { QMutexLocker locker(&m_mutex); return m_projectName; } int ProjectManager::scanCount() const { QMutexLocker locker(&m_mutex); return m_scans.size(); } QVector<ScanItem> ProjectManager::scans() const { QMutexLocker locker(&m_mutex); return m_scans; } QString ProjectManager::lastError() const { QMutexLocker locker(&m_mutex); return m_lastError; } bool ProjectManager::createAiTempDir() { if (m_projectDir.isEmpty()) return false; QDir dir(m_projectDir); return dir.mkpath(kAiTempSubdir); } bool ProjectManager::setDirty(bool dirty) { if (m_dirty == dirty) { return false; } m_dirty = dirty; return true; } bool ProjectManager::newProject(const QString &projectDir, const QString &name) { QDir dir(projectDir); if (!dir.exists() && !dir.mkpath(".")) { m_lastError = QString("Не удалось создать директорию проекта: %1").arg(projectDir); return false; } if (!QDir(projectDir).mkpath(kScansSubdir)) { m_lastError = QString("Не удалось создать поддиректорию scans/"); return false; } if (!QDir(projectDir).mkpath(kAiTempSubdir)) { m_lastError = QString("Не удалось создать поддиректорию ai_temp/"); return false; } bool changed1 = false, changed2 = false; { QMutexLocker locker(&m_mutex); m_lastError.clear(); m_projectDir = projectDir; m_projectName = name.isEmpty() ? QFileInfo(projectDir).fileName() : name; m_scans.clear(); changed1 = setDirty(true); if (!writeMetadata()) return false; changed2 = setDirty(false); } // Эмитим сигналы вне мьютекса — избегаем deadlock emit scansChanged(); if (changed1) emit projectChanged(); if (changed2) emit projectChanged(); qInfo() << "[Project] Created new project at" << projectDir; return true; } bool ProjectManager::openProject(const QString &projectDir) { { QMutexLocker locker(&m_mutex); m_lastError.clear(); } if (!QFileInfo(projectDir).isDir()) { QMutexLocker locker(&m_mutex); m_lastError = QString("Не директория: %1").arg(projectDir); return false; } QString projectName; int scanCount = 0; bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); m_projectDir = projectDir; m_scans.clear(); if (!readMetadata()) { m_projectDir.clear(); return false; } projectName = m_projectName; scanCount = m_scans.size(); emitProjectChangedSignal = setDirty(false); } // Эмитим сигналы вне мьютекса — избегаем deadlock emit scansChanged(); if (emitProjectChangedSignal) emit projectChanged(); qInfo() << "[Project] Opened project" << projectName << "with" << scanCount << "scans"; return true; } void ProjectManager::setTrackingMode(TrackingMode mode) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (m_trackingMode != mode) { m_trackingMode = mode; emitProjectChangedSignal = setDirty(true); } } if (emitProjectChangedSignal) emit projectChanged(); } TrackingMode ProjectManager::getTrackingMode() const { QMutexLocker locker(&m_mutex); return m_trackingMode; } void ProjectManager::setScannerPose(const Eigen::Affine3f& pose) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (!m_scanner_pose.isApprox(pose, 1e-6f)) { m_scanner_pose = pose; emitProjectChangedSignal = setDirty(true); } } if (emitProjectChangedSignal) emit projectChanged(); } Eigen::Affine3f ProjectManager::getScannerPose() const { QMutexLocker locker(&m_mutex); return m_scanner_pose; } bool ProjectManager::saveProject() { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (m_projectDir.isEmpty()) { m_lastError = "Проект не открыт"; return false; } // Сохраняем все облака на диск for (ScanItem& item : m_scans) { if (!saveScanToDisk(item)) { return false; } } if (!writeMetadata()) return false; emitProjectChangedSignal = setDirty(false); } if (emitProjectChangedSignal) emit projectChanged(); return true; } bool ProjectManager::saveProjectAs(const QString &projectDir) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); const QString oldDir = m_projectDir; QDir newDir(projectDir); if (!newDir.exists() && !newDir.mkpath(".")) { m_lastError = QString("Не удалось создать директорию: %1").arg(projectDir); return false; } if (!QDir(projectDir).mkpath(kScansSubdir)) { m_lastError = "Не удалось создать scans/"; return false; } // Создаём директорию для AI промежуточных результатов if (!QDir(projectDir).mkpath(kAiTempSubdir)) { m_lastError = "Не удалось создать ai_temp/"; return false; } // Сначала подгружаем все облака из старого проекта, пока m_projectDir // ещё указывает на oldDir. Иначе path-traversal check в loadScanFromDisk() // отвергнет старые пути как находящиеся вне нового проекта. for (ScanItem &item : m_scans) { if (!item.cloud && !loadScanFromDisk(item)) { return false; } } m_projectDir = projectDir; // Перезаписываем все облака в новом месте, чтобы проект был самодостаточен. for (ScanItem &item : m_scans) { if (!saveScanToDisk(item)) { m_projectDir = oldDir; return false; } } if (!writeMetadata()) { m_projectDir = oldDir; return false; } setDirty(false); emitProjectChangedSignal = true; } if (emitProjectChangedSignal) emit projectChanged(); qInfo() << "[Project] Saved project as" << projectDir; return true; } void ProjectManager::closeProject() { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); m_projectDir.clear(); m_projectName.clear(); m_scans.clear(); emitProjectChangedSignal = setDirty(false); } if (emitProjectChangedSignal) emit projectChanged(); emit scansChanged(); } int ProjectManager::addScan(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud, const QString &name) { bool emitProjectChangedSignal = false; int resultIndex = -1; QString itemName; int itemPointCount = 0; { QMutexLocker locker(&m_mutex); m_lastError.clear(); if (!cloud || cloud->empty()) { m_lastError = "Пустое облако, добавление отменено"; return -1; } if (m_projectDir.isEmpty()) { m_lastError = "Проект не открыт — создайте или откройте проект перед добавлением скана"; return -1; } ScanItem item; item.createdAt = QDateTime::currentDateTime(); item.name = name.isEmpty() ? QString("Scan %1").arg(m_scans.size() + 1) : name; // Используем timestamp + порядковый номер, чтобы избежать конфликтов имен const QString fname = QString("scan_%1_%2.ply") .arg(item.createdAt.toString("yyyyMMdd_hhmmsszzz")) .arg(m_scans.size()); item.relativeFilePath = QString("%1/%2").arg(kScansSubdir, fname); item.cloud = cloud; item.pointCount = static_cast<int>(cloud->size()); if (!saveScanToDisk(item)) { return -1; } m_scans.append(item); emitProjectChangedSignal = setDirty(true); if (!writeMetadata()) { m_scans.removeLast(); QFile::remove(QDir(m_projectDir).filePath(item.relativeFilePath)); return -1; } setDirty(false); resultIndex = m_scans.size() - 1; itemName = item.name; itemPointCount = item.pointCount; } if (emitProjectChangedSignal) emit projectChanged(); emit scansChanged(); qInfo() << "[Project] Added scan" << itemName << "with" << itemPointCount << "points"; return resultIndex; } bool ProjectManager::removeScan(int index) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_scans.size()) return false; const bool wasDirty = m_dirty; const ScanItem removedItem = m_scans[index]; const QString fullPath = QDir(m_projectDir).filePath(removedItem.relativeFilePath); // Сначала меняем модель и metadata. Если writeMetadata() упадёт, // полностью возвращаем прежнее состояние в памяти и на диске. m_scans.removeAt(index); emitProjectChangedSignal = setDirty(true); if (!writeMetadata()) { const QString writeError = m_lastError; m_scans.insert(index, removedItem); m_dirty = wasDirty; if (!writeMetadata()) { qCritical() << "[Project] rollback metadata write failed after removeScan:" << m_lastError; } m_lastError = writeError.isEmpty() ? QStringLiteral("Не удалось записать project.json после удаления скана") : writeError; qCritical() << "[Project]" << m_lastError; return false; } // Если файл удалить не удалось, тоже откатываем metadata: проект не // должен считать скан удалённым, когда файл остался на месте. if (!QFile::remove(fullPath)) { const QString removeError = QString("Не удалось удалить файл скана: %1").arg(fullPath); m_scans.insert(index, removedItem); m_dirty = wasDirty; if (!writeMetadata()) { qCritical() << "[Project] rollback metadata write failed after removeScan file error:" << m_lastError; } m_lastError = removeError; return false; } emitProjectChangedSignal = setDirty(false) || emitProjectChangedSignal; } if (emitProjectChangedSignal) emit projectChanged(); emit scansChanged(); return true; } bool ProjectManager::renameScan(int index, const QString &newName) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_scans.size()) return false; if (m_scans[index].name == newName) return true; const bool wasDirty = m_dirty; const QString oldName = m_scans[index].name; m_scans[index].name = newName; emitProjectChangedSignal = setDirty(true); if (!writeMetadata()) { const QString writeError = m_lastError; m_scans[index].name = oldName; m_dirty = wasDirty; if (!writeMetadata()) { qCritical() << "[Project] rollback metadata write failed after renameScan:" << m_lastError; } m_lastError = writeError.isEmpty() ? QStringLiteral("Не удалось записать project.json после переименования скана") : writeError; qCritical() << "[Project]" << m_lastError; return false; } emitProjectChangedSignal = setDirty(false) || emitProjectChangedSignal; } if (emitProjectChangedSignal) emit projectChanged(); emit scansChanged(); return true; } pcl::PointCloud<pcl::PointXYZRGB>::Ptr ProjectManager::scanCloud(int index) { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_scans.size()) return nullptr; ScanItem &item = m_scans[index]; if (!item.cloud) { loadScanFromDisk(item); } return item.cloud; } bool ProjectManager::setScanCloud(int index, const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud) { bool emitProjectChangedSignal = false; { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_scans.size()) return false; if (!cloud || cloud->empty()) return false; ScanItem &item = m_scans[index]; item.cloud = cloud; item.pointCount = static_cast<int>(cloud->size()); emitProjectChangedSignal = setDirty(true); } if (emitProjectChangedSignal) emit projectChanged(); return true; } bool ProjectManager::writeMetadata() { m_lastError.clear(); QJsonObject root; root["name"] = m_projectName; root["formatVersion"] = 1; QJsonArray scansArr; for (const ScanItem &item : m_scans) { QJsonObject s; s["name"] = item.name; s["file"] = item.relativeFilePath; s["createdAt"] = item.createdAt.toString(Qt::ISODate); s["pointCount"] = item.pointCount; scansArr.append(s); } root["scans"] = scansArr; // Сохраняем режим трекинга и позу сканера QJsonObject tracking; tracking["mode"] = static_cast<int>(m_trackingMode); // Исправление #8: сохраняем полную позу (translation + rotation через кватернион) Eigen::Quaternionf q(m_scanner_pose.linear()); tracking["pose"] = QJsonArray({ m_scanner_pose.translation().x(), m_scanner_pose.translation().y(), m_scanner_pose.translation().z(), q.x(), q.y(), q.z(), q.w() }); root["tracking"] = tracking; QFile f(metadataPath()); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_lastError = QString("Не удалось открыть %1 для записи").arg(metadataPath()); qWarning() << "[Project]" << m_lastError; return false; } f.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); return true; } bool ProjectManager::readMetadata() { m_lastError.clear(); QFile f(metadataPath()); if (!f.exists()) { m_lastError = QString("project.json не найден в %1").arg(m_projectDir); return false; } if (!f.open(QIODevice::ReadOnly)) { m_lastError = QString("Не удалось открыть %1").arg(metadataPath()); return false; } QJsonParseError err; const QJsonDocument doc = QJsonDocument::fromJson(f.readAll(), &err); if (err.error != QJsonParseError::NoError) { m_lastError = QString("Ошибка JSON: %1").arg(err.errorString()); return false; } const QJsonObject root = doc.object(); // --- Валидация схемы --- const int formatVersion = root.value("formatVersion").toInt(0); if (formatVersion < 1) { m_lastError = QString("Неизвестный или отсутствующий formatVersion: %1").arg(formatVersion); return false; } m_projectName = root.value("name").toString(QFileInfo(m_projectDir).fileName()); // Загружаем режим трекинга и позу сканера if (root.contains("tracking")) { QJsonObject tracking = root["tracking"].toObject(); m_trackingMode = static_cast<TrackingMode>(tracking.value("mode").toInt(0)); QJsonArray poseArr = tracking.value("pose").toArray(); if (poseArr.size() >= 7) { // Исправление #8: загружаем полную позу с кватернионом m_scanner_pose.translation() = Eigen::Vector3f(poseArr[0].toDouble(), poseArr[1].toDouble(), poseArr[2].toDouble()); Eigen::Quaternionf q(poseArr[6].toDouble(), poseArr[3].toDouble(), poseArr[4].toDouble(), poseArr[5].toDouble()); m_scanner_pose.linear() = q.toRotationMatrix(); } else if (poseArr.size() >= 3) { // Backward compatibility: old format without rotation m_scanner_pose.translation() = Eigen::Vector3f(poseArr[0].toDouble(), poseArr[1].toDouble(), poseArr[2].toDouble()); } } m_scans.clear(); for (const QJsonValue &v : root.value("scans").toArray()) { const QJsonObject s = v.toObject(); const QString filePath = s.value("file").toString(); if (filePath.isEmpty()) { qWarning() << "[Project] Пропущен скан без поля 'file'"; continue; } // --- Проверка path traversal при чтении метаданных --- const QString fullPath = QFileInfo(filePath).isAbsolute() ? filePath : QDir(m_projectDir).filePath(filePath); if (!isPathInsideProject(m_projectDir, fullPath)) { qWarning() << "[Project] Пропущен скан с path traversal:" << filePath; continue; } ScanItem item; item.name = s.value("name").toString(QStringLiteral("Unnamed")); item.relativeFilePath = filePath; item.createdAt = QDateTime::fromString(s.value("createdAt").toString(), Qt::ISODate); item.pointCount = s.value("pointCount").toInt(); m_scans.append(item); } return true; } bool ProjectManager::saveScanToDisk(ScanItem &item) { if (!item.cloud) { m_lastError = "saveScanToDisk: облако не загружено"; return false; } const QString fullPath = QDir(m_projectDir).filePath(item.relativeFilePath); // --- Проверка path traversal --- if (!isPathInsideProject(m_projectDir, fullPath)) { m_lastError = QString("Путь скана выходит за пределы директории проекта: %1").arg(fullPath); qWarning() << "[Project]" << m_lastError; return false; } // Убеждаемся, что директория существует. QFileInfo(fullPath).absoluteDir().mkpath("."); try { const int rc = pcl::io::savePLYFileBinary(toNarrowPath(fullPath), *item.cloud); if (rc != 0) { m_lastError = QString("pcl::io::savePLYFileBinary вернул %1 для %2").arg(rc).arg(fullPath); return false; } } catch (const std::exception &ex) { m_lastError = QString("Исключение при сохранении скана: %1").arg(ex.what()); return false; } item.pointCount = static_cast<int>(item.cloud->size()); return true; } bool ProjectManager::loadScanFromDisk(ScanItem &item) { const QString fullPath = QFileInfo(item.relativeFilePath).isAbsolute() ? item.relativeFilePath : QDir(m_projectDir).filePath(item.relativeFilePath); // --- Проверка path traversal --- if (!isPathInsideProject(m_projectDir, fullPath)) { m_lastError = QString("Путь скана выходит за пределы директории проекта: %1").arg(fullPath); qWarning() << "[Project]" << m_lastError; return false; } if (!QFile::exists(fullPath)) { m_lastError = QString("Файл скана не найден: %1").arg(fullPath); return false; } auto cloud = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(); try { const int rc = pcl::io::loadPLYFile(toNarrowPath(fullPath), *cloud); if (rc != 0) { m_lastError = QString("pcl::io::loadPLYFile вернул %1 для %2").arg(rc).arg(fullPath); return false; } } catch (const std::exception &ex) { m_lastError = QString("Исключение при загрузке скана: %1").arg(ex.what()); return false; } item.cloud = cloud; item.pointCount = static_cast<int>(cloud->size()); return true; }