mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-13 16:34:14 +02:00
Merge pull request #1462 from ispyisail/feat/qetz-container
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m8s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m8s
Add the zipped project format (.qetz) as a save option
This commit is contained in:
@@ -102,6 +102,13 @@ else()
|
||||
message(STATUS "Qt Qml module not available: JavaScript scripting (--run) disabled")
|
||||
endif()
|
||||
|
||||
# The zip container of a .qetz project is read and written with the bundled
|
||||
# miniz (sources/container/miniz): in memory only, and without timestamps so
|
||||
# that saving the same project twice gives the same bytes. The names must not
|
||||
# clash with the zlib Qt links. Every target including miniz.h needs the same
|
||||
# three, hence directory-wide.
|
||||
add_compile_definitions(MINIZ_NO_TIME MINIZ_NO_STDIO MINIZ_NO_ZLIB_COMPATIBLE_NAMES)
|
||||
|
||||
# The crash handler writes a backtrace into the dump. Detecting this with
|
||||
# __has_include(<execinfo.h>) is not enough: the header is present on FreeBSD
|
||||
# too, but backtrace() lives in a separate libexecinfo there, so the compile
|
||||
|
||||
@@ -165,6 +165,14 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/import/edz/lzma/LzmaDec.c
|
||||
${QET_DIR}/sources/import/edz/lzma/Ppmd7.c
|
||||
${QET_DIR}/sources/import/edz/lzma/Ppmd7Dec.c
|
||||
${QET_DIR}/sources/container/miniz/miniz.c
|
||||
${QET_DIR}/sources/container/miniz/miniz.h
|
||||
${QET_DIR}/sources/container/qetcontainer.cpp
|
||||
${QET_DIR}/sources/container/qetcontainerdb.cpp
|
||||
${QET_DIR}/sources/container/qetcontainerdb.h
|
||||
${QET_DIR}/sources/container/qetcontainer.h
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/qetzip.h
|
||||
${QET_DIR}/sources/borderproperties.cpp
|
||||
${QET_DIR}/sources/borderproperties.h
|
||||
${QET_DIR}/sources/bordertitleblock.cpp
|
||||
|
||||
+24
-7
@@ -870,6 +870,15 @@ int exportLinks(QETProject &project, const QString &output)
|
||||
int resaveProject(QETProject &project, const QString &output)
|
||||
{
|
||||
const QDomDocument doc = project.toXml();
|
||||
if (QETProject::isContainerPath(output)) {
|
||||
QString error;
|
||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
||||
err << "Cannot write '" << output << "': " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
out << "Re-saved project -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
@@ -955,14 +964,22 @@ int setTitleBlock(QETProject &project, const QString &output,
|
||||
}
|
||||
|
||||
const QDomDocument doc = project.toXml();
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
if (QETProject::isContainerPath(output)) {
|
||||
QString error;
|
||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
||||
err << "Cannot write '" << output << "': " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << doc.toString(4);
|
||||
file.close();
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << doc.toString(4);
|
||||
file.close();
|
||||
out << "Stamped " << fields.size() << " field(s) on "
|
||||
<< folios << " folio(s) -> " << output << "\n";
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright 2013-2014 RAD Game Tools and Valve Software
|
||||
Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
|
||||
|
||||
All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,5 @@
|
||||
miniz 3.1.2 (https://github.com/richgel999/miniz, release asset
|
||||
miniz-3.1.2.zip, sha256 f0446d863f9c19926ad9483c523fdc42e42b8d4a6a431d27e09d49c79a140d9a),
|
||||
MIT licence (LICENSE). Unmodified. Reads and writes the zip container of a
|
||||
.qetz project (sources/container/qetzip.*). Built with MINIZ_NO_TIME,
|
||||
MINIZ_NO_STDIO and MINIZ_NO_ZLIB_COMPATIBLE_NAMES (see qetzip.h).
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetcontainer.h"
|
||||
#include "qetcontainerdb.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
|
||||
const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip";
|
||||
|
||||
namespace {
|
||||
const QString PartTag = QStringLiteral("qetz-part");
|
||||
const QString FileAttribute = QStringLiteral("file");
|
||||
const QString ImageFileAttribute = QStringLiteral("qetz-file");
|
||||
|
||||
void setError(QString *error, const QString &text)
|
||||
{
|
||||
if (error) *error = text;
|
||||
}
|
||||
|
||||
//An element on its own, written without adding indentation: the
|
||||
//whitespace text already in the document (QETProject keeps the symbol
|
||||
//definitions as they were read, spacing included) must come back as it
|
||||
//was, or the next save writes it differently. Parts are one line.
|
||||
QByteArray serialize(const QDomNode &node)
|
||||
{
|
||||
QDomDocument document;
|
||||
document.appendChild(document.createProcessingInstruction(
|
||||
QStringLiteral("xml"),
|
||||
QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
|
||||
document.appendChild(document.importNode(node, true));
|
||||
return document.toString(-1).toUtf8();
|
||||
}
|
||||
|
||||
//A file name from a name chosen by the user: no folders, nothing a zip
|
||||
//tool or a file system would read another way.
|
||||
QString safeSegment(const QString &name, const QString &fallback)
|
||||
{
|
||||
QString out;
|
||||
for (const QChar c : name) {
|
||||
out += (c.isLetterOrNumber() || c == QLatin1Char('-') || c == QLatin1Char('_')
|
||||
|| c == QLatin1Char('.') || c == QLatin1Char(' '))
|
||||
? c : QLatin1Char('_');
|
||||
}
|
||||
out = out.trimmed();
|
||||
while (out.startsWith(QLatin1Char('.'))) out.remove(0, 1);
|
||||
return out.isEmpty() ? fallback : out;
|
||||
}
|
||||
|
||||
//@p name, or name~2, name~3... if already taken
|
||||
QString unique(const QString &name, QSet<QString> *taken)
|
||||
{
|
||||
QString candidate = name;
|
||||
for (int n = 2 ; taken->contains(candidate) ; ++n) {
|
||||
const int dot = name.lastIndexOf(QLatin1Char('.'));
|
||||
candidate = dot > name.lastIndexOf(QLatin1Char('/'))
|
||||
? name.left(dot) + QStringLiteral("~%1").arg(n) + name.mid(dot)
|
||||
: name + QStringLiteral("~%1").arg(n);
|
||||
}
|
||||
taken->insert(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
QDomElement placeholder(QDomDocument &document, const QString &file)
|
||||
{
|
||||
QDomElement part = document.createElement(PartTag);
|
||||
part.setAttribute(FileAttribute, file);
|
||||
return part;
|
||||
}
|
||||
|
||||
QList<QDomElement> childElements(const QDomElement &parent, const QString &tag)
|
||||
{
|
||||
QList<QDomElement> list;
|
||||
for (QDomElement e = parent.firstChildElement(tag) ; !e.isNull() ; e = e.nextSiblingElement(tag))
|
||||
list << e;
|
||||
return list;
|
||||
}
|
||||
|
||||
//The collection's symbol definitions, each to elements/<categories>/<name>
|
||||
void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path,
|
||||
QList<QetZip::Entry> *parts, QSet<QString> *taken)
|
||||
{
|
||||
for (QDomElement child = category.firstChildElement() ; !child.isNull() ;
|
||||
child = child.nextSiblingElement())
|
||||
{
|
||||
const QString name = safeSegment(child.attribute(QStringLiteral("name")),
|
||||
QStringLiteral("unnamed"));
|
||||
if (child.tagName() == QLatin1String("category")) {
|
||||
splitCollection(document, child, path + name + QLatin1Char('/'), parts, taken);
|
||||
} else if (child.tagName() == QLatin1String("element")) {
|
||||
QDomElement definition = child.firstChildElement(QStringLiteral("definition"));
|
||||
if (definition.isNull()) continue;
|
||||
const QString file = unique(path + name, taken);
|
||||
parts->append({file, serialize(definition), true});
|
||||
child.replaceChild(placeholder(document, file), definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::parse
|
||||
Parse @p xml as QETProject::openFile() reads a .qet: a text node made
|
||||
only of spaces is kept (a title-block value of " ", #973).
|
||||
*/
|
||||
bool QetContainer::parse(const QByteArray &xml, QDomDocument *document, QString *error)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
const auto result = document->setContent(xml, QDomDocument::ParseOption::PreserveSpacingOnlyNodes);
|
||||
if (!result) {
|
||||
setError(error, QStringLiteral("line %1: %2").arg(result.errorLine).arg(result.errorMessage));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
QString message;
|
||||
int line = 0;
|
||||
if (!document->setContent(xml, &message, &line)) {
|
||||
setError(error, QStringLiteral("line %1: %2").arg(line).arg(message));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::isZip
|
||||
A .qetz is recognised by what it holds, not its name: a crash backup
|
||||
of a .qetz project is plain XML, and a renamed file is still a zip.
|
||||
*/
|
||||
bool QetContainer::isZip(const QByteArray &content)
|
||||
{
|
||||
return content.startsWith("PK\x03\x04");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::readDocument
|
||||
The project document held in @p content: unzipped and joined for a
|
||||
.qetz, parsed for a .qet.
|
||||
*/
|
||||
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document, QString *error)
|
||||
{
|
||||
if (!isZip(content))
|
||||
return parse(content, document, error);
|
||||
QList<QetZip::Entry> entries;
|
||||
return QetZip::fromBytes(content, &entries, error)
|
||||
&& join(entries, document, error);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::split
|
||||
@param project : the project document, as QETProject::toXml() makes it
|
||||
@param writer : the program writing it, kept in the manifest
|
||||
@return the entries of the .qetz, mimetype first; empty with @p error
|
||||
set if @p project is not a project
|
||||
*/
|
||||
QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QString &writer,
|
||||
QString *error)
|
||||
{
|
||||
QDomDocument document = project.cloneNode(true).toDocument();
|
||||
QDomElement root = document.documentElement();
|
||||
if (root.tagName() != QLatin1String("project")) {
|
||||
setError(error, QStringLiteral("not a project document"));
|
||||
return {};
|
||||
}
|
||||
|
||||
//The engineering data out of the document, into project.sqlite
|
||||
const QByteArray database = QetContainerDb::extract(root, error);
|
||||
if (database.isEmpty())
|
||||
return {};
|
||||
|
||||
QList<QetZip::Entry> parts;
|
||||
QSet<QString> taken;
|
||||
|
||||
//Folios, each with its pictures taken out
|
||||
int folio = 0;
|
||||
QMap<QString, QByteArray> pictures; //name -> png, once each
|
||||
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
|
||||
{
|
||||
++folio;
|
||||
for (QDomElement images : childElements(diagram, QStringLiteral("images"))) {
|
||||
for (QDomElement image : childElements(images, QStringLiteral("image"))) {
|
||||
const QDomText text = image.firstChild().toText();
|
||||
if (text.isNull() || image.hasAttribute(ImageFileAttribute)) continue;
|
||||
const QByteArray base64 = text.data().toLatin1();
|
||||
const auto decoded = QByteArray::fromBase64Encoding(
|
||||
base64, QByteArray::AbortOnBase64DecodingErrors);
|
||||
//Only what comes back exactly leaves the folio
|
||||
if (!decoded || decoded.decoded.toBase64() != base64 || decoded.decoded.isEmpty())
|
||||
continue;
|
||||
const QString name = QStringLiteral("images/%1.%2")
|
||||
.arg(QString::fromLatin1(QCryptographicHash::hash(
|
||||
decoded.decoded, QCryptographicHash::Sha256).toHex()),
|
||||
decoded.decoded.startsWith("\x89PNG") ? QStringLiteral("png")
|
||||
: QStringLiteral("bin"));
|
||||
pictures.insert(name, decoded.decoded);
|
||||
image.removeChild(text);
|
||||
image.setAttribute(ImageFileAttribute, name);
|
||||
}
|
||||
}
|
||||
const QString uuid = diagram.attribute(QStringLiteral("uuid"));
|
||||
const QString file = unique(QStringLiteral("folios/%1.xml").arg(
|
||||
safeSegment(QString(uuid).remove(QLatin1Char('{')).remove(QLatin1Char('}')),
|
||||
QString::number(folio))),
|
||||
&taken);
|
||||
parts.append(QetZip::Entry{file, serialize(diagram), true});
|
||||
root.replaceChild(placeholder(document, file), diagram);
|
||||
}
|
||||
|
||||
//The project's symbols
|
||||
for (QDomElement collection : childElements(root, QStringLiteral("collection")))
|
||||
splitCollection(document, collection, QStringLiteral("elements/"), &parts, &taken);
|
||||
|
||||
//Title block templates
|
||||
for (QDomElement templates : childElements(root, QStringLiteral("titleblocktemplates"))) {
|
||||
for (QDomElement tbt : childElements(templates, QStringLiteral("titleblocktemplate"))) {
|
||||
const QString file = unique(QStringLiteral("titleblocks/%1.titleblock").arg(
|
||||
safeSegment(tbt.attribute(QStringLiteral("name")),
|
||||
QStringLiteral("unnamed"))),
|
||||
&taken);
|
||||
parts.append(QetZip::Entry{file, serialize(tbt), true});
|
||||
templates.replaceChild(placeholder(document, file), tbt);
|
||||
}
|
||||
}
|
||||
|
||||
QDomDocument manifest;
|
||||
QDomElement qetz = manifest.createElement(QStringLiteral("qetz"));
|
||||
qetz.setAttribute(QStringLiteral("format"), Format);
|
||||
qetz.setAttribute(QStringLiteral("min-reader"), Format);
|
||||
if (!writer.isEmpty()) qetz.setAttribute(QStringLiteral("writer"), writer);
|
||||
manifest.appendChild(qetz);
|
||||
|
||||
QList<QetZip::Entry> entries;
|
||||
entries.append(QetZip::Entry{QStringLiteral("mimetype"), QByteArray(MimeType), false});
|
||||
entries.append(QetZip::Entry{QStringLiteral("manifest.xml"), serialize(qetz), true});
|
||||
entries.append(QetZip::Entry{QStringLiteral("project.xml"), serialize(root), true});
|
||||
entries.append(QetZip::Entry{QStringLiteral("project.sqlite"), database, true});
|
||||
entries += parts;
|
||||
for (auto it = pictures.cbegin() ; it != pictures.cend() ; ++it)
|
||||
entries.append(QetZip::Entry{it.key(), it.value(), false}); //PNG is compressed already
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::join
|
||||
Rebuild in @p project the document split() made @p entries from.
|
||||
Refused, with @p error set: not a .qetz, made for a later QElectroTech
|
||||
(the manifest's min-reader), or a part missing or unreadable. Entries
|
||||
nothing names are ignored, so a later format may add some.
|
||||
*/
|
||||
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error)
|
||||
{
|
||||
QHash<QString, QByteArray> files;
|
||||
for (const QetZip::Entry &entry : entries)
|
||||
files.insert(entry.name, entry.data);
|
||||
|
||||
if (files.value(QStringLiteral("mimetype")) != QByteArray(MimeType)) {
|
||||
setError(error, QStringLiteral("not a QElectroTech project (.qetz)"));
|
||||
return false;
|
||||
}
|
||||
QDomDocument manifest;
|
||||
if (!parse(files.value(QStringLiteral("manifest.xml")), &manifest, error))
|
||||
return false;
|
||||
bool ok = false;
|
||||
const int min_reader = manifest.documentElement().attribute(QStringLiteral("min-reader")).toInt(&ok);
|
||||
if (!ok || min_reader > Format) {
|
||||
setError(error, QStringLiteral("made by a later QElectroTech (format %1, this one reads up to %2)")
|
||||
.arg(manifest.documentElement().attribute(QStringLiteral("format")))
|
||||
.arg(Format));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parse(files.value(QStringLiteral("project.xml")), project, error))
|
||||
return false;
|
||||
|
||||
//Parts, a snapshot first: the list is live and shrinks as they go
|
||||
const QDomNodeList list = project->elementsByTagName(PartTag);
|
||||
QList<QDomElement> placeholders;
|
||||
for (int i = 0 ; i < list.size() ; ++i) placeholders << list.at(i).toElement();
|
||||
for (QDomElement part : placeholders)
|
||||
{
|
||||
const QString file = part.attribute(FileAttribute);
|
||||
if (!files.contains(file)) {
|
||||
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
|
||||
return false;
|
||||
}
|
||||
QDomDocument document;
|
||||
QString part_error;
|
||||
if (!parse(files.value(file), &document, &part_error)) {
|
||||
setError(error, QStringLiteral("%1: %2").arg(file, part_error));
|
||||
return false;
|
||||
}
|
||||
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
|
||||
}
|
||||
|
||||
//Pictures back to base64, as the image's first child
|
||||
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
|
||||
for (int i = 0 ; i < images.size() ; ++i)
|
||||
{
|
||||
QDomElement image = images.at(i).toElement();
|
||||
if (!image.hasAttribute(ImageFileAttribute)) continue;
|
||||
const QString file = image.attribute(ImageFileAttribute);
|
||||
if (!files.contains(file)) {
|
||||
setError(error, QStringLiteral("picture \"%1\" is missing").arg(file));
|
||||
return false;
|
||||
}
|
||||
image.removeAttribute(ImageFileAttribute);
|
||||
image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())),
|
||||
image.firstChild());
|
||||
}
|
||||
|
||||
//The engineering data back from project.sqlite (format 2 on)
|
||||
if (files.contains(QStringLiteral("project.sqlite"))
|
||||
&& !QetContainerDb::restore(project->documentElement(),
|
||||
files.value(QStringLiteral("project.sqlite")), error))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETCONTAINER_H
|
||||
#define QETCONTAINER_H
|
||||
|
||||
#include "qetzip.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
|
||||
/**
|
||||
@brief The QetContainer namespace
|
||||
Splits a project document into the parts of a .qetz file and joins
|
||||
them back:
|
||||
|
||||
mimetype first, stored
|
||||
manifest.xml format and oldest format allowed to read it
|
||||
project.xml the project, each part replaced by
|
||||
<qetz-part file="..."/>
|
||||
project.sqlite the engineering data (format 2,
|
||||
QetContainerDb): title-block fields, symbol
|
||||
information, links, wires
|
||||
folios/<uuid>.xml one per folio
|
||||
elements/.../<name> the project's symbol definitions
|
||||
titleblocks/<name>.titleblock
|
||||
images/<sha256>.png folio pictures, once each, not base64
|
||||
|
||||
join(split(d)) is d. Nothing here knows what the parts mean: the
|
||||
project is read and written by the same code as a .qet file.
|
||||
*/
|
||||
namespace QetContainer
|
||||
{
|
||||
extern const char MimeType[];
|
||||
/// The format written; files of a later format are refused by join()
|
||||
/// unless their min-reader allows this one.
|
||||
constexpr int Format = 2;
|
||||
|
||||
QList<QetZip::Entry> split(const QDomDocument &project,
|
||||
const QString &writer = QString(),
|
||||
QString *error = nullptr);
|
||||
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
|
||||
QString *error = nullptr);
|
||||
|
||||
/// Parse @p xml as a .qet is read (QETProject::openFile)
|
||||
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
||||
/// true if @p content is a zip, so a .qetz rather than a .qet
|
||||
bool isZip(const QByteArray &content);
|
||||
/// The project document in @p content, a .qet or a .qetz
|
||||
bool readDocument(const QByteArray &content, QDomDocument *document, QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // QETCONTAINER_H
|
||||
@@ -0,0 +1,773 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetcontainerdb.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
#include <QVariant>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace {
|
||||
const QString KeyAttribute = QStringLiteral("qetz-key");
|
||||
const QString FolioAttribute = QStringLiteral("qetz-folio");
|
||||
//What a moved block leaves where it was. Removing it instead would leave
|
||||
//the whitespace before and after it side by side, read back as one text
|
||||
//node: the parent would come back a child short.
|
||||
const QString MarkerTag = QStringLiteral("qetz-db");
|
||||
const QString MarkerBlock = QStringLiteral("block");
|
||||
|
||||
void leaveMarker(QDomElement block)
|
||||
{
|
||||
QDomElement marker = block.ownerDocument().createElement(MarkerTag);
|
||||
marker.setAttribute(MarkerBlock, block.tagName());
|
||||
block.parentNode().replaceChild(marker, block);
|
||||
}
|
||||
|
||||
//Put @p block where its marker is in @p parent
|
||||
bool replaceMarker(QDomElement parent, const QDomElement &block)
|
||||
{
|
||||
for (QDomElement m = parent.firstChildElement(MarkerTag) ; !m.isNull() ; m = m.nextSiblingElement(MarkerTag)) {
|
||||
if (m.attribute(MarkerBlock) == block.tagName()) {
|
||||
parent.replaceChild(block, m);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList FolioData = {
|
||||
QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"),
|
||||
QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"),
|
||||
QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")};
|
||||
const QStringList ElementData = {
|
||||
QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"),
|
||||
QStringLiteral("freezeLabel")};
|
||||
const QStringList ConductorData = {
|
||||
QStringLiteral("uuid"), QStringLiteral("element1"), QStringLiteral("terminal1"),
|
||||
QStringLiteral("element2"), QStringLiteral("terminal2"), QStringLiteral("num"),
|
||||
QStringLiteral("formula"), QStringLiteral("function"), QStringLiteral("tension_protocol"),
|
||||
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
|
||||
QStringLiteral("cable"), QStringLiteral("bus"), QStringLiteral("freezeLabel")};
|
||||
|
||||
QString columns(const QStringList &names)
|
||||
{
|
||||
QStringList quoted;
|
||||
for (const QString &n : names) quoted << QStringLiteral("\"%1\" TEXT").arg(n);
|
||||
return quoted.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
QString schema()
|
||||
{
|
||||
return QStringLiteral(
|
||||
"CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);"
|
||||
"CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, attrs TEXT);"
|
||||
"CREATE TABLE folio (id TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE, %1,"
|
||||
" props_at INTEGER, props_attrs TEXT);"
|
||||
"CREATE TABLE folio_property (folio_id TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL,"
|
||||
" name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (folio_id, ord));"
|
||||
"CREATE TABLE element (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %2,"
|
||||
" info_at INTEGER, info_attrs TEXT, links_at INTEGER, links_attrs TEXT);"
|
||||
"CREATE TABLE element_info (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
||||
" name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));"
|
||||
"CREATE TABLE link (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
||||
" linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));"
|
||||
"CREATE TABLE conductor (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %3);"
|
||||
"CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE,"
|
||||
" installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);"
|
||||
"CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip,"
|
||||
" pos INTEGER NOT NULL, level INTEGER NOT NULL, element_uuid TEXT,"
|
||||
" PRIMARY KEY (strip_uuid, pos, level));"
|
||||
"CREATE TABLE strip_bridge (uuid TEXT PRIMARY KEY, strip_uuid TEXT NOT NULL REFERENCES terminal_strip,"
|
||||
" ord INTEGER NOT NULL, color TEXT);"
|
||||
"CREATE TABLE strip_bridge_terminal (bridge_uuid TEXT NOT NULL REFERENCES strip_bridge,"
|
||||
" ord INTEGER NOT NULL, element_uuid TEXT, PRIMARY KEY (bridge_uuid, ord));")
|
||||
.arg(columns(FolioData), columns(ElementData), columns(ConductorData));
|
||||
}
|
||||
|
||||
//Owns an sqlite3 handle
|
||||
class Database
|
||||
{
|
||||
public:
|
||||
~Database() { if (m_db) sqlite3_close(m_db); }
|
||||
sqlite3 *m_db = nullptr;
|
||||
bool exec(const QString &sql) {
|
||||
return sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
QString error() const { return QString::fromUtf8(sqlite3_errmsg(m_db)); }
|
||||
};
|
||||
|
||||
//Owns a prepared statement; binds and reads QVariants (null = SQL NULL)
|
||||
class Statement
|
||||
{
|
||||
public:
|
||||
Statement(sqlite3 *db, const QString &sql) {
|
||||
sqlite3_prepare_v2(db, sql.toUtf8().constData(), -1, &m_stmt, nullptr);
|
||||
}
|
||||
~Statement() { sqlite3_finalize(m_stmt); }
|
||||
bool ok() const { return m_stmt; }
|
||||
void bind(const QVariantList &values) {
|
||||
sqlite3_reset(m_stmt);
|
||||
sqlite3_clear_bindings(m_stmt);
|
||||
for (int i = 0 ; i < values.size() ; ++i) {
|
||||
const QVariant &v = values.at(i);
|
||||
if (v.isNull()) {
|
||||
sqlite3_bind_null(m_stmt, i + 1);
|
||||
} else if (v.typeId() == QMetaType::Int) {
|
||||
sqlite3_bind_int(m_stmt, i + 1, v.toInt());
|
||||
} else {
|
||||
const QByteArray utf8 = v.toString().toUtf8();
|
||||
sqlite3_bind_text(m_stmt, i + 1, utf8.constData(), utf8.size(), SQLITE_TRANSIENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool run(const QVariantList &values) {
|
||||
bind(values);
|
||||
return sqlite3_step(m_stmt) == SQLITE_DONE;
|
||||
}
|
||||
bool next() { return sqlite3_step(m_stmt) == SQLITE_ROW; }
|
||||
QVariant value(int column) const {
|
||||
switch (sqlite3_column_type(m_stmt, column)) {
|
||||
case SQLITE_NULL: return QVariant();
|
||||
case SQLITE_INTEGER: return QVariant(sqlite3_column_int(m_stmt, column));
|
||||
default: return QVariant(QString::fromUtf8(
|
||||
reinterpret_cast<const char *>(sqlite3_column_text(m_stmt, column)),
|
||||
sqlite3_column_bytes(m_stmt, column)));
|
||||
}
|
||||
}
|
||||
sqlite3_stmt *m_stmt = nullptr;
|
||||
};
|
||||
|
||||
void setError(QString *error, const QString &text) { if (error) *error = text; }
|
||||
|
||||
QString placeholders(int n)
|
||||
{
|
||||
QStringList q;
|
||||
for (int i = 0 ; i < n ; ++i) q << QStringLiteral("?");
|
||||
return q.join(QLatin1Char(','));
|
||||
}
|
||||
|
||||
//The attributes of @p e but @p skip, and its tag, as JSON
|
||||
QVariant attributesJson(const QDomElement &e, const QString &skip = QString(), bool with_tag = false)
|
||||
{
|
||||
QJsonObject o;
|
||||
if (with_tag) o.insert(QStringLiteral("tag"), e.tagName());
|
||||
const QDomNamedNodeMap map = e.attributes();
|
||||
for (int i = 0 ; i < map.count() ; ++i) {
|
||||
const QDomAttr a = map.item(i).toAttr();
|
||||
if (a.name() != skip) o.insert(a.name(), a.value());
|
||||
}
|
||||
if (o.isEmpty()) return QVariant();
|
||||
return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
void setAttributesJson(QDomElement e, const QVariant &json)
|
||||
{
|
||||
if (json.isNull()) return;
|
||||
const QJsonObject o = QJsonDocument::fromJson(json.toString().toUtf8()).object();
|
||||
for (auto it = o.begin() ; it != o.end() ; ++it)
|
||||
if (it.key() != QLatin1String("tag") && it.key() != QLatin1String("qetz-ws"))
|
||||
e.setAttribute(it.key(), it.value().toString());
|
||||
}
|
||||
|
||||
//A list block's attributes, and the whitespace between its entries if
|
||||
//it has any (a document read from a file does; a save has none):
|
||||
//"qetz-ws" holds, for each entry and after the last, the text before it.
|
||||
QVariant blockJson(const QDomElement &block)
|
||||
{
|
||||
QJsonObject o;
|
||||
const QDomNamedNodeMap map = block.attributes();
|
||||
for (int i = 0 ; i < map.count() ; ++i) {
|
||||
const QDomAttr a = map.item(i).toAttr();
|
||||
o.insert(a.name(), a.value());
|
||||
}
|
||||
QJsonArray ws;
|
||||
QString pending;
|
||||
bool any = false;
|
||||
for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
|
||||
if (n.isText()) { pending += n.nodeValue(); any = true; }
|
||||
else { ws.append(pending); pending.clear(); }
|
||||
}
|
||||
ws.append(pending);
|
||||
if (any) o.insert(QStringLiteral("qetz-ws"), ws);
|
||||
if (o.isEmpty()) return QVariant();
|
||||
return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
//The whitespace blockJson() kept, back between the entries of @p block
|
||||
void restoreWhitespace(QDomElement block, const QVariant &json)
|
||||
{
|
||||
if (json.isNull()) return;
|
||||
const QJsonArray ws = QJsonDocument::fromJson(json.toString().toUtf8()).object()
|
||||
.value(QStringLiteral("qetz-ws")).toArray();
|
||||
if (ws.isEmpty()) return;
|
||||
QDomDocument document = block.ownerDocument();
|
||||
QList<QDomNode> entries;
|
||||
for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) entries << n;
|
||||
for (int i = 0 ; i < entries.size() && i < ws.size() ; ++i)
|
||||
if (!ws.at(i).toString().isEmpty())
|
||||
block.insertBefore(document.createTextNode(ws.at(i).toString()), entries.at(i));
|
||||
if (ws.size() > entries.size() && !ws.last().toString().isEmpty())
|
||||
block.appendChild(document.createTextNode(ws.last().toString()));
|
||||
}
|
||||
|
||||
//The value of a list entry: its only child, a text node, or none
|
||||
bool simpleValue(const QDomElement &e, QVariant *value)
|
||||
{
|
||||
const QDomNodeList kids = e.childNodes();
|
||||
if (kids.size() == 0) { *value = QVariant(); return true; }
|
||||
if (kids.size() == 1 && kids.at(0).isText() && !kids.at(0).isCDATASection()) {
|
||||
*value = kids.at(0).nodeValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//A list block -- <properties>, <elementInformations>, <links_uuids> --
|
||||
//this can move: entries named @p entry with simple values, and
|
||||
//whitespace between them that a save would write anyway.
|
||||
bool listBlock(const QDomElement &block, const QString &entry)
|
||||
{
|
||||
for (QDomNode n = block.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
|
||||
if (n.isElement()) {
|
||||
QVariant v;
|
||||
if (n.toElement().tagName() != entry || !simpleValue(n.toElement(), &v)) return false;
|
||||
} else if (!(n.isText() && !n.isCDATASection() && n.nodeValue().trimmed().isEmpty())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int childIndex(const QDomNode &child)
|
||||
{
|
||||
int i = 0;
|
||||
for (QDomNode n = child.parentNode().firstChild() ; !n.isNull() ; n = n.nextSibling(), ++i)
|
||||
if (n == child) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
QVariantList take(QDomElement e, const QStringList &names)
|
||||
{
|
||||
QVariantList values;
|
||||
for (const QString &n : names) {
|
||||
values << (e.hasAttribute(n) ? QVariant(e.attribute(n)) : QVariant());
|
||||
e.removeAttribute(n);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void put(QDomElement e, const QStringList &names, const QVariantList &values, int from)
|
||||
{
|
||||
for (int i = 0 ; i < names.size() ; ++i)
|
||||
if (!values.at(from + i).isNull()) e.setAttribute(names.at(i), values.at(from + i).toString());
|
||||
}
|
||||
|
||||
//Same tags, attributes, text and child order
|
||||
bool sameTree(const QDomNode &a, const QDomNode &b)
|
||||
{
|
||||
if (a.nodeType() != b.nodeType() || a.nodeName() != b.nodeName()) return false;
|
||||
if (!a.isElement() && a.nodeValue() != b.nodeValue()) return false;
|
||||
if (a.isElement()) {
|
||||
const QDomNamedNodeMap am = a.attributes(), bm = b.attributes();
|
||||
if (am.count() != bm.count()) return false;
|
||||
for (int i = 0 ; i < am.count() ; ++i) {
|
||||
const QDomAttr x = am.item(i).toAttr();
|
||||
if (!bm.contains(x.name()) || bm.namedItem(x.name()).nodeValue() != x.value()) return false;
|
||||
}
|
||||
}
|
||||
QDomNode ca = a.firstChild(), cb = b.firstChild();
|
||||
for ( ; !ca.isNull() && !cb.isNull() ; ca = ca.nextSibling(), cb = cb.nextSibling())
|
||||
if (!sameTree(ca, cb)) return false;
|
||||
return ca.isNull() && cb.isNull();
|
||||
}
|
||||
|
||||
bool hasWhitespaceText(const QDomNode &node)
|
||||
{
|
||||
for (QDomNode n = node.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
|
||||
if (n.isText() && n.nodeValue().trimmed().isEmpty()) return true;
|
||||
if (hasWhitespaceText(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList StripData = {
|
||||
QStringLiteral("installation"), QStringLiteral("location"), QStringLiteral("name"),
|
||||
QStringLiteral("comment"), QStringLiteral("description")};
|
||||
|
||||
struct StripRows
|
||||
{
|
||||
QList<QVariantList> strips, terminals, bridges, bridge_terminals;
|
||||
};
|
||||
|
||||
//<terminal_strips> as TerminalStrip::toXml() writes it, from @p rows
|
||||
QDomElement stripsXml(QDomDocument &document, const StripRows &rows)
|
||||
{
|
||||
QDomElement root = document.createElement(QStringLiteral("terminal_strips"));
|
||||
for (const QVariantList &st : rows.strips) {
|
||||
const QString uuid = st.at(0).toString();
|
||||
QDomElement strip = root.appendChild(document.createElement(QStringLiteral("terminal_strip"))).toElement();
|
||||
QDomElement data = strip.appendChild(document.createElement(QStringLiteral("terminal_strip_data"))).toElement();
|
||||
data.setAttribute(QStringLiteral("uuid"), uuid);
|
||||
QDomElement infos = data.appendChild(document.createElement(QStringLiteral("informations"))).toElement();
|
||||
for (int i = 0 ; i < StripData.size() ; ++i) {
|
||||
const QVariant v = st.at(2 + i);
|
||||
if (v.isNull()) continue;
|
||||
QDomElement info = infos.appendChild(document.createElement(QStringLiteral("information"))).toElement();
|
||||
info.setAttribute(QStringLiteral("name"), StripData.at(i));
|
||||
info.appendChild(document.createTextNode(v.toString()));
|
||||
}
|
||||
QDomElement layout = strip.appendChild(document.createElement(QStringLiteral("layout"))).toElement();
|
||||
QDomElement physical;
|
||||
int last_pos = -1;
|
||||
for (const QVariantList &t : rows.terminals) {
|
||||
if (t.at(0).toString() != uuid) continue;
|
||||
if (t.at(1).toInt() != last_pos) {
|
||||
physical = layout.appendChild(document.createElement(QStringLiteral("physical_terminal"))).toElement();
|
||||
last_pos = t.at(1).toInt();
|
||||
}
|
||||
QDomElement real = physical.appendChild(document.createElement(QStringLiteral("real_terminal"))).toElement();
|
||||
if (!t.at(3).isNull()) real.setAttribute(QStringLiteral("element_uuid"), t.at(3).toString());
|
||||
}
|
||||
for (const QVariantList &b : rows.bridges) {
|
||||
if (b.at(1).toString() != uuid) continue;
|
||||
QDomElement bridge = strip.appendChild(document.createElement(QStringLiteral("terminal_strip_bridge"))).toElement();
|
||||
bridge.setAttribute(QStringLiteral("uuid"), b.at(0).toString());
|
||||
if (!b.at(3).isNull()) bridge.setAttribute(QStringLiteral("color"), b.at(3).toString());
|
||||
QDomElement reals = bridge.appendChild(document.createElement(QStringLiteral("real_terminals"))).toElement();
|
||||
for (const QVariantList &bt : rows.bridge_terminals) {
|
||||
if (bt.at(0).toString() != b.at(0).toString()) continue;
|
||||
QDomElement real = reals.appendChild(document.createElement(QStringLiteral("real_terminal"))).toElement();
|
||||
if (!bt.at(2).isNull()) real.setAttribute(QStringLiteral("uuid"), bt.at(2).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
//The rows of @p block, if rebuilding them gives @p block back exactly
|
||||
bool stripRows(const QDomElement &block, StripRows *rows)
|
||||
{
|
||||
if (block.attributes().count() || hasWhitespaceText(block)) return false;
|
||||
QSet<QString> strip_uuids, bridge_uuids;
|
||||
int ord = 0;
|
||||
for (QDomElement strip = block.firstChildElement() ; !strip.isNull() ; strip = strip.nextSiblingElement()) {
|
||||
const QDomElement data = strip.firstChildElement(QStringLiteral("terminal_strip_data"));
|
||||
const QString uuid = data.attribute(QStringLiteral("uuid"));
|
||||
if (uuid.isEmpty() || strip_uuids.contains(uuid)) return false;
|
||||
strip_uuids.insert(uuid);
|
||||
QHash<QString, QString> info;
|
||||
for (QDomElement i = data.firstChildElement(QStringLiteral("informations")).firstChildElement() ;
|
||||
!i.isNull() ; i = i.nextSiblingElement())
|
||||
info.insert(i.attribute(QStringLiteral("name")), i.text());
|
||||
QVariantList row{uuid, ord++};
|
||||
for (const QString &n : StripData) row << (info.contains(n) ? QVariant(info.value(n)) : QVariant());
|
||||
rows->strips << row;
|
||||
int pos = 0;
|
||||
for (QDomElement phy = strip.firstChildElement(QStringLiteral("layout")).firstChildElement() ;
|
||||
!phy.isNull() ; phy = phy.nextSiblingElement(), ++pos) {
|
||||
int level = 0;
|
||||
for (QDomElement real = phy.firstChildElement() ; !real.isNull() ; real = real.nextSiblingElement())
|
||||
rows->terminals << QVariantList{uuid, pos, level++, real.hasAttribute(QStringLiteral("element_uuid"))
|
||||
? QVariant(real.attribute(QStringLiteral("element_uuid"))) : QVariant()};
|
||||
}
|
||||
int b_ord = 0;
|
||||
for (QDomElement br = strip.firstChildElement(QStringLiteral("terminal_strip_bridge")) ;
|
||||
!br.isNull() ; br = br.nextSiblingElement(QStringLiteral("terminal_strip_bridge"))) {
|
||||
const QString buuid = br.attribute(QStringLiteral("uuid"));
|
||||
if (buuid.isEmpty() || bridge_uuids.contains(buuid)) return false;
|
||||
bridge_uuids.insert(buuid);
|
||||
rows->bridges << QVariantList{buuid, uuid, b_ord++, br.hasAttribute(QStringLiteral("color"))
|
||||
? QVariant(br.attribute(QStringLiteral("color"))) : QVariant()};
|
||||
int k = 0;
|
||||
for (QDomElement real = br.firstChildElement(QStringLiteral("real_terminals")).firstChildElement() ;
|
||||
!real.isNull() ; real = real.nextSiblingElement())
|
||||
rows->bridge_terminals << QVariantList{buuid, k++, real.hasAttribute(QStringLiteral("uuid"))
|
||||
? QVariant(real.attribute(QStringLiteral("uuid"))) : QVariant()};
|
||||
}
|
||||
}
|
||||
QDomDocument scratch;
|
||||
return sameTree(block, stripsXml(scratch, *rows));
|
||||
}
|
||||
|
||||
QList<QDomElement> children(const QDomElement &parent, const QString &tag)
|
||||
{
|
||||
QList<QDomElement> list;
|
||||
for (QDomElement e = parent.firstChildElement(tag) ; !e.isNull() ; e = e.nextSiblingElement(tag))
|
||||
list << e;
|
||||
return list;
|
||||
}
|
||||
|
||||
//uuid when present and unique among @p items, else prefix + count
|
||||
QStringList keys(const QList<QDomElement> &items, const QString &prefix)
|
||||
{
|
||||
QHash<QString, int> seen;
|
||||
for (const QDomElement &e : items)
|
||||
if (e.hasAttribute(QStringLiteral("uuid"))) ++seen[e.attribute(QStringLiteral("uuid"))];
|
||||
QStringList out;
|
||||
int made_up = 0;
|
||||
for (const QDomElement &e : items) {
|
||||
const QString uuid = e.attribute(QStringLiteral("uuid"));
|
||||
out << ((!uuid.isEmpty() && seen.value(uuid) == 1)
|
||||
? uuid : prefix + QString::number(++made_up));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainerDb::extract
|
||||
Move the engineering data of @p project (the <project> element, its
|
||||
folios still in it) into a new database.
|
||||
@return the database file, or an empty array with @p error set
|
||||
*/
|
||||
QByteArray QetContainerDb::extract(QDomElement project, QString *error)
|
||||
{
|
||||
Database db;
|
||||
if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK || !db.exec(schema())) {
|
||||
setError(error, QStringLiteral("cannot create the project database"));
|
||||
return {};
|
||||
}
|
||||
db.exec(QStringLiteral("BEGIN"));
|
||||
|
||||
Statement meta(db.m_db, QStringLiteral("INSERT INTO meta VALUES (?,?)"));
|
||||
Statement project_property(db.m_db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?)"));
|
||||
Statement folio(db.m_db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?,?)").arg(placeholders(FolioData.size())));
|
||||
Statement folio_property(db.m_db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?)"));
|
||||
Statement element(db.m_db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?,?,?)").arg(placeholders(ElementData.size())));
|
||||
Statement element_info(db.m_db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?)"));
|
||||
Statement link(db.m_db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)"));
|
||||
Statement conductor(db.m_db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size())));
|
||||
bool ok = meta.ok() && project_property.ok() && folio.ok() && folio_property.ok()
|
||||
&& element.ok() && element_info.ok() && link.ok() && conductor.ok();
|
||||
|
||||
ok = ok && meta.run({QStringLiteral("format"), QStringLiteral("2")});
|
||||
|
||||
//<properties> of a block, as rows of @p rows (with @p prefix values)
|
||||
auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) {
|
||||
int ord = 0;
|
||||
for (QDomElement p : children(block, QStringLiteral("property"))) {
|
||||
QVariant value;
|
||||
simpleValue(p, &value);
|
||||
ok = ok && rows.run(QVariantList(prefix) << ord++ << (p.hasAttribute(QStringLiteral("name"))
|
||||
? QVariant(p.attribute(QStringLiteral("name"))) : QVariant())
|
||||
<< value << attributesJson(p, QStringLiteral("name"), true));
|
||||
}
|
||||
};
|
||||
|
||||
//Project properties
|
||||
const QDomElement project_props = project.firstChildElement(QStringLiteral("properties"));
|
||||
if (!project_props.isNull() && listBlock(project_props, QStringLiteral("property"))) {
|
||||
ok = ok && meta.run({QStringLiteral("props_at"), QString::number(childIndex(project_props))});
|
||||
ok = ok && meta.run({QStringLiteral("props_attrs"), blockJson(project_props)});
|
||||
int ord = 0;
|
||||
for (QDomElement p : children(project_props, QStringLiteral("property"))) {
|
||||
QVariant value;
|
||||
simpleValue(p, &value);
|
||||
ok = ok && project_property.run({ord++, p.hasAttribute(QStringLiteral("name"))
|
||||
? QVariant(p.attribute(QStringLiteral("name"))) : QVariant(),
|
||||
value, attributesJson(p, QStringLiteral("name"), true)});
|
||||
}
|
||||
leaveMarker(project_props);
|
||||
}
|
||||
|
||||
const QList<QDomElement> diagrams = children(project, QStringLiteral("diagram"));
|
||||
QList<QDomElement> all_elements, all_conductors;
|
||||
for (const QDomElement &d : diagrams) {
|
||||
for (const QDomElement &list : children(d, QStringLiteral("elements")))
|
||||
all_elements += children(list, QStringLiteral("element"));
|
||||
for (const QDomElement &list : children(d, QStringLiteral("conductors")))
|
||||
all_conductors += children(list, QStringLiteral("conductor"));
|
||||
}
|
||||
const QStringList element_keys = keys(all_elements, QStringLiteral("e"));
|
||||
const QStringList conductor_keys = keys(all_conductors, QStringLiteral("c"));
|
||||
int e_index = 0, c_index = 0;
|
||||
|
||||
int pos = 0;
|
||||
for (QDomElement d : diagrams)
|
||||
{
|
||||
const QString id = QStringLiteral("f%1").arg(++pos);
|
||||
QVariantList row{id, pos};
|
||||
row += take(d, FolioData);
|
||||
QDomElement props = d.firstChildElement(QStringLiteral("properties"));
|
||||
const bool move_props = !props.isNull() && listBlock(props, QStringLiteral("property"));
|
||||
row << (move_props ? QVariant(childIndex(props)) : QVariant())
|
||||
<< (move_props ? blockJson(props) : QVariant());
|
||||
ok = ok && folio.run(row);
|
||||
if (move_props) {
|
||||
moveProperties(props, folio_property, {id});
|
||||
leaveMarker(props);
|
||||
}
|
||||
d.setAttribute(FolioAttribute, id);
|
||||
|
||||
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
||||
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
||||
const QString key = element_keys.at(e_index++);
|
||||
QVariantList erow{key, id};
|
||||
erow += take(e, ElementData);
|
||||
QDomElement info = e.firstChildElement(QStringLiteral("elementInformations"));
|
||||
QDomElement links = e.firstChildElement(QStringLiteral("links_uuids"));
|
||||
if (!info.isNull() && !listBlock(info, QStringLiteral("elementInformation"))) info = QDomElement();
|
||||
if (!links.isNull() && !listBlock(links, QStringLiteral("link_uuid"))) links = QDomElement();
|
||||
const int info_at = info.isNull() ? -1 : childIndex(info);
|
||||
const int links_at = links.isNull() ? -1 : childIndex(links);
|
||||
erow << (info_at < 0 ? QVariant() : QVariant(info_at))
|
||||
<< (info_at < 0 ? QVariant() : blockJson(info))
|
||||
<< (links_at < 0 ? QVariant() : QVariant(links_at))
|
||||
<< (links_at < 0 ? QVariant() : blockJson(links));
|
||||
ok = ok && element.run(erow);
|
||||
if (info_at >= 0) {
|
||||
int ord = 0;
|
||||
for (QDomElement i : children(info, QStringLiteral("elementInformation"))) {
|
||||
QVariant value;
|
||||
simpleValue(i, &value);
|
||||
ok = ok && element_info.run({key, ord++, i.hasAttribute(QStringLiteral("name"))
|
||||
? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(),
|
||||
value, attributesJson(i, QStringLiteral("name"), true)});
|
||||
}
|
||||
}
|
||||
if (links_at >= 0) {
|
||||
int ord = 0;
|
||||
for (QDomElement l : children(links, QStringLiteral("link_uuid")))
|
||||
ok = ok && link.run({key, ord++, l.hasAttribute(QStringLiteral("uuid"))
|
||||
? QVariant(l.attribute(QStringLiteral("uuid"))) : QVariant(),
|
||||
attributesJson(l, QStringLiteral("uuid"), true)});
|
||||
}
|
||||
if (info_at >= 0) leaveMarker(info);
|
||||
if (links_at >= 0) leaveMarker(links);
|
||||
e.setAttribute(KeyAttribute, key);
|
||||
}
|
||||
}
|
||||
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
||||
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
||||
const QString key = conductor_keys.at(c_index++);
|
||||
QVariantList crow{key, id};
|
||||
crow += take(c, ConductorData);
|
||||
ok = ok && conductor.run(crow);
|
||||
c.setAttribute(KeyAttribute, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Terminal strips, when their rows rebuild them exactly
|
||||
const QDomElement strips = project.firstChildElement(QStringLiteral("terminal_strips"));
|
||||
StripRows strip_rows;
|
||||
if (!strips.isNull() && stripRows(strips, &strip_rows)) {
|
||||
Statement st(db.m_db, QStringLiteral("INSERT INTO terminal_strip VALUES (?,?,?,?,?,?,?)"));
|
||||
Statement tt(db.m_db, QStringLiteral("INSERT INTO strip_terminal VALUES (?,?,?,?)"));
|
||||
Statement bt(db.m_db, QStringLiteral("INSERT INTO strip_bridge VALUES (?,?,?,?)"));
|
||||
Statement btt(db.m_db, QStringLiteral("INSERT INTO strip_bridge_terminal VALUES (?,?,?)"));
|
||||
for (const QVariantList &r : strip_rows.strips) ok = ok && st.run(r);
|
||||
for (const QVariantList &r : strip_rows.terminals) ok = ok && tt.run(r);
|
||||
for (const QVariantList &r : strip_rows.bridges) ok = ok && bt.run(r);
|
||||
for (const QVariantList &r : strip_rows.bridge_terminals) ok = ok && btt.run(r);
|
||||
ok = ok && meta.run({QStringLiteral("terminal_strips"), QStringLiteral("1")});
|
||||
leaveMarker(strips);
|
||||
}
|
||||
|
||||
if (!ok || !db.exec(QStringLiteral("COMMIT"))) {
|
||||
setError(error, QStringLiteral("cannot fill the project database: %1").arg(db.error()));
|
||||
return {};
|
||||
}
|
||||
|
||||
sqlite3_int64 size = 0;
|
||||
unsigned char *bytes = sqlite3_serialize(db.m_db, "main", &size, 0);
|
||||
if (!bytes) {
|
||||
setError(error, QStringLiteral("cannot write the project database"));
|
||||
return {};
|
||||
}
|
||||
const QByteArray file(reinterpret_cast<const char *>(bytes), qsizetype(size));
|
||||
sqlite3_free(bytes);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainerDb::restore
|
||||
Put the engineering data in @p database back into @p project (the
|
||||
<project> element, its folios joined back already).
|
||||
*/
|
||||
bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QString *error)
|
||||
{
|
||||
QDomDocument document = project.ownerDocument();
|
||||
Database db;
|
||||
if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) {
|
||||
setError(error, QStringLiteral("cannot open the project database"));
|
||||
return false;
|
||||
}
|
||||
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
|
||||
if (!copy) { setError(error, QStringLiteral("out of memory")); return false; }
|
||||
memcpy(copy, database.constData(), size_t(database.size()));
|
||||
if (sqlite3_deserialize(db.m_db, "main", copy, database.size(), database.size(),
|
||||
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) {
|
||||
setError(error, QStringLiteral("the project database cannot be read"));
|
||||
return false;
|
||||
}
|
||||
//A file is untrusted: nothing in its schema may run, and every name
|
||||
//read must be a plain table.
|
||||
db.exec(QStringLiteral("PRAGMA trusted_schema = OFF"));
|
||||
db.exec(QStringLiteral("PRAGMA query_only = ON"));
|
||||
for (const char *table : {"meta", "project_property", "folio", "folio_property",
|
||||
"element", "element_info", "link", "conductor", "terminal_strip",
|
||||
"strip_terminal", "strip_bridge", "strip_bridge_terminal"}) {
|
||||
Statement kind(db.m_db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?"));
|
||||
kind.bind({QString::fromLatin1(table)});
|
||||
if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) {
|
||||
setError(error, QStringLiteral("the project database has no table %1").arg(QLatin1String(table)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
|
||||
const QVariant &attrs, const QString &default_tag) {
|
||||
const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object();
|
||||
QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag));
|
||||
if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString());
|
||||
setAttributesJson(e, attrs);
|
||||
if (!value.isNull()) e.appendChild(document.createTextNode(value.toString()));
|
||||
return e;
|
||||
};
|
||||
|
||||
//Project properties
|
||||
QHash<QString, QVariant> meta;
|
||||
{
|
||||
Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta"));
|
||||
while (s.next()) meta.insert(s.value(0).toString(), s.value(1));
|
||||
}
|
||||
if (meta.contains(QStringLiteral("props_at"))) {
|
||||
QDomElement props = document.createElement(QStringLiteral("properties"));
|
||||
setAttributesJson(props, meta.value(QStringLiteral("props_attrs")));
|
||||
Statement s(db.m_db, QStringLiteral("SELECT name, value, attrs FROM project_property ORDER BY ord"));
|
||||
while (s.next())
|
||||
props.appendChild(entry(s.value(0), QStringLiteral("name"), s.value(1), s.value(2),
|
||||
QStringLiteral("property")));
|
||||
restoreWhitespace(props, meta.value(QStringLiteral("props_attrs")));
|
||||
if (!replaceMarker(project, props)) {
|
||||
setError(error, QStringLiteral("the project's properties have no place"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (meta.contains(QStringLiteral("terminal_strips"))) {
|
||||
StripRows rows;
|
||||
auto read = [&db](const QString &sql, int columns, QList<QVariantList> *out) {
|
||||
Statement s(db.m_db, sql);
|
||||
while (s.next()) {
|
||||
QVariantList r;
|
||||
for (int i = 0 ; i < columns ; ++i) r << s.value(i);
|
||||
*out << r;
|
||||
}
|
||||
};
|
||||
read(QStringLiteral("SELECT * FROM terminal_strip ORDER BY ord"), 7, &rows.strips);
|
||||
read(QStringLiteral("SELECT * FROM strip_terminal ORDER BY strip_uuid, pos, level"), 4, &rows.terminals);
|
||||
read(QStringLiteral("SELECT * FROM strip_bridge ORDER BY ord"), 4, &rows.bridges);
|
||||
read(QStringLiteral("SELECT * FROM strip_bridge_terminal ORDER BY bridge_uuid, ord"), 3, &rows.bridge_terminals);
|
||||
if (!replaceMarker(project, stripsXml(document, rows))) {
|
||||
setError(error, QStringLiteral("the terminal strips have no place"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (QDomElement d : children(project, QStringLiteral("diagram")))
|
||||
{
|
||||
const QString id = d.attribute(FolioAttribute);
|
||||
if (id.isEmpty()) continue;
|
||||
d.removeAttribute(FolioAttribute);
|
||||
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE id = ?")
|
||||
.arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
||||
f.bind({id});
|
||||
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
|
||||
QVariantList values;
|
||||
for (int i = 0 ; i < FolioData.size() + 2 ; ++i) values << f.value(i);
|
||||
put(d, FolioData, values, 0);
|
||||
if (!values.at(FolioData.size()).isNull()) {
|
||||
QDomElement props = document.createElement(QStringLiteral("properties"));
|
||||
setAttributesJson(props, values.at(FolioData.size() + 1));
|
||||
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE folio_id = ? ORDER BY ord"));
|
||||
p.bind({id});
|
||||
while (p.next())
|
||||
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
|
||||
QStringLiteral("property")));
|
||||
restoreWhitespace(props, values.at(FolioData.size() + 1));
|
||||
if (!replaceMarker(d, props)) {
|
||||
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
||||
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
||||
if (!e.hasAttribute(KeyAttribute)) continue;
|
||||
const QString key = e.attribute(KeyAttribute);
|
||||
e.removeAttribute(KeyAttribute);
|
||||
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE key = ?")
|
||||
.arg(QStringLiteral("\"") + ElementData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
||||
s.bind({key});
|
||||
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
|
||||
QVariantList v;
|
||||
for (int i = 0 ; i < ElementData.size() + 4 ; ++i) v << s.value(i);
|
||||
put(e, ElementData, v, 0);
|
||||
const int n = ElementData.size();
|
||||
QDomElement info, links;
|
||||
if (!v.at(n).isNull()) {
|
||||
info = document.createElement(QStringLiteral("elementInformations"));
|
||||
setAttributesJson(info, v.at(n + 1));
|
||||
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord"));
|
||||
i.bind({key});
|
||||
while (i.next())
|
||||
info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2),
|
||||
QStringLiteral("elementInformation")));
|
||||
restoreWhitespace(info, v.at(n + 1));
|
||||
}
|
||||
if (!v.at(n + 2).isNull()) {
|
||||
links = document.createElement(QStringLiteral("links_uuids"));
|
||||
setAttributesJson(links, v.at(n + 3));
|
||||
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord"));
|
||||
l.bind({key});
|
||||
while (l.next())
|
||||
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
|
||||
QStringLiteral("link_uuid")));
|
||||
restoreWhitespace(links, v.at(n + 3));
|
||||
}
|
||||
if ((!info.isNull() && !replaceMarker(e, info))
|
||||
|| (!links.isNull() && !replaceMarker(e, links))) {
|
||||
setError(error, QStringLiteral("symbol %1's information has no place").arg(key));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
||||
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
||||
if (!c.hasAttribute(KeyAttribute)) continue;
|
||||
const QString key = c.attribute(KeyAttribute);
|
||||
c.removeAttribute(KeyAttribute);
|
||||
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE key = ?")
|
||||
.arg(QStringLiteral("\"") + ConductorData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
||||
s.bind({key});
|
||||
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
|
||||
QVariantList v;
|
||||
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i);
|
||||
put(c, ConductorData, v, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETCONTAINERDB_H
|
||||
#define QETCONTAINERDB_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDomElement>
|
||||
|
||||
/**
|
||||
@brief The QetContainerDb namespace
|
||||
The engineering data of a project, moved between its document and the
|
||||
project.sqlite of a .qetz: what a folio's title block says, a symbol's
|
||||
information, a coil's contacts, a wire's ends and number. In a .qetz
|
||||
these live only in the database; the folio files keep where things are
|
||||
drawn. extract() takes them out of the document into a database,
|
||||
restore() puts them back, so that restore(extract(d)) is d.
|
||||
|
||||
Each moved item leaves its key in the document (qetz-key, qetz-folio):
|
||||
its uuid where that is unique, else a made-up one. A block of a shape
|
||||
this does not model -- a value holding elements, say -- stays in the
|
||||
document, so nothing can be lost by moving.
|
||||
*/
|
||||
namespace QetContainerDb
|
||||
{
|
||||
QByteArray extract(QDomElement project, QString *error = nullptr);
|
||||
bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // QETCONTAINERDB_H
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetzip.h"
|
||||
|
||||
#include "miniz/miniz.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
|
||||
namespace {
|
||||
void setError(QString *error, const QString &text)
|
||||
{
|
||||
if (error) *error = text;
|
||||
}
|
||||
|
||||
//A name read from an archive is only ever a key, but refuse the ones a
|
||||
//careless caller could turn into a path outside a folder: absolute,
|
||||
//with a drive, with "..", or not valid UTF-8.
|
||||
bool safeName(const QString &name)
|
||||
{
|
||||
if (name.isEmpty() || name.startsWith(QLatin1Char('/'))
|
||||
|| name.contains(QLatin1Char('\\')) || name.contains(QLatin1Char(':'))
|
||||
|| name.contains(QChar::ReplacementCharacter)) {
|
||||
return false;
|
||||
}
|
||||
for (const QString &part : name.split(QLatin1Char('/'))) {
|
||||
if (part == QLatin1String("..")) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetZip::toBytes
|
||||
@return the zip holding @p entries in their order, or an empty array
|
||||
with @p error set
|
||||
*/
|
||||
QByteArray QetZip::toBytes(const QList<Entry> &entries, QString *error)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
if (!mz_zip_writer_init_heap(&zip, 0, 0)) {
|
||||
setError(error, QStringLiteral("cannot start a zip archive"));
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QString> names;
|
||||
for (const Entry &entry : entries)
|
||||
{
|
||||
if (!safeName(entry.name) || names.contains(entry.name)) {
|
||||
setError(error, QStringLiteral("invalid or repeated entry name \"%1\"").arg(entry.name));
|
||||
mz_zip_writer_end(&zip);
|
||||
return {};
|
||||
}
|
||||
names.insert(entry.name);
|
||||
const QByteArray name = entry.name.toUtf8();
|
||||
const mz_uint level = entry.compress ? MZ_DEFAULT_LEVEL : MZ_NO_COMPRESSION;
|
||||
if (!mz_zip_writer_add_mem(&zip, name.constData(), entry.data.constData(),
|
||||
size_t(entry.data.size()), level)) {
|
||||
setError(error, QStringLiteral("cannot add \"%1\": %2")
|
||||
.arg(entry.name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip)))));
|
||||
mz_zip_writer_end(&zip);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void *buffer = nullptr;
|
||||
size_t size = 0;
|
||||
if (!mz_zip_writer_finalize_heap_archive(&zip, &buffer, &size)) {
|
||||
setError(error, QStringLiteral("cannot finish the zip archive"));
|
||||
mz_zip_writer_end(&zip);
|
||||
return {};
|
||||
}
|
||||
const QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
||||
mz_zip_writer_end(&zip); //frees buffer
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetZip::write
|
||||
Write @p entries to @p path, replacing it only once the whole archive
|
||||
is written.
|
||||
*/
|
||||
bool QetZip::write(const QString &path, const QList<Entry> &entries, QString *error)
|
||||
{
|
||||
const QByteArray bytes = toBytes(entries, error);
|
||||
if (bytes.isEmpty()) return false;
|
||||
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
setError(error, file.errorString());
|
||||
return false;
|
||||
}
|
||||
if (file.write(bytes) != bytes.size() || !file.commit()) {
|
||||
setError(error, file.errorString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetZip::fromBytes
|
||||
Read every file entry of the zip @p zip into @p entries, in archive
|
||||
order. Folders are skipped. An encrypted entry, an unsafe or repeated
|
||||
name, a method miniz cannot read, or more than MaxEntries entries or
|
||||
MaxUncompressedSize bytes refuse the whole archive.
|
||||
*/
|
||||
bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QString *error)
|
||||
{
|
||||
if (!entries) return false;
|
||||
entries->clear();
|
||||
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
if (!mz_zip_reader_init_mem(&zip, zip_bytes.constData(), size_t(zip_bytes.size()), 0)) {
|
||||
setError(error, QStringLiteral("not a zip archive: %1")
|
||||
.arg(QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip)))));
|
||||
return false;
|
||||
}
|
||||
|
||||
const mz_uint count = mz_zip_reader_get_num_files(&zip);
|
||||
if (count > mz_uint(MaxEntries)) {
|
||||
setError(error, QStringLiteral("too many entries (%1)").arg(count));
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
|
||||
qint64 total = 0;
|
||||
QSet<QString> names;
|
||||
for (mz_uint i = 0 ; i < count ; ++i)
|
||||
{
|
||||
mz_zip_archive_file_stat stat;
|
||||
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
|
||||
setError(error, QStringLiteral("unreadable entry %1").arg(i));
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
if (stat.m_is_directory) continue;
|
||||
|
||||
const QString name = QString::fromUtf8(stat.m_filename);
|
||||
if (stat.m_is_encrypted || !stat.m_is_supported || !safeName(name)
|
||||
|| names.contains(name)) {
|
||||
setError(error, QStringLiteral("entry \"%1\" cannot be read").arg(name));
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
total += qint64(stat.m_uncomp_size);
|
||||
if (total > MaxUncompressedSize) {
|
||||
setError(error, QStringLiteral("archive too large once unpacked"));
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t size = 0;
|
||||
void *data = mz_zip_reader_extract_to_heap(&zip, i, &size, 0);
|
||||
if (!data) {
|
||||
setError(error, QStringLiteral("cannot unpack \"%1\": %2")
|
||||
.arg(name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip)))));
|
||||
mz_zip_reader_end(&zip);
|
||||
return false;
|
||||
}
|
||||
Entry entry;
|
||||
entry.name = name;
|
||||
entry.data = QByteArray(static_cast<const char *>(data), qsizetype(size));
|
||||
entry.compress = stat.m_method != 0;
|
||||
mz_free(data);
|
||||
names.insert(name);
|
||||
entries->append(entry);
|
||||
}
|
||||
mz_zip_reader_end(&zip);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetZip::read
|
||||
fromBytes() on the file at @p path
|
||||
*/
|
||||
bool QetZip::read(const QString &path, QList<Entry> *entries, QString *error)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
setError(error, file.errorString());
|
||||
return false;
|
||||
}
|
||||
return fromBytes(file.readAll(), entries, error);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2006-2026 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETZIP_H
|
||||
#define QETZIP_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
@brief The QetZip namespace
|
||||
Reads and writes the zip file a .qetz project is stored in: plain ZIP,
|
||||
Deflate, as ODF and OOXML documents are, so any unzip tool opens it.
|
||||
A whole archive is read into, or written from, memory -- projects are
|
||||
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
||||
save leaves the previous file as it was.
|
||||
|
||||
Entries carry no timestamp: saving the same project twice gives the
|
||||
same bytes.
|
||||
*/
|
||||
namespace QetZip
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
QString name; ///< path inside the archive, '/'-separated
|
||||
QByteArray data;
|
||||
bool compress = true; ///< false: stored (pictures, the mimetype entry)
|
||||
};
|
||||
|
||||
/// Largest archive read(): uncompressed total and entry count
|
||||
constexpr qint64 MaxUncompressedSize = qint64(2) * 1024 * 1024 * 1024;
|
||||
constexpr int MaxEntries = 100000;
|
||||
|
||||
bool write(const QString &path, const QList<Entry> &entries, QString *error = nullptr);
|
||||
QByteArray toBytes(const QList<Entry> &entries, QString *error = nullptr);
|
||||
bool read(const QString &path, QList<Entry> *entries, QString *error = nullptr);
|
||||
bool fromBytes(const QByteArray &zip, QList<Entry> *entries, QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // QETZIP_H
|
||||
@@ -2669,8 +2669,8 @@ void projectDataBase::exportDb(projectDataBase *db,
|
||||
if (dir_.isEmpty()) {
|
||||
dir_ = QETApp::documentDir() % "/" % tr("untitled") % ".sqlite";
|
||||
} else {
|
||||
dir_.remove(".qet");
|
||||
dir_.append(".sqlite");
|
||||
const QFileInfo project_file(dir_);
|
||||
dir_ = project_file.path() % "/" % project_file.completeBaseName() % ".sqlite";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-5
@@ -339,11 +339,16 @@ int ProjectView::tryClosingDiagrams()
|
||||
*/
|
||||
QString ProjectView::askUserForFilePath(bool assign) {
|
||||
// ask the user for a filepath in order to save the project
|
||||
const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file");
|
||||
const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file");
|
||||
QString selected_filter = QETProject::isContainerPath(m_project->filePath()) ? qetz_filter : qet_filter;
|
||||
QString filepath = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Save as", "dialog title"),
|
||||
m_project -> currentDir() + "/" + tr("untitled") + ".qet",
|
||||
tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file")
|
||||
m_project -> currentDir() + "/" + tr("untitled")
|
||||
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"),
|
||||
qet_filter + ";;" + qetz_filter,
|
||||
&selected_filter
|
||||
);
|
||||
|
||||
// if no filepath is provided, return an empty string
|
||||
@@ -359,9 +364,22 @@ QString ProjectView::askUserForFilePath(bool assign) {
|
||||
// extension at all whenever the portal didn't (bugtracker #270).
|
||||
// Stripping any existing suffix first and re-appending it once is
|
||||
// correct either way.
|
||||
if (filepath.endsWith(".qet", Qt::CaseInsensitive))
|
||||
filepath.chop(4);
|
||||
filepath += ".qet";
|
||||
// Zipped (.qetz) when that file type is chosen, or when the name typed
|
||||
// says .qetz anywhere in its trailing extensions: typing "plan.qetz"
|
||||
// over the selected "untitled" of "untitled.qet" gives "plan.qetz.qet"
|
||||
// with the GTK dialog, and still means a .qetz.
|
||||
bool zipped = selected_filter == qetz_filter;
|
||||
for (;;) {
|
||||
if (filepath.endsWith(".qetz", Qt::CaseInsensitive)) {
|
||||
filepath.chop(5);
|
||||
zipped = true;
|
||||
} else if (filepath.endsWith(".qet", Qt::CaseInsensitive)) {
|
||||
filepath.chop(4);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
filepath += zipped ? ".qetz" : ".qet";
|
||||
|
||||
if (assign) {
|
||||
// assign the provided filepath to the currently edited project
|
||||
|
||||
@@ -1179,6 +1179,7 @@ QStringList QETApp::handledFileExtensions()
|
||||
static QStringList ext;
|
||||
if (!ext.count()) {
|
||||
ext << "qet";
|
||||
ext << "qetz";
|
||||
ext << "elmt";
|
||||
ext << QString(TITLEBLOCKS_FILE_EXTENSION).remove(QRegularExpression("^\\."));
|
||||
}
|
||||
|
||||
@@ -1809,7 +1809,7 @@ bool QETDiagramEditor::openProject()
|
||||
this,
|
||||
tr("Open a file"),
|
||||
open_dialog_dir.absolutePath(),
|
||||
tr("QElectroTech project(*.qet);;XML Files (*.xml);;All files (*)")
|
||||
tr("QElectroTech project(*.qet *.qetz);;XML Files (*.xml);;All files (*)")
|
||||
);
|
||||
if (filepath.isEmpty()) return(false);
|
||||
|
||||
|
||||
+50
-10
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "qetproject.h"
|
||||
#include "container/qetcontainer.h"
|
||||
|
||||
#include "ElementsCollection/xmlelementcollection.h"
|
||||
#include "autoNum/assignvariables.h"
|
||||
@@ -399,10 +400,20 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
||||
load_timer.start();
|
||||
|
||||
bool opened_here = file->isOpen() ? false : true;
|
||||
if (!file->isOpen()
|
||||
&& !file->open(QIODevice::ReadOnly
|
||||
| QIODevice::Text)) {
|
||||
return FileOpenFailed;
|
||||
if (opened_here)
|
||||
{
|
||||
//A .qetz is a zip and is read as bytes: text mode would turn
|
||||
//its CR LF pairs into LF on Windows. A .qet is read as text,
|
||||
//as it always was.
|
||||
if (!file->open(QIODevice::ReadOnly)) {
|
||||
return FileOpenFailed;
|
||||
}
|
||||
if (!QetContainer::isZip(file->peek(4))) {
|
||||
file->close();
|
||||
if (!file->open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return FileOpenFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
QFileInfo fi(*file);
|
||||
setFilePath(fi.absoluteFilePath());
|
||||
@@ -424,12 +435,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
||||
// call .text() on themselves -- which is exactly where the bug was.
|
||||
// The option exists since Qt 6.5; older Qt always drops such nodes,
|
||||
// so there an all-whitespace value still reloads as "".
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
|
||||
#else
|
||||
if (!xml_project.setContent(content))
|
||||
#endif
|
||||
// QetContainer::parse() does exactly that, for a .qet and for each part
|
||||
// of a .qetz alike.
|
||||
QString read_error;
|
||||
if (!QetContainer::readDocument(content, &xml_project, &read_error))
|
||||
{
|
||||
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
|
||||
if(opened_here) {
|
||||
file->close();
|
||||
}
|
||||
@@ -1809,6 +1820,30 @@ bool QETProject::close()
|
||||
@see setFilePath()
|
||||
@return true if the project was successfully saved, else false
|
||||
*/
|
||||
/**
|
||||
@brief QETProject::isContainerPath
|
||||
@return true if @p path names a .qetz file: write() saves it zipped
|
||||
*/
|
||||
bool QETProject::isContainerPath(const QString &path)
|
||||
{
|
||||
return path.endsWith(QLatin1String(".qetz"), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::writeContainer
|
||||
Write the project document @p project to @p path as a .qetz: the same
|
||||
document, in parts, zipped (#1440). The file is replaced only once
|
||||
the whole archive is written.
|
||||
*/
|
||||
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error)
|
||||
{
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(
|
||||
project,
|
||||
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
|
||||
error);
|
||||
return !entries.isEmpty() && QetZip::write(path, entries, error);
|
||||
}
|
||||
|
||||
QETResult QETProject::write()
|
||||
{
|
||||
// this operation requires a filepath
|
||||
@@ -1831,7 +1866,12 @@ QETResult QETProject::write()
|
||||
|
||||
QDomDocument xml_project(toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
|
||||
if (isContainerPath(m_file_path))
|
||||
{
|
||||
if (!writeContainer(xml_project, m_file_path, &error_message))
|
||||
return(error_message);
|
||||
}
|
||||
else if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
|
||||
return(error_message);
|
||||
|
||||
// The project has just been written to a writable file (e.g. saved to
|
||||
|
||||
@@ -402,6 +402,11 @@ class QETProject : public QObject
|
||||
void writeBackup();
|
||||
void init();
|
||||
ProjectState openFile(QFile *file);
|
||||
public:
|
||||
static bool isContainerPath(const QString &path);
|
||||
static bool writeContainer(const QDomDocument &project, const QString &path,
|
||||
QString *error = nullptr);
|
||||
private:
|
||||
static QUuid derivedUuid(const QByteArray &content);
|
||||
void refresh();
|
||||
|
||||
|
||||
@@ -921,7 +921,9 @@ QJsonObject LiveServer::snapshot(const QString &raw_path)
|
||||
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
|
||||
QDomDocument xml(project->toXml());
|
||||
QString error;
|
||||
if (!QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
||||
if (QETProject::isContainerPath(info.absoluteFilePath())
|
||||
? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error)
|
||||
: !QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
||||
return failure(QStringLiteral("snapshot not written: %1").arg(error));
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
|
||||
{QStringLiteral("folio"), e->currentDiagramView()
|
||||
|
||||
@@ -436,7 +436,9 @@ bool QetScriptApi::save(const QString &output)
|
||||
|
||||
QDomDocument xml_doc(m_project->toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_doc, output, &error_message)) {
|
||||
if (QETProject::isContainerPath(output)
|
||||
? !QETProject::writeContainer(xml_doc, output, &error_message)
|
||||
: !QET::writeXmlFile(xml_doc, output, &error_message)) {
|
||||
log(QStringLiteral("qet.save: %1").arg(error_message));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "../../container/qetcontainer.h"
|
||||
#include "projectconfigpages.h"
|
||||
|
||||
#include "../autoNum/autonumschemecommand.h"
|
||||
@@ -1165,7 +1166,7 @@ void ProjectAutoNumConfigPage::importFromProject()
|
||||
this,
|
||||
tr("Import numbering from a project"),
|
||||
m_project->currentDir(),
|
||||
tr("Project QElectroTech (*.qet)"));
|
||||
tr("Project QElectroTech (*.qet *.qetz)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -1178,7 +1179,7 @@ void ProjectAutoNumConfigPage::importFromProject()
|
||||
}
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(&file)) {
|
||||
if (!QetContainer::readDocument(file.readAll(), &doc)) {
|
||||
QMessageBox::warning(this, tr("Import not possible"),
|
||||
tr("%1 is not a valid QElectroTech project.")
|
||||
.arg(QFileInfo(path).fileName()));
|
||||
|
||||
@@ -1221,3 +1221,29 @@ if(Qt6_VERSION VERSION_LESS 6.5.0)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The zip a .qetz project is stored in: round trip, same bytes twice,
|
||||
# unsafe entry names refused (sources/container/qetzip.*, bundled miniz).
|
||||
add_executable(
|
||||
tst_qetzip
|
||||
tst_qetzip.cpp
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/miniz/miniz.c)
|
||||
add_test(NAME tst_qetzip COMMAND tst_qetzip)
|
||||
target_include_directories(tst_qetzip PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_qetzip PRIVATE Qt::Test)
|
||||
|
||||
# A project split into the parts of a .qetz and joined back is the same
|
||||
# document, on every example (and QET_CONTAINER_CORPUS if set).
|
||||
add_executable(
|
||||
tst_qetcontainer
|
||||
tst_qetcontainer.cpp
|
||||
${QET_DIR}/sources/container/qetcontainer.cpp
|
||||
${QET_DIR}/sources/container/qetcontainerdb.cpp
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/miniz/miniz.c)
|
||||
add_test(NAME tst_qetcontainer COMMAND tst_qetcontainer)
|
||||
target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3)
|
||||
target_compile_definitions(tst_qetcontainer PRIVATE
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "container/qetcontainer.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
// A project document split into the parts of a .qetz, zipped, unzipped and
|
||||
// joined back is the document it was: every example, and every .qet in the
|
||||
// folder QET_CONTAINER_CORPUS names, if set -- exactly, whitespace text
|
||||
// included: QETProject writes the symbol definitions back as it read them,
|
||||
// spacing and all, so a part that came back re-indented would change the
|
||||
// next save.
|
||||
class tst_qetcontainer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static QList<QDomNode> children(const QDomNode &node)
|
||||
{
|
||||
QList<QDomNode> all;
|
||||
for (QDomNode c = node.firstChild() ; !c.isNull() ; c = c.nextSibling())
|
||||
all << c;
|
||||
return all;
|
||||
}
|
||||
|
||||
// empty when equal, else where they differ
|
||||
static QString difference(const QDomNode &a, const QDomNode &b, const QString &path)
|
||||
{
|
||||
const QString here = path + QLatin1Char('/') + a.nodeName();
|
||||
if (a.nodeType() != b.nodeType() || a.nodeName() != b.nodeName())
|
||||
return here + QStringLiteral(": %1 vs %2").arg(a.nodeName(), b.nodeName());
|
||||
if (!a.isElement() && !a.isDocument() && a.nodeValue() != b.nodeValue())
|
||||
return here + QStringLiteral(": value differs");
|
||||
if (a.isElement()) {
|
||||
const QDomNamedNodeMap aa = a.attributes(), ba = b.attributes();
|
||||
if (aa.count() != ba.count())
|
||||
return here + QStringLiteral(": %1 vs %2 attributes").arg(aa.count()).arg(ba.count());
|
||||
for (int i = 0 ; i < aa.count() ; ++i) {
|
||||
const QDomAttr attr = aa.item(i).toAttr();
|
||||
if (!ba.contains(attr.name()) || ba.namedItem(attr.name()).nodeValue() != attr.value())
|
||||
return here + QStringLiteral(": attribute %1 differs").arg(attr.name());
|
||||
}
|
||||
}
|
||||
const QList<QDomNode> ac = children(a), bc = children(b);
|
||||
if (ac.size() != bc.size())
|
||||
return here + QStringLiteral(": %1 vs %2 children").arg(ac.size()).arg(bc.size());
|
||||
for (int i = 0 ; i < ac.size() ; ++i) {
|
||||
const QString d = difference(ac.at(i), bc.at(i), here + QStringLiteral("[%1]").arg(i));
|
||||
if (!d.isEmpty()) return d;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QByteArray read(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
static QList<QetZip::Entry> splitFile(const QString &path, QDomDocument *original)
|
||||
{
|
||||
QString error;
|
||||
if (!QetContainer::parse(read(path), original, &error)) return {};
|
||||
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
|
||||
}
|
||||
|
||||
private slots:
|
||||
void sameDocumentBack_data()
|
||||
{
|
||||
QTest::addColumn<QString>("file");
|
||||
QStringList dirs{QStringLiteral(QET_EXAMPLES_DIR)};
|
||||
const QString corpus = qEnvironmentVariable("QET_CONTAINER_CORPUS");
|
||||
if (!corpus.isEmpty()) dirs << corpus;
|
||||
for (const QString &dir : dirs) {
|
||||
QDirIterator it(dir, {QStringLiteral("*.qet")}, QDir::Files, QDirIterator::Subdirectories);
|
||||
QStringList files;
|
||||
while (it.hasNext()) files << it.next();
|
||||
files.sort();
|
||||
for (const QString &f : files)
|
||||
QTest::newRow(qPrintable(QDir(dir).relativeFilePath(f))) << f;
|
||||
}
|
||||
}
|
||||
void sameDocumentBack()
|
||||
{
|
||||
QFETCH(QString, file);
|
||||
QDomDocument original;
|
||||
QString error;
|
||||
if (!QetContainer::parse(read(file), &original, &error))
|
||||
QSKIP(qPrintable(QStringLiteral("not readable as XML, as QElectroTech finds too: ") + error));
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||
QCOMPARE(entries.first().name, QStringLiteral("mimetype"));
|
||||
|
||||
QList<QetZip::Entry> unzipped;
|
||||
QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error));
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error));
|
||||
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
// Splitting and joining adds no whitespace text. QETProject keeps the
|
||||
// symbol definitions as it read them and writes them back as they
|
||||
// are, so indentation a part gained would change the next save
|
||||
// (forum attachments 1328, 1978, 2057, 3003, 3004 did, with indented
|
||||
// parts). Here: a document without any whitespace-only text, as
|
||||
// QETProject::toXml() builds one, must come back without any.
|
||||
void noWhitespaceAdded()
|
||||
{
|
||||
QDomDocument original;
|
||||
QVERIFY(original.setContent(read(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"))));
|
||||
auto whitespace_nodes = [](const QDomDocument &d) {
|
||||
int n = 0;
|
||||
std::function<void(const QDomNode &)> walk = [&](const QDomNode &node) {
|
||||
for (QDomNode c = node.firstChild() ; !c.isNull() ; c = c.nextSibling()) {
|
||||
if (c.isText() && c.nodeValue().trimmed().isEmpty()) ++n;
|
||||
walk(c);
|
||||
}
|
||||
};
|
||||
walk(d);
|
||||
return n;
|
||||
};
|
||||
QCOMPARE(whitespace_nodes(original), 0);
|
||||
QString error;
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||
QCOMPARE(whitespace_nodes(back), 0);
|
||||
}
|
||||
|
||||
// The engineering data is in project.sqlite and nowhere else: the folio
|
||||
// files hold no symbol information, links, wire numbers or title-block
|
||||
// fields, and the database holds them all (industrial.qet: 50 folios).
|
||||
void databaseHoldsTheData()
|
||||
{
|
||||
QDomDocument original;
|
||||
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||
QVERIFY(!entries.isEmpty());
|
||||
QByteArray database;
|
||||
for (const QetZip::Entry &e : entries) {
|
||||
if (e.name == QLatin1String("project.sqlite")) database = e.data;
|
||||
if (e.name.startsWith(QStringLiteral("folios/")) || e.name == QLatin1String("project.xml")) {
|
||||
QVERIFY2(!e.data.contains("<elementInformation "), qPrintable(e.name));
|
||||
QVERIFY2(!e.data.contains("<link_uuid "), qPrintable(e.name));
|
||||
QVERIFY2(!e.data.contains("<property "), qPrintable(e.name));
|
||||
QVERIFY2(!QString::fromUtf8(e.data).contains(QRegularExpression(QStringLiteral("<conductor [^>]* num="))),
|
||||
qPrintable(e.name));
|
||||
}
|
||||
}
|
||||
QVERIFY(database.startsWith("SQLite format 3"));
|
||||
|
||||
sqlite3 *db = nullptr;
|
||||
QCOMPARE(sqlite3_open(":memory:", &db), SQLITE_OK);
|
||||
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
|
||||
memcpy(copy, database.constData(), size_t(database.size()));
|
||||
QCOMPARE(sqlite3_deserialize(db, "main", copy, database.size(), database.size(),
|
||||
SQLITE_DESERIALIZE_FREEONCLOSE), SQLITE_OK);
|
||||
auto count = [db](const char *table) {
|
||||
sqlite3_stmt *st = nullptr;
|
||||
sqlite3_prepare_v2(db, (QByteArray("SELECT count(*) FROM ") + table).constData(), -1, &st, nullptr);
|
||||
sqlite3_step(st);
|
||||
const int n = sqlite3_column_int(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
return n;
|
||||
};
|
||||
const QDomNodeList diagrams = original.elementsByTagName(QStringLiteral("diagram"));
|
||||
QCOMPARE(count("folio"), int(diagrams.size()));
|
||||
QCOMPARE(count("element"), int(original.elementsByTagName(QStringLiteral("element")).size())
|
||||
- int(original.elementsByTagName(QStringLiteral("collection")).at(0).toElement()
|
||||
.elementsByTagName(QStringLiteral("element")).size()));
|
||||
QCOMPARE(count("element_info"), int(original.elementsByTagName(QStringLiteral("elementInformation")).size()));
|
||||
QCOMPARE(count("link"), int(original.elementsByTagName(QStringLiteral("link_uuid")).size()));
|
||||
QCOMPARE(count("conductor"), int(original.elementsByTagName(QStringLiteral("conductor")).size()));
|
||||
QVERIFY(count("folio_property") + count("project_property") > 0);
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
// Terminal strips move into the database, and come back exactly:
|
||||
// industrial.qet with two strips on its own terminal symbols, parsed as
|
||||
// a save leaves it (no whitespace text).
|
||||
void terminalStripsInDatabase()
|
||||
{
|
||||
QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
|
||||
const QByteArray strips =
|
||||
"<terminal_strips>"
|
||||
"<terminal_strip><terminal_strip_data uuid=\"{11111111-0000-4000-8000-000000000001}\"><informations>"
|
||||
"<information name=\"installation\">=A1</information><information name=\"location\">+CAB1</information>"
|
||||
"<information name=\"name\">X1</information><information name=\"comment\">main & aux</information>"
|
||||
"</informations></terminal_strip_data>"
|
||||
"<layout><physical_terminal><real_terminal element_uuid=\"{c2180165-8d35-44eb-83c4-100395451915}\"/></physical_terminal>"
|
||||
"<physical_terminal><real_terminal element_uuid=\"{05f681ee-04dd-489d-afc8-a646ec6016f9}\"/>"
|
||||
"<real_terminal element_uuid=\"{a33935cf-671a-480f-9497-1a632eab7cbd}\"/></physical_terminal></layout>"
|
||||
"<terminal_strip_bridge uuid=\"{22222222-0000-4000-8000-000000000001}\" color=\"#ff0000\"><real_terminals>"
|
||||
"<real_terminal uuid=\"{c2180165-8d35-44eb-83c4-100395451915}\"/>"
|
||||
"<real_terminal uuid=\"{05f681ee-04dd-489d-afc8-a646ec6016f9}\"/></real_terminals></terminal_strip_bridge>"
|
||||
"</terminal_strip>"
|
||||
"<terminal_strip><terminal_strip_data uuid=\"{11111111-0000-4000-8000-000000000002}\"><informations>"
|
||||
"<information name=\"name\">X2</information></informations></terminal_strip_data>"
|
||||
"<layout><physical_terminal><real_terminal element_uuid=\"{c96008ed-1c74-4c0a-836a-bd8458491a16}\"/></physical_terminal></layout>"
|
||||
"</terminal_strip></terminal_strips>";
|
||||
const int at = xml.indexOf("<collection");
|
||||
QVERIFY(at > 0);
|
||||
xml.insert(at, strips);
|
||||
QDomDocument original;
|
||||
QVERIFY(original.setContent(xml)); // as a save leaves it: no whitespace text
|
||||
QString error;
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||
for (const QetZip::Entry &e : entries)
|
||||
if (e.name == QLatin1String("project.xml"))
|
||||
QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml");
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
// The folio pictures leave the folios, once each
|
||||
void picturesAreFiles()
|
||||
{
|
||||
QDomDocument original;
|
||||
const QList<QetZip::Entry> entries = splitFile(
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/weneedpolonez-Polonez_MR89_wiring_diagram.qet"), &original);
|
||||
QVERIFY(!entries.isEmpty());
|
||||
int pictures = 0;
|
||||
for (const QetZip::Entry &e : entries) {
|
||||
if (e.name.startsWith(QStringLiteral("images/"))) {
|
||||
++pictures;
|
||||
QVERIFY(e.data.startsWith("\x89PNG"));
|
||||
QVERIFY(!e.compress);
|
||||
}
|
||||
if (e.name.startsWith(QStringLiteral("folios/")))
|
||||
QVERIFY2(!e.data.contains("iVBORw0KGgo"), "a folio still holds a PNG in base64");
|
||||
}
|
||||
QVERIFY(pictures > 0);
|
||||
QVERIFY(pictures < 121); // the 121 placed pictures repeat a few files
|
||||
}
|
||||
|
||||
// A file whose manifest needs a later reader is refused, not half read
|
||||
void laterFormatRefused()
|
||||
{
|
||||
QDomDocument original;
|
||||
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original);
|
||||
QVERIFY(!entries.isEmpty());
|
||||
for (QetZip::Entry &e : entries)
|
||||
if (e.name == QLatin1String("manifest.xml"))
|
||||
e.data = QString::fromUtf8(e.data).replace(QRegularExpression(QStringLiteral("min-reader=\"\\d+\"")),
|
||||
QStringLiteral("min-reader=\"9\"")).toUtf8();
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||
QVERIFY2(error.contains(QStringLiteral("later")), qPrintable(error));
|
||||
}
|
||||
|
||||
void missingPartRefused()
|
||||
{
|
||||
QDomDocument original;
|
||||
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original);
|
||||
for (int i = 0 ; i < entries.size() ; ++i)
|
||||
if (entries.at(i).name.startsWith(QStringLiteral("folios/"))) { entries.removeAt(i); break; }
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||
QVERIFY2(error.contains(QStringLiteral("missing")), qPrintable(error));
|
||||
}
|
||||
|
||||
// A crafted project.sqlite whose "element" is a view, not a table, is
|
||||
// refused: nothing in a file's schema is trusted.
|
||||
void viewInsteadOfTableRefused()
|
||||
{
|
||||
QDomDocument original;
|
||||
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original);
|
||||
QVERIFY(!entries.isEmpty());
|
||||
for (QetZip::Entry &e : entries) {
|
||||
if (e.name != QLatin1String("project.sqlite")) continue;
|
||||
sqlite3 *db = nullptr;
|
||||
sqlite3_open(":memory:", &db);
|
||||
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(e.data.size())));
|
||||
memcpy(copy, e.data.constData(), size_t(e.data.size()));
|
||||
sqlite3_deserialize(db, "main", copy, e.data.size(), e.data.size(),
|
||||
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
|
||||
QCOMPARE(sqlite3_exec(db, "ALTER TABLE element RENAME TO element_data;"
|
||||
"CREATE VIEW element AS SELECT * FROM element_data;",
|
||||
nullptr, nullptr, nullptr), SQLITE_OK);
|
||||
sqlite3_int64 size = 0;
|
||||
unsigned char *bytes = sqlite3_serialize(db, "main", &size, 0);
|
||||
e.data = QByteArray(reinterpret_cast<const char *>(bytes), qsizetype(size));
|
||||
sqlite3_free(bytes);
|
||||
sqlite3_close(db);
|
||||
}
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||
QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error));
|
||||
}
|
||||
|
||||
void notAProjectRefused()
|
||||
{
|
||||
QDomDocument back;
|
||||
QString error;
|
||||
QVERIFY(!QetContainer::join({{QStringLiteral("mimetype"), "application/zip", false}}, &back, &error));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_qetcontainer)
|
||||
|
||||
#include "tst_qetcontainer.moc"
|
||||
@@ -0,0 +1,145 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "container/qetzip.h"
|
||||
#include "container/miniz/miniz.h"
|
||||
|
||||
// The zip a .qetz project is stored in (sources/container/qetzip.*):
|
||||
// entries come back as written, in order, stored ones stored; the same
|
||||
// entries give the same bytes; unsafe names are refused both ways.
|
||||
class tst_qetzip : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static QList<QetZip::Entry> sample()
|
||||
{
|
||||
QList<QetZip::Entry> entries;
|
||||
entries.append({QStringLiteral("mimetype"), QByteArray("application/x-qelectrotech-project+zip"), false});
|
||||
QByteArray xml("<project>");
|
||||
for (int i = 0 ; i < 2000 ; ++i) xml += "<element name=\"K1\"/>";
|
||||
xml += "</project>";
|
||||
entries.append({QStringLiteral("project.xml"), xml, true});
|
||||
QByteArray picture;
|
||||
for (int i = 0 ; i < 4096 ; ++i) picture += char((i * 7919) & 0xff);
|
||||
entries.append({QStringLiteral("images/00.png"), picture, false});
|
||||
entries.append({QStringLiteral("folios/é ü.xml"), QByteArray("<diagram/>"), true});
|
||||
entries.append({QStringLiteral("empty"), QByteArray(), true});
|
||||
return entries;
|
||||
}
|
||||
|
||||
// a zip holding one entry named @p name, written past QetZip's checks
|
||||
static QByteArray rawZip(const char *name)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
mz_zip_writer_init_heap(&zip, 0, 0);
|
||||
mz_zip_writer_add_mem(&zip, name, "x", 1, MZ_DEFAULT_LEVEL);
|
||||
void *buffer = nullptr;
|
||||
size_t size = 0;
|
||||
mz_zip_writer_finalize_heap_archive(&zip, &buffer, &size);
|
||||
const QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
||||
mz_zip_writer_end(&zip);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void roundTrip()
|
||||
{
|
||||
const QList<QetZip::Entry> in = sample();
|
||||
QString error;
|
||||
const QByteArray zip = QetZip::toBytes(in, &error);
|
||||
QVERIFY2(!zip.isEmpty(), qPrintable(error));
|
||||
QVERIFY(zip.startsWith("PK"));
|
||||
QVERIFY2(zip.size() < in.at(1).data.size() / 4, "the XML entry was not compressed");
|
||||
|
||||
QList<QetZip::Entry> out;
|
||||
QVERIFY2(QetZip::fromBytes(zip, &out, &error), qPrintable(error));
|
||||
QCOMPARE(out.size(), in.size());
|
||||
for (int i = 0 ; i < in.size() ; ++i) {
|
||||
QCOMPARE(out.at(i).name, in.at(i).name);
|
||||
QCOMPARE(out.at(i).data, in.at(i).data);
|
||||
if (!in.at(i).data.isEmpty())
|
||||
QCOMPARE(out.at(i).compress, in.at(i).compress);
|
||||
}
|
||||
// mimetype first and stored: its bytes appear as is after the header
|
||||
QCOMPARE(zip.indexOf("application/x-qelectrotech-project+zip"), 30 + int(qstrlen("mimetype")));
|
||||
}
|
||||
|
||||
void sameBytesTwice()
|
||||
{
|
||||
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
||||
}
|
||||
|
||||
void refusesUnsafeNamesOnWrite_data()
|
||||
{
|
||||
QTest::addColumn<QString>("name");
|
||||
QTest::newRow("parent") << QStringLiteral("../x.xml");
|
||||
QTest::newRow("inner parent") << QStringLiteral("folios/../../x.xml");
|
||||
QTest::newRow("absolute") << QStringLiteral("/etc/x");
|
||||
QTest::newRow("drive") << QStringLiteral("C:/x");
|
||||
QTest::newRow("backslash") << QStringLiteral("folios\\x.xml");
|
||||
QTest::newRow("empty") << QString();
|
||||
}
|
||||
void refusesUnsafeNamesOnWrite()
|
||||
{
|
||||
QFETCH(QString, name);
|
||||
QString error;
|
||||
QVERIFY(QetZip::toBytes({{name, QByteArray("x"), true}}, &error).isEmpty());
|
||||
QVERIFY(!error.isEmpty());
|
||||
}
|
||||
|
||||
void refusesRepeatedNames()
|
||||
{
|
||||
QString error;
|
||||
QVERIFY(QetZip::toBytes({{QStringLiteral("a"), "1", true}, {QStringLiteral("a"), "2", true}}, &error).isEmpty());
|
||||
}
|
||||
|
||||
void refusesUnsafeNamesOnRead()
|
||||
{
|
||||
QList<QetZip::Entry> out;
|
||||
QString error;
|
||||
QVERIFY(QetZip::fromBytes(rawZip("ok/name.xml"), &out, &error));
|
||||
QVERIFY(!QetZip::fromBytes(rawZip("../evil.xml"), &out, &error));
|
||||
QVERIFY(out.isEmpty());
|
||||
// miniz will not write a name starting with '/': write "Xabs.xml"
|
||||
// and turn the X into '/' in both of its headers
|
||||
QByteArray absolute = rawZip("Xabs.xml");
|
||||
QCOMPARE(absolute.count("Xabs.xml"), 2);
|
||||
absolute.replace("Xabs.xml", "/abs.xml");
|
||||
QVERIFY(!QetZip::fromBytes(absolute, &out, &error));
|
||||
}
|
||||
|
||||
void refusesGarbage()
|
||||
{
|
||||
QList<QetZip::Entry> out;
|
||||
QString error;
|
||||
QVERIFY(!QetZip::fromBytes(QByteArray("not a zip at all"), &out, &error));
|
||||
QVERIFY(!error.isEmpty());
|
||||
QByteArray cut = QetZip::toBytes(sample());
|
||||
cut.chop(cut.size() / 2);
|
||||
QVERIFY(!QetZip::fromBytes(cut, &out, &error));
|
||||
}
|
||||
|
||||
void writeAndRead()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
const QString path = dir.filePath(QStringLiteral("p.qetz"));
|
||||
QString error;
|
||||
QVERIFY2(QetZip::write(path, sample(), &error), qPrintable(error));
|
||||
QList<QetZip::Entry> out;
|
||||
QVERIFY2(QetZip::read(path, &out, &error), qPrintable(error));
|
||||
QCOMPARE(out.size(), sample().size());
|
||||
QCOMPARE(out.at(1).data, sample().at(1).data);
|
||||
// a refused write leaves the file as it was
|
||||
QVERIFY(!QetZip::write(path, {{QStringLiteral("../x"), "x", true}}, &error));
|
||||
QVERIFY(QetZip::read(path, &out, &error));
|
||||
QCOMPARE(out.size(), sample().size());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_qetzip)
|
||||
|
||||
#include "tst_qetzip.moc"
|
||||
@@ -30,9 +30,12 @@ class tst_resaveunchanged : public QObject
|
||||
|
||||
// --resave @p in to a new file, in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead); returns the new file's path.
|
||||
QString resave(const QString &in)
|
||||
QString resave(const QString &in, const QString &suffix = QStringLiteral(".qet"))
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
|
||||
//Always p.qet, in its own folder: an untitled project takes its
|
||||
//file name as title, so two names would make two projects.
|
||||
QDir().mkpath(m_dir.filePath(QStringLiteral("run%1").arg(m_run)));
|
||||
const QString out = m_dir.filePath(QStringLiteral("run%1/p").arg(m_run) + suffix);
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
@@ -282,6 +285,38 @@ private slots:
|
||||
QVERIFY2(read(saved).contains(b), "the arrow without a terminal was not kept");
|
||||
}
|
||||
|
||||
// Saved as a .qetz (the zipped project, #1440) and that saved again as a
|
||||
// .qet, a project is what it is saved straight as a .qet: the same
|
||||
// file but for when and where it was saved. The examples with pictures
|
||||
// (weneedpolonez), tables and links (industrial) and several title
|
||||
// blocks (m_000).
|
||||
void containerSavesTheSameProject_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
for (const char *name : {"Projet_vierge.qet", "industrial.qet", "m_000.qet",
|
||||
"weneedpolonez-Polonez_MR89_wiring_diagram.qet"})
|
||||
QTest::newRow(name) << QStringLiteral(QET_EXAMPLES_DIR "/") + QLatin1String(name);
|
||||
}
|
||||
void containerSavesTheSameProject()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString straight = resave(project);
|
||||
QVERIFY2(!straight.isEmpty(), "--resave as .qet failed");
|
||||
const QString zipped = resave(project, QStringLiteral(".qetz"));
|
||||
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
|
||||
QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip");
|
||||
QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller");
|
||||
const QString back = resave(zipped);
|
||||
QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
|
||||
|
||||
auto without_save_stamps = [](QByteArray xml) {
|
||||
static const QRegularExpression stamp(QStringLiteral(
|
||||
"\\s*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
|
||||
return QString::fromUtf8(xml).remove(stamp);
|
||||
};
|
||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
||||
}
|
||||
|
||||
// A title-block value that is a single space is kept through two saves
|
||||
// (#973), and a value with accents comes back as it went in.
|
||||
void singleSpaceValueKept()
|
||||
|
||||
Reference in New Issue
Block a user