diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index 5c4d5188f..262cf1e88 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -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 &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 \n"; + return 2; + } + return recoverProject(rest.at(0), rest.at(1)); + } + const QString input = rest.value(0); if (input.isEmpty()) { err << "Usage: qelectrotech " << flag << " \n"; diff --git a/sources/cli_export.h b/sources/cli_export.h index 09f66826f..357746403 100644 --- a/sources/cli_export.h +++ b/sources/cli_export.h @@ -55,6 +55,7 @@ namespace CLIExport { qelectrotech --info [output.json] qelectrotech --check-elements qelectrotech --resave + qelectrotech --recover qelectrotech --set-titleblock key=value... PDF: one multi-page document (one diagram per page). diff --git a/sources/container/qetcontainer.cpp b/sources/container/qetcontainer.cpp index 406e43751..95f306c0e 100644 --- a/sources/container/qetcontainer.cpp +++ b/sources/container/qetcontainer.cpp @@ -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 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 QetContainer::split(const QDomDocument &project, const QStr for a later one this may read (its min-reader allows it). */ bool QetContainer::join(const QList &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 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 &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 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 &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; } diff --git a/sources/container/qetcontainer.h b/sources/container/qetcontainer.h index 404aa630b..7447fafc3 100644 --- a/sources/container/qetcontainer.h +++ b/sources/container/qetcontainer.h @@ -77,7 +77,11 @@ namespace QetContainer const QHash &kept_from = QHash()); bool join(const QList &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); diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index aaab24a38..4b46ca703 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -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 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 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 markers, wires; + QList 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())); } /** diff --git a/sources/container/qetcontainerdb.h b/sources/container/qetcontainerdb.h index 4f4b31408..66b8fa7d0 100644 --- a/sources/container/qetcontainerdb.h +++ b/sources/container/qetcontainerdb.h @@ -20,6 +20,7 @@ #include #include +#include /** @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); } diff --git a/sources/container/qetzip.cpp b/sources/container/qetzip.cpp index dc9e2c9f7..edb780dab 100644 --- a/sources/container/qetzip.cpp +++ b/sources/container/qetzip.cpp @@ -160,9 +160,12 @@ bool QetZip::write(const QString &path, const QList &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 *entries, QString *error) +bool QetZip::fromBytes(const QByteArray &zip_bytes, QList *entries, QString *error, + QStringList *skipped) { if (!entries) return false; entries->clear(); @@ -188,6 +191,10 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList *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 *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 *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; } diff --git a/sources/container/qetzip.h b/sources/container/qetzip.h index 808a861ce..28a423bd7 100644 --- a/sources/container/qetzip.h +++ b/sources/container/qetzip.h @@ -21,6 +21,7 @@ #include #include #include +#include /** @brief The QetZip namespace @@ -49,7 +50,8 @@ namespace QetZip bool write(const QString &path, const QList &entries, QString *error = nullptr); QByteArray toBytes(const QList &entries, QString *error = nullptr); bool read(const QString &path, QList *entries, QString *error = nullptr); - bool fromBytes(const QByteArray &zip, QList *entries, QString *error = nullptr); + bool fromBytes(const QByteArray &zip, QList *entries, QString *error = nullptr, + QStringList *skipped = nullptr); } #endif // QETZIP_H diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index 4c1c020dd..541c0ee70 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -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("damage"); + QTest::addColumn("said"); + QTest::addColumn("folios"); + QTest::addColumn("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 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(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 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 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("