diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 8224d09b2..37144c7d3 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -107,6 +107,8 @@ set(QET_RES_FILES
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
)
set(QET_SRC_FILES
+ ${QET_DIR}/sources/import/edz/edzarchive.cpp
+ ${QET_DIR}/sources/import/edz/edzarchive.h
${QET_DIR}/sources/borderproperties.cpp
${QET_DIR}/sources/borderproperties.h
${QET_DIR}/sources/bordertitleblock.cpp
diff --git a/sources/import/edz/edzarchive.cpp b/sources/import/edz/edzarchive.cpp
new file mode 100644
index 000000000..02726efe2
--- /dev/null
+++ b/sources/import/edz/edzarchive.cpp
@@ -0,0 +1,154 @@
+/*
+ Copyright 2006 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "edzarchive.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+EdzArchive::EdzArchive() = default;
+EdzArchive::~EdzArchive() = default;
+
+/**
+ Extract the .edz at @a edz_path into a fresh temporary directory.
+ @return true on success; extractedDir()/partXmlPath() are then valid.
+*/
+bool EdzArchive::extract(const QString &edz_path)
+{
+ m_error.clear();
+
+ const QFileInfo fi(edz_path);
+ if (!fi.exists() || !fi.isFile()) {
+ m_error = QStringLiteral("File not found: %1").arg(edz_path);
+ return false;
+ }
+
+ m_dir.reset(new QTemporaryDir);
+ if (!m_dir->isValid()) {
+ m_error = QStringLiteral("Could not create a temporary directory: %1")
+ .arg(m_dir->errorString());
+ return false;
+ }
+
+ if (!extractWithSevenZipCli(fi.absoluteFilePath(), m_dir->path())) {
+ return false;
+ }
+
+ if (partXmlPath().isEmpty()) {
+ m_error = QStringLiteral("No *.part.xml found inside the .edz");
+ return false;
+ }
+ return true;
+}
+
+/** @return the temporary folder holding the extracted tree (empty if none). */
+QString EdzArchive::extractedDir() const
+{
+ return m_dir ? m_dir->path() : QString();
+}
+
+/** @return absolute path to the first *.part.xml in the extracted tree. */
+QString EdzArchive::partXmlPath() const
+{
+ const QString root = extractedDir();
+ if (root.isEmpty()) {
+ return QString();
+ }
+ QDirIterator it(root, QStringList{QStringLiteral("*.part.xml")},
+ QDir::Files, QDirIterator::Subdirectories);
+ return it.hasNext() ? it.next() : QString();
+}
+
+QString EdzArchive::errorString() const
+{
+ return m_error;
+}
+
+/**
+ M0 backend: run a 7-Zip command-line tool to extract the archive.
+ Placeholder for a bundled decompressor in a later milestone.
+*/
+bool EdzArchive::extractWithSevenZipCli(const QString &edz_path,
+ const QString &dest)
+{
+ const QString exe = findSevenZip();
+ if (exe.isEmpty()) {
+ m_error = QStringLiteral(
+ "7-Zip not found. A .edz is a 7z archive; install 7-Zip or "
+ "put 7z on PATH.");
+ return false;
+ }
+
+ QProcess proc;
+ proc.start(exe, QStringList{QStringLiteral("x"), edz_path,
+ QStringLiteral("-o%1").arg(dest),
+ QStringLiteral("-y")});
+ if (!proc.waitForStarted()) {
+ m_error = QStringLiteral("Could not start 7-Zip: %1").arg(exe);
+ return false;
+ }
+ proc.waitForFinished(-1);
+
+ if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) {
+ QString detail;
+ const QString out = QString::fromLocal8Bit(proc.readAllStandardOutput());
+ for (const QString &line : out.split('\n')) {
+ const QString t = line.trimmed();
+ if (t.startsWith(QStringLiteral("ERROR"))
+ || t.startsWith(QStringLiteral("Cannot"))
+ || t.startsWith(QStringLiteral("Can't"))) {
+ detail += (detail.isEmpty() ? QString() : QStringLiteral(" ")) + t;
+ }
+ }
+ if (detail.isEmpty()) {
+ detail = QStringLiteral("7-Zip exit code %1").arg(proc.exitCode());
+ }
+ m_error = QStringLiteral("7-Zip could not extract the archive: %1")
+ .arg(detail);
+ return false;
+ }
+ return true;
+}
+
+/** Locate a 7-Zip executable: PATH first, then the usual install locations. */
+QString EdzArchive::findSevenZip()
+{
+ for (const QString &name : {QStringLiteral("7z"), QStringLiteral("7za"),
+ QStringLiteral("7zr")}) {
+ const QString found = QStandardPaths::findExecutable(name);
+ if (!found.isEmpty()) {
+ return found;
+ }
+ }
+ const QStringList candidates {
+ QStringLiteral("C:/Program Files/7-Zip/7z.exe"),
+ QStringLiteral("C:/Program Files (x86)/7-Zip/7z.exe"),
+ QStringLiteral("/usr/bin/7z"),
+ QStringLiteral("/usr/local/bin/7z"),
+ QStringLiteral("/opt/homebrew/bin/7z"),
+ };
+ for (const QString &c : candidates) {
+ if (QFileInfo::exists(c)) {
+ return c;
+ }
+ }
+ return QString();
+}
diff --git a/sources/import/edz/edzarchive.h b/sources/import/edz/edzarchive.h
new file mode 100644
index 000000000..f678859d1
--- /dev/null
+++ b/sources/import/edz/edzarchive.h
@@ -0,0 +1,62 @@
+/*
+ Copyright 2006 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef EDZARCHIVE_H
+#define EDZARCHIVE_H
+
+#include
+#include
+
+class QTemporaryDir;
+
+/**
+ @brief Extracts an EPLAN Data Portal package (.edz) to a temporary folder.
+
+ A .edz is a 7-Zip archive holding a part definition (part.xml), an EPLAN
+ macro, a product image and metadata. EdzArchive unpacks it so the rest of the
+ import pipeline can read the (portable) part.xml.
+
+ The extraction backend sits behind a single private method so it can be
+ swapped without touching callers: this M0 spike shells out to a 7-Zip CLI via
+ QProcess; a later milestone replaces that with a bundled decompressor so no
+ external tool is required at runtime.
+
+ The extracted tree lives in a QTemporaryDir owned by this object and is
+ removed when the EdzArchive is destroyed.
+*/
+class EdzArchive
+{
+ public:
+ EdzArchive();
+ ~EdzArchive();
+
+ bool extract(const QString &edz_path);
+
+ QString extractedDir() const;
+ QString partXmlPath() const;
+ QString errorString() const;
+
+ private:
+ bool extractWithSevenZipCli(const QString &edz_path,
+ const QString &dest);
+ static QString findSevenZip();
+
+ QScopedPointer m_dir;
+ QString m_error;
+};
+
+#endif // EDZARCHIVE_H