Add recovering what can be read of a damaged .qetz (--recover)

A .qetz that does not open as it is -- a picture's file missing (as
the title-change fault wrote them), a folio file missing, a row deleted
from project.sqlite by hand (issue #1458), a damaged database -- was
refused whole, and nothing in QElectroTech could get anything back.

QetContainer::recover() reads what it can and says what it left out:
an entry that cannot be unpacked, a missing folio file or picture, a
symbol or wire that does not match the database, and, when the
database cannot be read at all, the information it held (the wires go
too, as it holds their ends). join(), restore() and QetZip::fromBytes()
take an optional list for this; given none they refuse as before.
`qelectrotech --recover damaged.qetz out.qet` writes what was read to a
.qet and lists what was left out.

Measured on industrial (50 folios, 618 symbols, 671 wires): a wire row
deleted -> 670 wires, the rest whole; a folio file missing -> 49
folios; a database page overwritten -> every folio and symbol drawn, no
wires and no symbol information. A zip cut short (no central directory)
still cannot be read.

Tests: recoverLeavesOutOnlyTheDamage (nothing, folio file, wire row,
database page), recoverMissingPicture, recoverNotAZipRefused; they fail
with the database side kept strict. The strict cases pass unchanged.
ctest 106/106.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-16 10:15:37 +13:00
parent c4b5cf2f11
commit 2bd19e04be
9 changed files with 349 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)) {
@@ -1042,6 +1080,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
@@ -749,8 +749,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,
@@ -759,10 +773,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);
@@ -777,24 +792,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);
@@ -821,25 +842,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);
@@ -1135,12 +1162,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;
@@ -1156,10 +1185,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"))) {
@@ -1176,17 +1204,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
@@ -160,9 +160,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();
@@ -188,6 +191,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;
@@ -197,6 +204,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;
@@ -211,8 +222,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
+114
View File
@@ -3,6 +3,7 @@
#include <QDir>
#include <QFile>
#include <QtEndian>
#include <functional>
@@ -764,6 +765,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)