From 2269a4641b20d7ee189d49e7d514f69d20f206e0 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Tue, 15 Sep 2026 23:34:17 +0000 Subject: [PATCH] Persist the project uuid, derived from the file content for legacy files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QETProject::m_uuid was created in the constructor and never written, so a project got a new uuid every time it was opened. Inside a running instance it is only used to name the SQLite connection, but nothing outside the instance could tell which project a file belongs to. Motivation A .qet file is increasingly handled by tools outside QElectroTech: Git repositories on GitHub or GitLab, cloud storage, key-value stores, per-project locks. All of them need a stable key for "this project": - The file name and path are not stable: files get renamed, moved, checked out in different places. - The project title is user-editable and not unique. - Folio uuids (persisted separately) are only unique within their project; keying folios globally needs a project identifier as well, e.g. projects/{projectUuid}/folios/{folioUuid}. Change Write the uuid as an attribute of and restore it in QETProject::openFile(), right after parsing and before the project is built from the XML. Older versions ignore the attribute, so files stay readable in both directions. The project database is not affected: it takes its connection name from the uuid created at construction (m_uuid is declared before m_data_base), before the file is read. Two open projects carrying the same persisted uuid therefore still get distinct connections. Files without a uuid: why not a random one Keeping the random uuid created by the constructor and saving it conflicts with #754 / #779: saving an unmodified project must give the same bytes every time. Every example project predates the attribute. Measured on the 24 example projects (resaved 3x each from the same original, QT_HASH_SEED=0 so that QDom's attribute order is stable, isolated HOME per run): upstream master 23/24 byte-identical persist, random uuid 0/24 persist, derived uuid (this) 23/24 The remaining project, schema_indus.qet, differs only in element uuids, the known residual #779 leaves for elements; its project uuid is stable. Instead, a project file without a uuid gets a name-based (version 5) uuid derived from the raw content of the file: QUuid::createUuidV5(, "qet-project-legacy\n" + file content without CR) - The same file always yields the same uuid, so resaving an unmodified legacy project stays reproducible. - Different projects practically never share a uuid, because any difference in content gives a different one. This is unlike folios, where only data such as title and position could be used; the raw file bytes are stable input for the whole project. - Carriage returns are dropped before hashing. QFile's Text mode already strips them on Windows but not elsewhere, and git's autocrlf can change them on checkout; either way the uuid is the same on every platform. - The uuid is derived once, at load time, and saved from then on. After that it is read, never recomputed: renaming the project, editing it or changing it in the same session as the migration does not change it. - Two people opening the same legacy file on different branches get the same project uuid. The namespace uuid is fixed in the code and must never change, or every legacy project would get a different uuid. Known limitations, open for discussion - Copies share the uuid. Two byte-identical legacy files get the same uuid (examples/cablage-eclairages_sikli-v5.qet and câblage-éclairages-sikli-v5.qet are such a pair), and so does a migrated file copied in the file manager or saved with "Save as". That is what identity means for a copy, and the same happens with Git, but a tool that treats the uuid as globally unique has to cope with it. Regenerating the uuid on "Save as" could be a follow-up, if that is the preferred behaviour. - A legacy file that differs from another only in formatting (e.g. re-indented) gets a different uuid. The two sides of a merge only agree if they started from the same bytes, which is the normal case. Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock / --info) - 24 example projects, 3 resaves each from the same original: results above; the project uuid is identical across runs. All 24 uuids are distinct, except the byte-identical pair mentioned above. - Resaving an already migrated file is byte-identical to the first output. - The same legacy file with CRLF line endings gets the same uuid as with LF. - Changing the project title in a migrated file keeps its uuid. - Migrating and modifying in the same run (--set-titleblock on a legacy file) gives the same uuid as a plain resave. - Re-indenting a legacy file gives a different uuid (expected). - A migrated file opened with upstream master loads normally; the attribute is ignored and dropped on save. - --info on a migrated file still works. Refs #754, #779 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp --- sources/qetproject.cpp | 54 ++++++++++++++++++++++++++++++++++++++++-- sources/qetproject.h | 1 + 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index a86670dc2..18afbdde6 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -255,6 +255,38 @@ void QETProject::init() } +/** + @brief QETProject::derivedUuid + Name-based (version 5) uuid for a project file that has no uuid yet, + because it was written before the uuid was persisted. + + A random uuid would do as an identity, but it would make saving an + unmodified legacy project non-reproducible : every load would invent a + different one and write it out (see #754). The uuid is therefore derived + from the content of the file, so the same file always yields the same + uuid, while two different projects practically never share one. + Carriage returns are dropped first, so that a checkout with CRLF line + endings (Windows, git autocrlf) gives the same uuid as one with LF. + + It is computed once, when the file is loaded, and saved from then on : + editing, renaming or moving the project later does not change it. + @param content : the raw content of the project file + @return the derived uuid +*/ +QUuid QETProject::derivedUuid(const QByteArray &content) +{ + //Fixed namespace for QElectroTech project uuids, never change it : + //doing so would change the uuid given to every legacy project. + static const QUuid project_namespace( + QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}")); + + QByteArray normalized(content); + normalized.replace('\r', QByteArray()); + return QUuid::createUuidV5(project_namespace, + QByteArrayLiteral("qet-project-legacy\n") + + normalized); +} + /** @brief QETProject::openFile @param file @@ -274,9 +306,11 @@ QETProject::ProjectState QETProject::openFile(QFile *file) QFileInfo fi(*file); setFilePath(fi.absoluteFilePath()); - //Extract the content of the xml + //Extract the content of the xml. The raw bytes are kept : a project + //file without a persisted uuid derives its uuid from them. + const QByteArray content = file->readAll(); QDomDocument xml_project; - if (!xml_project.setContent(file)) + if (!xml_project.setContent(content)) { if(opened_here) { file->close(); @@ -285,6 +319,17 @@ QETProject::ProjectState QETProject::openFile(QFile *file) } const qint64 xml_parse_ms = load_timer.elapsed(); + //Restore the persisted project uuid before anything else is built + //from the file. The project database already got its connection name + //from the uuid created at construction, it does not depend on this. + const QDomElement root_elmt = xml_project.documentElement(); + if (root_elmt.tagName() == QLatin1String("project")) + { + const QUuid persisted_uuid(root_elmt.attribute(QStringLiteral("uuid"))); + m_uuid = persisted_uuid.isNull() ? derivedUuid(content) + : persisted_uuid; + } + //Build the project from the xml readProjectXml(xml_project); @@ -1038,6 +1083,11 @@ QDomDocument QETProject::toXml() setTitle(QFileInfo(m_file_path).completeBaseName()); } project_root.setAttribute("title", project_title_); + //Persist the project identity, so that the project keeps the same + //uuid across save/load. Without it every load invents a new one, and + //nothing outside the running instance (version control, a cloud or + //key-value store, a lock...) can tell which project a file belongs to. + project_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString()); xml_doc.appendChild(project_root); // titleblock templates, if any diff --git a/sources/qetproject.h b/sources/qetproject.h index 5ec4f4ae7..91339c8ee 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -271,6 +271,7 @@ class QETProject : public QObject void writeBackup(); void init(); ProjectState openFile(QFile *file); + static QUuid derivedUuid(const QByteArray &content); void refresh(); // attributes