Merge pull request #1504 from ispyisail/feat/qetz-recover

Add recovering what can be read of a damaged .qetz project (--recover)
This commit is contained in:
ispyisail
2026-10-16 11:26:55 +13:00
committed by GitHub
9 changed files with 348 additions and 53 deletions
+48
View File
@@ -28,6 +28,7 @@
#include "dxfexport.h"
#include "exportproperties.h"
#include "pdf_links.h"
#include "container/qetcontainer.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/element.h"
@@ -85,6 +86,7 @@ const QHash<QString, QString> &exportFlags()
{"--info", "info"},
{"--check-elements", "check"},
{"--resave", "resave"},
{"--recover", "recover"},
{"--set-titleblock", "settb"},
};
return flags;
@@ -870,6 +872,42 @@ int exportLinks(QETProject &project, const QString &output)
/// Round-trip: load the project and write its XML back out, so an external
/// diff can reveal markup QET silently normalises (tolerated-but-invalid XML).
/// What can be read of the damaged .qetz @p input, written to the .qet
/// @p output, and what was left out (QetContainer::recover()).
int recoverProject(const QString &input, const QString &output)
{
QFile file(input);
if (!file.open(QIODevice::ReadOnly)) {
err << "Cannot read '" << input << "': " << file.errorString() << "\n";
return 2;
}
const QByteArray content = file.readAll();
if (!QetContainer::isZip(content)) {
err << "'" << input << "' is not a .qetz project; a .qet opens as it is, or can be fixed in a text editor\n";
return 2;
}
QDomDocument document;
QStringList lost;
QString error;
if (!QetContainer::recover(content, &document, &lost, &error)) {
err << "Nothing can be recovered from '" << input << "': " << error << "\n";
return 1;
}
if (!QET::writeXmlFile(document, output, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
out << "Recovered " << input << " -> " << output << "\n";
if (lost.isEmpty()) {
out << "Nothing was left out.\n";
} else {
out << "Left out:\n";
for (const QString &item : std::as_const(lost))
out << " " << item << "\n";
}
return 0;
}
int resaveProject(QETProject &project, const QString &output)
{
if (QETProject::isContainerPath(output)) {
@@ -1038,6 +1076,16 @@ int run(const QStringList &args)
return checkElements(rest.at(0));
}
// --recover reads what it can of a damaged .qetz, which a project
// cannot be opened from.
if (format == "recover") {
if (rest.size() < 2) {
err << "Usage: qelectrotech --recover <damaged.qetz> <output.qet>\n";
return 2;
}
return recoverProject(rest.at(0), rest.at(1));
}
const QString input = rest.value(0);
if (input.isEmpty()) {
err << "Usage: qelectrotech " << flag << " <project.qet> <output>\n";
+1
View File
@@ -55,6 +55,7 @@ namespace CLIExport {
qelectrotech --info <project.qet> [output.json]
qelectrotech --check-elements <element.elmt | directory>
qelectrotech --resave <project.qet> <output.qet>
qelectrotech --recover <damaged.qetz> <output.qet>
qelectrotech --set-titleblock <project.qet> <output.qet> key=value...
PDF: one multi-page document (one diagram per page).
+56 -16
View File
@@ -184,6 +184,25 @@ bool QetContainer::readDocument(const QByteArray &content, QDomDocument *documen
&& join(entries, document, error, nullptr, pictures, format);
}
/**
@brief QetContainer::recover
What can be read of the .qetz @p content, a damaged one, into @p project:
a folio file, a picture or an entry that cannot be read is left out,
symbols and wires that do not match the database lose what it held, and
a database that cannot be read loses it all (wires, whose ends it holds,
go too). Each thing left out is said in @p lost.
@return false, with @p error, when nothing can be read: not a zip, no
project.xml, or a later format
*/
bool QetContainer::recover(const QByteArray &content, QDomDocument *project, QStringList *lost, QString *error)
{
QStringList none;
if (!lost) lost = &none;
QList<QetZip::Entry> entries;
return QetZip::fromBytes(content, &entries, error, lost)
&& join(entries, project, error, nullptr, nullptr, nullptr, lost);
}
QString QetContainer::pictureFile(const QDomElement &image)
{
return image.attribute(ImageFileAttribute);
@@ -421,23 +440,35 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
for a later one this may read (its min-reader allows it).
*/
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error,
QStringList *folio_files, Pictures *pictures, int *format)
QStringList *folio_files, Pictures *pictures, int *format, QStringList *lost)
{
//Recovering a damaged project (recover()): what is missing or
//damaged is noted in lost and left out, instead of refusing it all
const auto noted = [error, lost](const QString &problem) {
if (lost) {
lost->append(problem);
return true;
}
setError(error, problem);
return false;
};
QHash<QString, QByteArray> files;
for (const QetZip::Entry &entry : entries)
files.insert(entry.name, entry.data);
const QByteArray mimetype = files.value(QStringLiteral("mimetype"));
if (mimetype != QByteArray(MimeType) && mimetype != QByteArray(OldMimeType)) {
setError(error, QStringLiteral("not a QElectroTech project (.qetz)"));
if (mimetype != QByteArray(MimeType) && mimetype != QByteArray(OldMimeType)
&& !noted(QStringLiteral("not a QElectroTech project (.qetz)")))
return false;
}
QDomDocument manifest;
if (!parse(files.value(QStringLiteral("manifest.xml")), &manifest, error))
QString manifest_error;
const bool has_manifest = parse(files.value(QStringLiteral("manifest.xml")), &manifest, &manifest_error);
if (!has_manifest && !noted(manifest_error))
return false;
bool ok = false;
const int min_reader = manifest.documentElement().attribute(QStringLiteral("min-reader")).toInt(&ok);
if (!ok || min_reader > Format) {
//A later format is not damage: refused all the same
if (has_manifest && (!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));
@@ -460,29 +491,33 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
&& file.startsWith(QLatin1String("folios/")))
folio_files->append(file);
if (!files.contains(file)) {
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
return false;
if (!noted(QStringLiteral("part \"%1\" is missing").arg(file))) return false;
part.parentNode().removeChild(part);
continue;
}
QDomDocument document;
QString part_error;
if (!parse(files.value(file), &document, &part_error)) {
setError(error, QStringLiteral("%1: %2").arg(file, part_error));
return false;
if (!noted(QStringLiteral("%1: %2").arg(file, part_error))) return false;
part.parentNode().removeChild(part);
continue;
}
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
}
//Pictures back to base64, as the image's first child, or into
//pictures
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
for (int i = 0 ; i < images.size() ; ++i)
const QDomNodeList image_list = project->elementsByTagName(QStringLiteral("image"));
QList<QDomElement> images;
for (int i = 0 ; i < image_list.size() ; ++i) images << image_list.at(i).toElement();
for (QDomElement image : images)
{
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;
if (!noted(QStringLiteral("picture \"%1\" is missing").arg(file))) return false;
image.parentNode().removeChild(image);
continue;
}
if (pictures) {
pictures->insert(file, files.value(file));
@@ -496,7 +531,12 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
//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))
files.value(QStringLiteral("project.sqlite")), error, lost))
return false;
if (lost) {
if (!files.contains(QStringLiteral("project.sqlite")))
lost->append(QStringLiteral("project.sqlite is missing"));
QetContainerDb::dropLeftovers(project->documentElement(), lost);
}
return true;
}
+5 -1
View File
@@ -77,7 +77,11 @@ namespace QetContainer
const QHash<QString, QByteArray> &kept_from = QHash<QString, QByteArray>());
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
QString *error = nullptr, QStringList *folio_files = nullptr,
Pictures *pictures = nullptr, int *format = nullptr);
Pictures *pictures = nullptr, int *format = nullptr,
QStringList *lost = nullptr);
/// What can be read of a damaged .qetz, and in @p lost what could not
bool recover(const QByteArray &content, QDomDocument *project, QStringList *lost,
QString *error = nullptr);
/// The file join() left the picture of @p image in, or an empty string
QString pictureFile(const QDomElement &image);
+94 -30
View File
@@ -776,8 +776,22 @@ struct UsedRows
}
};
//The engineering data of the folio @p d back from @p db
bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error)
//A database that does not match its folios: refused, or, with @p lost
//(a damaged project recovered, QetContainer::recover()), @p problem
//noted and gone past. @return true to go on
bool noted(QString *error, QStringList *lost, const QString &problem)
{
if (lost) {
lost->append(problem);
return true;
}
setError(error, problem);
return false;
}
//The engineering data of the folio @p d back from @p db; with @p lost,
//what does not match is noted and left out (noted())
bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error, QStringList *lost)
{
QDomDocument document = d.ownerDocument();
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
@@ -786,10 +800,11 @@ bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error)
};
const QString id = keyOf(d, FolioAttribute);
if (id.isEmpty()) return true;
if (!used.use(used.folios, id, QStringLiteral("folio"), error)) return false;
QString problem;
if (!used.use(used.folios, id, QStringLiteral("folio"), &problem)) return noted(error, lost, problem);
Statement f(db, QStringLiteral("SELECT %1, props_attrs FROM folio WHERE uuid = ?").arg(quoted(FolioData)));
f.bind({id});
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
if (!f.next()) return noted(error, lost, QStringLiteral("folio %1 is not in the database").arg(id));
QVariantList values;
for (int i = 0 ; i < FolioData.size() + 1 ; ++i) values << f.value(i);
put(d, FolioData, values, 0);
@@ -804,24 +819,30 @@ bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error)
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
QStringLiteral("property"), p.value(3)));
restoreWhitespace(props, values.at(FolioData.size()));
if (!replaceMarker(d, props)) {
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
if (!replaceMarker(d, props)
&& !noted(error, lost, 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"))) {
const QString key = keyOf(e, KeyAttribute);
if (key.isEmpty()) continue;
if (!used.use(used.elements, key, QStringLiteral("symbol"), error)) return false;
if (!used.use(used.elements, key, QStringLiteral("symbol"), &problem)) {
if (!noted(error, lost, problem)) return false;
continue;
}
Statement s(db, QStringLiteral("SELECT %1, info_attrs, links_attrs, folio_uuid FROM element WHERE uuid = ?")
.arg(quoted(ElementData)));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
if (!s.next()) {
if (!noted(error, lost, QStringLiteral("symbol %1 is not in the database").arg(key))) return false;
continue;
}
if (s.value(ElementData.size() + 2).toString() != id) {
setError(error, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id));
return false;
if (!noted(error, lost, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id)))
return false;
continue;
}
QVariantList v;
for (int i = 0 ; i < ElementData.size() + 2 ; ++i) v << s.value(i);
@@ -848,25 +869,31 @@ bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error)
QStringLiteral("link_uuid")));
restoreWhitespace(links, v.at(n + 1));
}
if ((!info.isNull() && !replaceMarker(e, info))
|| (!links.isNull() && !replaceMarker(e, links))) {
setError(error, QStringLiteral("symbol %1's information has no place").arg(key));
if (((!info.isNull() && !replaceMarker(e, info))
|| (!links.isNull() && !replaceMarker(e, links)))
&& !noted(error, lost, 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"))) {
const QString key = keyOf(c, KeyAttribute);
if (key.isEmpty()) continue;
if (!used.use(used.conductors, key, QStringLiteral("wire"), error)) return false;
if (!used.use(used.conductors, key, QStringLiteral("wire"), &problem)) {
if (!noted(error, lost, problem)) return false;
continue;
}
Statement s(db, QStringLiteral("SELECT %1, folio_uuid FROM conductor WHERE uuid = ?")
.arg(quoted(ConductorData)));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
if (!s.next()) {
if (!noted(error, lost, QStringLiteral("wire %1 is not in the database").arg(key))) return false;
continue;
}
if (s.value(ConductorData.size()).toString() != id) {
setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id));
return false;
if (!noted(error, lost, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id)))
return false;
continue;
}
QVariantList v;
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i);
@@ -1163,12 +1190,14 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB
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)
bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QString *error,
QStringList *lost)
{
QDomDocument document = project.ownerDocument();
Database db;
if (!openDatabase(db, database, error) || !readyToRead(db, project, error))
return false;
QString problem;
if (!openDatabase(db, database, &problem) || !readyToRead(db, project, &problem))
return noted(error, lost, problem);
//Project properties
QHash<QString, QVariant> meta;
@@ -1184,10 +1213,9 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
props.appendChild(entryElement(document, s.value(0), QStringLiteral("name"), s.value(1), s.value(2),
QStringLiteral("property"), s.value(3)));
restoreWhitespace(props, meta.value(QStringLiteral("props_attrs")));
if (!replaceMarker(project, props)) {
setError(error, QStringLiteral("the project's properties have no place"));
if (!replaceMarker(project, props)
&& !noted(error, lost, QStringLiteral("the project's properties have no place")))
return false;
}
}
if (meta.contains(QStringLiteral("terminal_strips"))) {
@@ -1204,17 +1232,53 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
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"));
if (!replaceMarker(project, stripsXml(document, rows))
&& !noted(error, lost, QStringLiteral("the terminal strips have no place")))
return false;
}
}
UsedRows used;
for (QDomElement d : children(project, QStringLiteral("diagram")))
if (!restoreDiagram(db, d, used, error))
if (!restoreDiagram(db, d, used, error, lost))
return false;
return noRowLeft(db, used, error);
if (!noRowLeft(db, used, &problem))
return noted(error, lost, problem);
return true;
}
/**
@brief QetContainerDb::dropLeftovers
After restore() of a damaged project (with lost): take out of
@p project what only the database could have filled -- the markers of
blocks it held, the keys of rows -- and the wires whose ends it held,
which cannot be drawn without them. Each kind left out is said in @p lost.
*/
void QetContainerDb::dropLeftovers(QDomElement project, QStringList *lost)
{
QList<QDomElement> markers, wires;
QList<QDomElement> all{project};
for (int i = 0 ; i < all.size() ; ++i) {
QDomElement e = all.at(i);
for (QDomElement c = e.firstChildElement() ; !c.isNull() ; c = c.nextSiblingElement())
all << c;
if (e.tagName() == MarkerTag) markers << e;
e.removeAttribute(KeyAttribute);
e.removeAttribute(FolioAttribute);
e.removeAttribute(KeptPartAttribute);
if (e.tagName() == QLatin1String("conductor") && e.parentNode().nodeName() == QLatin1String("conductors")
//Every format names a wire's ends by terminal (older files
//by terminal only), and a .qetz holds them in the database
&& (!e.hasAttribute(QStringLiteral("terminal1")) || !e.hasAttribute(QStringLiteral("terminal2"))))
wires << e;
}
for (QDomElement m : std::as_const(markers))
m.parentNode().removeChild(m);
for (QDomElement w : std::as_const(wires))
w.parentNode().removeChild(w);
if (lost && !markers.isEmpty())
lost->append(QStringLiteral("%1 blocks of information the database held").arg(markers.size()));
if (lost && !wires.isEmpty())
lost->append(QStringLiteral("%1 wires, whose ends the database held").arg(wires.size()));
}
/**
+8 -1
View File
@@ -20,6 +20,7 @@
#include <QByteArray>
#include <QDomElement>
#include <QStringList>
/**
@brief The QetContainerDb namespace
@@ -41,7 +42,13 @@ namespace QetContainerDb
{
QByteArray extract(QDomElement project, QString *error = nullptr,
const QByteArray &kept_from = QByteArray());
bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr);
/// With @p lost, a damaged project being recovered: what does not
/// match is noted there and left out, instead of refusing it all
bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr,
QStringList *lost = nullptr);
/// What only the database could fill, left in @p project after a
/// restore() with lost, taken out; said in @p lost
void dropLeftovers(QDomElement project, QStringList *lost);
/// true if @p database is of the format this writes
bool currentFormat(const QByteArray &database);
}
+20 -4
View File
@@ -162,9 +162,12 @@ bool QetZip::write(const QString &path, const QList<Entry> &entries, QString *er
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.
MaxUncompressedSize bytes refuse the whole archive. With @p skipped (a
damaged project recovered), an entry that cannot be read is named
there and left out instead; the size limits still refuse it all.
*/
bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QString *error)
bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QString *error,
QStringList *skipped)
{
if (!entries) return false;
entries->clear();
@@ -190,6 +193,10 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
{
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
if (skipped) {
skipped->append(QStringLiteral("unreadable entry %1").arg(i));
continue;
}
setError(error, QStringLiteral("unreadable entry %1").arg(i));
mz_zip_reader_end(&zip);
return false;
@@ -199,6 +206,10 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
const QString name = QString::fromUtf8(stat.m_filename);
if (stat.m_is_encrypted || !stat.m_is_supported || !safeName(name)
|| names.contains(name)) {
if (skipped) {
skipped->append(QStringLiteral("entry \"%1\" cannot be read").arg(name));
continue;
}
setError(error, QStringLiteral("entry \"%1\" cannot be read").arg(name));
mz_zip_reader_end(&zip);
return false;
@@ -213,8 +224,13 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
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)))));
const QString problem = QStringLiteral("cannot unpack \"%1\": %2")
.arg(name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip))));
if (skipped) {
skipped->append(problem);
continue;
}
setError(error, problem);
mz_zip_reader_end(&zip);
return false;
}
+3 -1
View File
@@ -21,6 +21,7 @@
#include <QByteArray>
#include <QList>
#include <QString>
#include <QStringList>
/**
@brief The QetZip namespace
@@ -49,7 +50,8 @@ namespace QetZip
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);
bool fromBytes(const QByteArray &zip, QList<Entry> *entries, QString *error = nullptr,
QStringList *skipped = nullptr);
}
#endif // QETZIP_H
+113
View File
@@ -797,6 +797,119 @@ private slots:
QString error;
QVERIFY(!QetContainer::join({{QStringLiteral("mimetype"), "application/zip", false}}, &back, &error));
}
// recover() reads what it can of a damaged .qetz: each kind of damage
// costs what it held and nothing else, and is said
void recoverLeavesOutOnlyTheDamage_data()
{
QTest::addColumn<QString>("damage");
QTest::addColumn<QString>("said");
QTest::addColumn<int>("folios");
QTest::addColumn<int>("wires");
QTest::newRow("nothing") << QString() << QString() << 0 << 0;
QTest::newRow("folio file missing") << QStringLiteral("folio") << QStringLiteral("is missing") << -1 << 0;
QTest::newRow("wire row deleted") << QStringLiteral("row") << QStringLiteral("not in the database") << 0 << -1;
QTest::newRow("database page overwritten") << QStringLiteral("page") << QStringLiteral("cannot be read") << 0 << -1000;
}
void recoverLeavesOutOnlyTheDamage()
{
QFETCH(QString, damage);
QFETCH(QString, said);
QFETCH(int, folios);
QFETCH(int, wires);
QDomDocument original;
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
QVERIFY(!entries.isEmpty());
auto count = [](const QDomDocument &d, const QString &tag) {
return int(d.documentElement().elementsByTagName(tag).size());
};
const int folios_before = count(original, QStringLiteral("diagram"));
const int wires_before = count(original, QStringLiteral("conductor"));
if (damage == QLatin1String("folio")) {
for (int i = 0 ; i < entries.size() ; ++i)
if (entries.at(i).name.startsWith(QStringLiteral("folios/"))) { entries.removeAt(i); break; }
} else if (damage == QLatin1String("row")) {
alterDatabase(entries, "DELETE FROM conductor WHERE rowid = (SELECT min(rowid) FROM conductor)");
} else if (damage == QLatin1String("page")) {
for (QetZip::Entry &entry : entries) {
if (entry.name != QLatin1String("project.sqlite")) continue;
const int page_size = qFromBigEndian<quint16>(entry.data.constData() + 16);
const int page = int(entry.data.size() / page_size / 2);
entry.data.replace(page * page_size, page_size, QByteArray(page_size, char(0xff)));
}
}
QByteArray zip;
QString error;
QVERIFY2(QetZip::toBytes(entries, &error).size() > 0, qPrintable(error));
zip = QetZip::toBytes(entries, &error);
QDomDocument back;
QStringList lost;
QVERIFY2(QetContainer::recover(zip, &back, &lost, &error), qPrintable(error));
if (said.isEmpty()) {
QVERIFY2(lost.isEmpty(), qPrintable(lost.join(QLatin1Char('\n'))));
} else {
QVERIFY2(lost.join(QLatin1Char('\n')).contains(said), qPrintable(lost.join(QLatin1Char('\n'))));
// Strict reading refuses it
QDomDocument strict;
QVERIFY(!QetContainer::join(entries, &strict));
}
QCOMPARE(count(back, QStringLiteral("diagram")), folios_before + folios);
if (wires == -1000)
QCOMPARE(count(back, QStringLiteral("conductor")), 0);
else
QCOMPARE(count(back, QStringLiteral("conductor")), wires_before + wires);
// Nothing of the container's own left in the document
QVERIFY(!back.toString().contains(QStringLiteral("qetz-")));
}
// A picture's file missing (F118 wrote such projects): the picture is
// left out, the rest read
void recoverMissingPicture()
{
QList<QetZip::Entry> entries;
QString error;
QVERIFY2(QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries, &error),
qPrintable(error));
QDomDocument document;
QVERIFY2(QetContainer::join(entries, &document, &error), qPrintable(error));
// A picture on the first folio, then saved and its file dropped
QDomElement folio = document.documentElement().firstChildElement(QStringLiteral("diagram"));
QDomElement images = folio.firstChildElement(QStringLiteral("images"));
if (images.isNull()) images = folio.appendChild(document.createElement(QStringLiteral("images"))).toElement();
QDomElement image = document.createElement(QStringLiteral("image"));
image.setAttribute(QStringLiteral("type"), QStringLiteral("png"));
image.appendChild(document.createTextNode(QStringLiteral(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")));
images.appendChild(image);
QList<QetZip::Entry> saved = QetContainer::split(document, QString(), &error);
QVERIFY2(!saved.isEmpty(), qPrintable(error));
bool dropped = false;
for (int i = 0 ; i < saved.size() ; ++i)
if (saved.at(i).name.startsWith(QStringLiteral("images/"))) { saved.removeAt(i); dropped = true; break; }
QVERIFY(dropped);
QDomDocument strict;
QVERIFY(!QetContainer::join(saved, &strict, &error));
QVERIFY(error.contains(QStringLiteral("picture")));
QDomDocument back;
QStringList lost;
QVERIFY2(QetContainer::recover(QetZip::toBytes(saved), &back, &lost, &error), qPrintable(error));
QCOMPARE(lost.size(), 1);
QVERIFY(lost.first().contains(QStringLiteral("picture")));
QCOMPARE(int(back.documentElement().elementsByTagName(QStringLiteral("image")).size()), 0);
QVERIFY(back.toString().contains(QStringLiteral("<elementInformation ")));
}
// What cannot be read as a zip at all is still refused
void recoverNotAZipRefused()
{
QDomDocument back;
QStringList lost;
QString error;
QVERIFY(!QetContainer::recover(QByteArray("not a zip").repeated(10), &back, &lost, &error));
QVERIFY(!error.isEmpty());
}
};
QTEST_APPLESS_MAIN(tst_qetcontainer)