.qetz: keep a symbol's type in its folio; refuse a database that does not match

A folio file in a .qetz did not say which symbol each item is: the type
(embed://...) was moved to project.sqlite, so a folio read alone could
not be drawn or diffed. It stays in the folio file now; format 2 files,
which moved it, are still read.

Nothing checked that project.sqlite and the folio files agree: a row no
item has, an item's row on another folio, or information rows of no
symbol were read as far as they went. Reading now refuses such a file and
says what does not match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-14 08:57:25 +13:00
parent 8d118faf36
commit 5975b75766
2 changed files with 139 additions and 7 deletions
+59 -7
View File
@@ -63,9 +63,14 @@ const QStringList FolioData = {
QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"),
QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")};
//From format 3 on an item's uuid is its row's key, not a column of its
//own: it stays in the folio file, where it says which row is the item's
//own: it stays in the folio file, where it says which row is the item's.
//So does a symbol's type, the symbol it is: a folio file read alone
//still says what is drawn where
const QStringList ElementData = {
QStringLiteral("type"), QStringLiteral("prefix"), QStringLiteral("freezeLabel")};
QStringLiteral("prefix"), QStringLiteral("freezeLabel")};
//Format 2 also moved the type
const QStringList ElementDataV2 = {
QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"), QStringLiteral("freezeLabel")};
const QStringList ConductorData = {
QStringLiteral("element1"), QStringLiteral("terminal1"),
QStringLiteral("element2"), QStringLiteral("terminal2"), QStringLiteral("num"),
@@ -735,7 +740,7 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
const QString item_key = v2 ? QStringLiteral("key") : QStringLiteral("uuid");
const QString element_ref = v2 ? QStringLiteral("element_key") : QStringLiteral("element_uuid");
const QString show_column = v2 ? QStringLiteral("NULL") : QStringLiteral("show");
const QStringList element_data = v2 ? QStringList{QStringLiteral("uuid")} + ElementData : ElementData;
const QStringList element_data = v2 ? ElementDataV2 : ElementData;
const QStringList conductor_data = v2 ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData;
auto quoted = [](const QStringList &names) {
return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\"");
@@ -783,10 +788,22 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
}
}
//Every row must be an item's, once, on the folio the item is on:
//nothing enforces it in the file, which is untrusted
QSet<QString> folios_used, elements_used, conductors_used;
auto use = [error](QSet<QString> &used, const QString &key, const QString &what) {
if (used.contains(key)) {
setError(error, QStringLiteral("%1 %2 is in the folios twice").arg(what, key));
return false;
}
used.insert(key);
return true;
};
for (QDomElement d : children(project, QStringLiteral("diagram")))
{
const QString id = keyOf(d, FolioAttribute);
if (id.isEmpty()) continue;
if (!use(folios_used, id, QStringLiteral("folio"))) return false;
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE %2 = ?")
.arg(quoted(FolioData), folio_key));
f.bind({id});
@@ -813,10 +830,15 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
for (QDomElement e : children(list, QStringLiteral("element"))) {
const QString key = keyOf(e, KeyAttribute);
if (key.isEmpty()) continue;
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE %2 = ?")
.arg(quoted(element_data), item_key));
if (!use(elements_used, key, QStringLiteral("symbol"))) return false;
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs, %2 FROM element WHERE %3 = ?")
.arg(quoted(element_data), folio_ref, item_key));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
if (s.value(element_data.size() + 4).toString() != id) {
setError(error, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id));
return false;
}
QVariantList v;
for (int i = 0 ; i < element_data.size() + 4 ; ++i) v << s.value(i);
put(e, element_data, v, 0);
@@ -854,15 +876,45 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
const QString key = keyOf(c, KeyAttribute);
if (key.isEmpty()) continue;
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE %2 = ?")
.arg(quoted(conductor_data), item_key));
if (!use(conductors_used, key, QStringLiteral("wire"))) return false;
Statement s(db.m_db, QStringLiteral("SELECT %1, %2 FROM conductor WHERE %3 = ?")
.arg(quoted(conductor_data), folio_ref, item_key));
s.bind({key});
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
if (s.value(conductor_data.size()).toString() != id) {
setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id));
return false;
}
QVariantList v;
for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i);
put(c, conductor_data, v, 0);
}
}
}
//No row left over: each table's rows, counted, are the items read,
//and every entry belongs to a row
auto count = [&db](const QString &sql) {
Statement s(db.m_db, sql);
return s.next() ? s.value(0).toInt() : -1;
};
const struct { QString sql; int expected; QString what; } checks[] = {
{QStringLiteral("SELECT count(*) FROM folio"), int(folios_used.size()), QStringLiteral("folio rows")},
{QStringLiteral("SELECT count(*) FROM element"), int(elements_used.size()), QStringLiteral("symbol rows")},
{QStringLiteral("SELECT count(*) FROM conductor"), int(conductors_used.size()), QStringLiteral("wire rows")},
{QStringLiteral("SELECT count(*) FROM folio_property WHERE %1 NOT IN (SELECT %2 FROM folio)")
.arg(folio_ref, folio_key), 0, QStringLiteral("folio property rows of no folio")},
{QStringLiteral("SELECT count(*) FROM element_info WHERE %1 NOT IN (SELECT %2 FROM element)")
.arg(element_ref, item_key), 0, QStringLiteral("information rows of no symbol")},
{QStringLiteral("SELECT count(*) FROM link WHERE %1 NOT IN (SELECT %2 FROM element)")
.arg(element_ref, item_key), 0, QStringLiteral("link rows of no symbol")}};
for (const auto &check : checks) {
const int found = count(check.sql);
if (found != check.expected) {
setError(error, QStringLiteral("the project database does not match its folios: %1 %2, %3 expected")
.arg(found).arg(check.what).arg(check.expected));
return false;
}
}
return true;
}
+80
View File
@@ -323,6 +323,86 @@ private slots:
// A crafted project.sqlite whose "element" is a view, not a table, is
// refused: nothing in a file's schema is trusted.
// project.sqlite changed by @p sql in @p entries
static void alterDatabase(QList<QetZip::Entry> &entries, const char *sql)
{
for (QetZip::Entry &e : entries) {
if (e.name != QLatin1String("project.sqlite")) continue;
sqlite3 *db = nullptr;
sqlite3_open(":memory:", &db);
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(e.data.size())));
memcpy(copy, e.data.constData(), size_t(e.data.size()));
sqlite3_deserialize(db, "main", copy, e.data.size(), e.data.size(),
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
QCOMPARE(sqlite3_exec(db, sql, nullptr, nullptr, nullptr), SQLITE_OK);
sqlite3_int64 size = 0;
unsigned char *bytes = sqlite3_serialize(db, "main", &size, 0);
e.data = QByteArray(reinterpret_cast<const char *>(bytes), qsizetype(size));
sqlite3_free(bytes);
sqlite3_close(db);
}
}
// A database that does not match its folio files -- a row no item has,
// an item's row on another folio, entries of no symbol -- is refused
// with what is wrong, not read as far as it goes
void mismatchRefused_data()
{
QTest::addColumn<QByteArray>("sql");
QTest::addColumn<QString>("said");
QTest::newRow("symbol row of no symbol")
<< QByteArray("INSERT INTO element SELECT 'x', folio_uuid, prefix, freezeLabel, info_at, info_attrs,"
" links_at, links_attrs FROM element LIMIT 1")
<< QStringLiteral("symbol rows");
QTest::newRow("wire row of no wire")
<< QByteArray("INSERT INTO conductor SELECT 'x', folio_uuid, element1, terminal1, element2, terminal2,"
" num, formula, function, tension_protocol, conductor_color, conductor_section, cable,"
" bus, freezeLabel FROM conductor LIMIT 1")
<< QStringLiteral("wire rows");
QTest::newRow("symbol on another folio")
<< QByteArray("UPDATE element SET folio_uuid = (SELECT uuid FROM folio WHERE pos = 2)"
" WHERE folio_uuid = (SELECT uuid FROM folio WHERE pos = 1)")
<< QStringLiteral("the database says another");
QTest::newRow("information of no symbol")
<< QByteArray("UPDATE element_info SET element_uuid = 'x' WHERE rowid = 1")
<< QStringLiteral("information rows of no symbol");
}
void mismatchRefused()
{
QFETCH(QByteArray, sql);
QFETCH(QString, said);
QDomDocument original;
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
QVERIFY(!entries.isEmpty());
{
QDomDocument back;
QString error;
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
}
alterDatabase(entries, sql.constData());
QDomDocument back;
QString error;
QVERIFY(!QetContainer::join(entries, &back, &error));
QVERIFY2(error.contains(said), qPrintable(error));
}
// A folio file says which symbol each item is (its type), read alone
void folioSaysWhichSymbol()
{
QDomDocument original;
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
QVERIFY(!entries.isEmpty());
int typed = 0, symbols = 0;
for (const QetZip::Entry &e : entries) {
if (!e.name.startsWith(QStringLiteral("folios/"))) continue;
const QString text = QString::fromUtf8(e.data);
symbols += int(text.count(QStringLiteral("<element ")));
typed += int(text.count(QRegularExpression(QStringLiteral("<element [^>]*type=\"embed://"))));
}
QVERIFY(symbols > 0);
QCOMPARE(typed, symbols);
}
void viewInsteadOfTableRefused()
{
QDomDocument original;