From 60489c98938624cbe31064bfc056f9c2d7fdc61a Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 12 Oct 2026 20:43:57 +1300 Subject: [PATCH] Fix two faults the end-state gates found (stage D7) A .qetz re-saved after opening from its index saved wrong folio numbers in its database: reading each folio's header wrote its row with a number counted from the folios read so far ("1/1", "2/2" for "1/3", "2/3"), and an index-opened project keeps the rows it was saved with, so nothing wrote them again. The folio table's rows are no longer written while the project is being read (projectDataBase:: diagramInfoChanged()). Folio list tables showed it. A .qetz whose project.sqlite is not a database crashed QElectroTech: the first statement could not be prepared and SQLite was handed a null statement. A statement that cannot be prepared now binds nothing and steps to no row, and the file is refused. Gates: 342 corpus projects, written by QElectroTech 0.3 to 0.200, all open; .qet -> .qetz -> .qet equals the plain resave with every folio built; a .qetz re-saved twice holds the same bytes entry by entry but the save's own name, path and time (the first fault made the first re-save differ); hostile .qetz (truncated, garbage database, view for a table, trigger, escaping and repeated names, later format, missing folio, 2 GiB zip bomb) neither crash nor hang and none opens; an unknown extra entry still opens. tst_lazyfolios: folio numbers with totals from the index as when built (fails without the fix); tst_qetcontainer: garbage database refused (segfaults without it). ctest 96/96 both ways; save and export gates same on 342. Co-Authored-By: Claude Opus 5.5 --- sources/container/qetcontainerdb.cpp | 8 ++++++- sources/dataBase/projectdatabase.cpp | 7 +++++++ tests/qttest/lazy_folios_probe.cpp | 31 ++++++++++++++++++++++++++++ tests/qttest/tst_qetcontainer.cpp | 19 +++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index f455f945a..3d706e975 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -149,7 +149,11 @@ class Statement } ~Statement() { sqlite3_finalize(m_stmt); } bool ok() const { return m_stmt; } + //A statement that could not be prepared (the database is not + //one, a table is missing) binds nothing and steps to no row: + //SQLite's own calls are not all safe on a null statement void bind(const QVariantList &values) { + if (!m_stmt) return; sqlite3_reset(m_stmt); sqlite3_clear_bindings(m_stmt); for (int i = 0 ; i < values.size() ; ++i) { @@ -165,11 +169,13 @@ class Statement } } bool run(const QVariantList &values) { + if (!m_stmt) return false; bind(values); return sqlite3_step(m_stmt) == SQLITE_DONE; } - bool next() { return sqlite3_step(m_stmt) == SQLITE_ROW; } + bool next() { return m_stmt && sqlite3_step(m_stmt) == SQLITE_ROW; } QVariant value(int column) const { + if (!m_stmt) return QVariant(); switch (sqlite3_column_type(m_stmt, column)) { case SQLITE_NULL: return QVariant(); case SQLITE_INTEGER: return QVariant(sqlite3_column_int(m_stmt, column)); diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 15da3aeae..d73675c59 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -2052,6 +2052,13 @@ void projectDataBase::removeDiagram(Diagram *diagram) void projectDataBase::diagramInfoChanged(Diagram *diagram) { m_content_changed = true; + //Not while the project is being read: a folio's header read gives + //it a folio number counted from the folios read so far ("1/1" for + //the first of three); the rows are filled once every folio is + //read, from the document or from the index a .qetz was saved with + if (m_update_blocked) { + return; + } bindDiagramInfoValues(m_update_diagram_info_query, diagram); if (!m_update_diagram_info_query.exec()) { diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp index 09630399c..2a3349ad5 100644 --- a/tests/qttest/lazy_folios_probe.cpp +++ b/tests/qttest/lazy_folios_probe.cpp @@ -765,6 +765,37 @@ int main(int argc, char **argv) check(indexed.unloadedFolioCount() == folios.size() - strip_folios.size(), "the .qetz opens building only the folios of the terminal strip's terminals"); check(numbering(indexed) == numbering(eager), "symbols' numbering from the index as when built"); + //The folios' rows as saved, not as reading their headers one + //by one numbers them ("1/1" for the first of three, D7) + { + auto folioRows = [](QETProject &project) { + QStringList rows; + for (const QString &row : tables(project)) + if (row.startsWith("diagram")) rows << row; + return rows; + }; + check(folioRows(indexed) == folioRows(eager), "the folios' rows from the index as when built"); + //Folio numbers that count the folios ("%id/%total") + const QString totals = output + "/totals.qetz"; + qputenv("QET_EAGER_FOLIOS", "1"); + { + QETProject numbered(zipped); + QetScriptApi api(&numbered); + for (int i = 0 ; i < api.folioCount() ; ++i) + api.setFolioProperty(i, "folio", "%id/%total"); + check(numbered.writeContainerFile(totals, &error), "folio numbers with totals saved"); + } + QETProject usual_totals(totals); + qunsetenv("QET_EAGER_FOLIOS"); + QETProject lazy_totals(totals); + check(lazy_totals.openedFromIndex(), "opened from its index"); + const QStringList lazy_rows = folioRows(lazy_totals), usual_rows = folioRows(usual_totals); + if (lazy_rows != usual_rows) + for (int i = 0 ; i < lazy_rows.size() && i < 3 ; ++i) + std::printf(" lazily %s\n built %s\n", qPrintable(lazy_rows.at(i).left(140)), + qPrintable(usual_rows.value(i).left(140))); + check(lazy_rows == usual_rows, "folio numbers with totals from the index as when built"); + } for (int i = 0; i < folios.size(); ++i) same(indexed, i, "shown from the .qetz"); check(indexed.toXml().toString() == eager.toXml().toString(), "the .qetz saves as when built"); diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index 1bccb0ce0..592d011e6 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -319,6 +319,25 @@ private slots: // A crafted project.sqlite whose "element" is a view, not a table, is // refused: nothing in a file's schema is trusted. + void garbageDatabaseRefused() + { + //Bytes that are no database: refused, not read (crashed reading + //the first statement until stage D7) + QDomDocument original; + QList entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original); + bool replaced = false; + for (QetZip::Entry &entry : entries) + if (entry.name == QLatin1String("project.sqlite")) { + entry.data = QByteArray("not a database").repeated(100); + replaced = true; + } + QVERIFY(replaced); + QDomDocument back; + QString error; + QVERIFY(!QetContainer::join(entries, &back, &error)); + QVERIFY(!error.isEmpty()); + } + void viewInsteadOfTableRefused() { QDomDocument original;