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