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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-16 10:15:37 +13:00
parent c4b5cf2f11
commit 2bd19e04be
9 changed files with 349 additions and 53 deletions
+20 -4
View File
@@ -160,9 +160,12 @@ bool QetZip::write(const QString &path, const QList<Entry> &entries, QString *er
Read every file entry of the zip @p zip into @p entries, in archive
order. Folders are skipped. An encrypted entry, an unsafe or repeated
name, a method miniz cannot read, or more than MaxEntries entries or
MaxUncompressedSize bytes refuse the whole archive.
MaxUncompressedSize bytes refuse the whole archive. With @p skipped (a
damaged project recovered), an entry that cannot be read is named
there and left out instead; the size limits still refuse it all.
*/
bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QString *error)
bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QString *error,
QStringList *skipped)
{
if (!entries) return false;
entries->clear();
@@ -188,6 +191,10 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
{
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
if (skipped) {
skipped->append(QStringLiteral("unreadable entry %1").arg(i));
continue;
}
setError(error, QStringLiteral("unreadable entry %1").arg(i));
mz_zip_reader_end(&zip);
return false;
@@ -197,6 +204,10 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
const QString name = QString::fromUtf8(stat.m_filename);
if (stat.m_is_encrypted || !stat.m_is_supported || !safeName(name)
|| names.contains(name)) {
if (skipped) {
skipped->append(QStringLiteral("entry \"%1\" cannot be read").arg(name));
continue;
}
setError(error, QStringLiteral("entry \"%1\" cannot be read").arg(name));
mz_zip_reader_end(&zip);
return false;
@@ -211,8 +222,13 @@ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList<Entry> *entries, QStri
size_t size = 0;
void *data = mz_zip_reader_extract_to_heap(&zip, i, &size, 0);
if (!data) {
setError(error, QStringLiteral("cannot unpack \"%1\": %2")
.arg(name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip)))));
const QString problem = QStringLiteral("cannot unpack \"%1\": %2")
.arg(name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip))));
if (skipped) {
skipped->append(problem);
continue;
}
setError(error, problem);
mz_zip_reader_end(&zip);
return false;
}