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));
+7
View File
@@ -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()) {
+31
View File
@@ -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");
+19
View File
@@ -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<QetZip::Entry> 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;