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 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-12 20:43:57 +13:00
parent 5c4fa8fadf
commit 60489c9893
4 changed files with 64 additions and 1 deletions
+7 -1
View File
@@ -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));