mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-17 11:24:14 +02:00
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:
@@ -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";
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user