From ae151b350ae050797ee2e979c836a3933abe2f4d Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 18 Oct 2026 08:04:13 +1300 Subject: [PATCH] Fix a crash opening a project whose symbol has a terminal with no orientation A terminal in a symbol's definition saved without its orientation attribute crashed QElectroTech whenever the project was opened, in the GUI and with --resave alike: Qet::orientationFromString() read the first character of an empty string. Hand-edited or generated symbols, or a damaged file, can hold such a terminal. An empty string is now an inconsistency like any other, and the terminal faces north, as the function's comment already said. Every non-empty string gives the same orientation as before. tst_terminalnoorientation runs the real binary's --resave on fixtures/terminal_no_orientation.qet (unlinked_contact_label.qet with one terminal's orientation removed): it crashes without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XCdg2Er2vYhB8Vwa5G7fSR --- sources/qet.cpp | 9 +- tests/qttest/CMakeLists.txt | 14 + .../fixtures/terminal_no_orientation.qet | 315 ++++++++++++++++++ tests/qttest/tst_terminalnoorientation.cpp | 89 +++++ 4 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 tests/qttest/fixtures/terminal_no_orientation.qet create mode 100644 tests/qttest/tst_terminalnoorientation.cpp diff --git a/sources/qet.cpp b/sources/qet.cpp index 03a042e89..fc62b0830 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -49,10 +49,11 @@ @return l'orientation designee par la chaine de caractere */ Qet::Orientation Qet::orientationFromString(const QString &s) { - QChar c = s[0]; - if (c == 'e') return(Qet::East); - else if (c == 's') return(Qet::South); - else if (c == 'w') return (Qet::West); + //An empty string (a terminal saved without its orientation) is + //an inconsistency too: s[0] would read past its end and crash + if (s.startsWith(QLatin1Char('e'))) return(Qet::East); + else if (s.startsWith(QLatin1Char('s'))) return(Qet::South); + else if (s.startsWith(QLatin1Char('w'))) return (Qet::West); else return(Qet::North); } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 1304bc356..fbb8cc809 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -665,6 +665,20 @@ target_link_libraries(tst_freetextwidth PRIVATE Qt::Test Qt::Gui Qt::Xml) target_compile_definitions(tst_freetextwidth PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# A terminal in a symbol's definition with no orientation attribute faces +# north instead of crashing the program on opening (Qet::orientationFromString() +# read the first character of an empty string): the real binary's --resave +# on fixtures/terminal_no_orientation.qet. +add_executable( + tst_terminalnoorientation + tst_terminalnoorientation.cpp) +add_test(NAME tst_terminalnoorientation COMMAND tst_terminalnoorientation) +set_tests_properties(tst_terminalnoorientation PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +add_dependencies(tst_terminalnoorientation qelectrotech) +target_link_libraries(tst_terminalnoorientation PRIVATE Qt::Test Qt::Xml) +target_compile_definitions(tst_terminalnoorientation PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # A free text whose x, y or rotation is "nan" or "inf" is loaded at 0 for # that number, so a resave and the SVG export carry no non-finite number: # the real binary on fixtures/free_text_nonfinite.qet. diff --git a/tests/qttest/fixtures/terminal_no_orientation.qet b/tests/qttest/fixtures/terminal_no_orientation.qet new file mode 100644 index 000000000..c762ea389 --- /dev/null +++ b/tests/qttest/fixtures/terminal_no_orientation.qet @@ -0,0 +1,315 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + skeleton + /tmp/qet-mcp-new-k41dmnyo/skeleton.qet + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + label + + + + %{label} + + + + + + + + + + Elements importats + Zavedené prvky + Importerede elementer + Importierte Elemente + Εισηγμένα στοιχεία + Imported elements + Elementos importados + Éléments importés + Uvezeni elementi + Elementi importati + インバートされた要素 + 전기 심볼 요소 + 전기 심볼 요소 + Elementen geïmporteerd + Elementen geïmporteerd + Elementy importowane + Elementos importados + Elementos importados + Elemente importate + Импортированные элементы + Uvoženi elementi + Importerade element + İthal öğeler + Імпортовані елементи + 导入元件 + + + + كهربائي + Elèctric + Elektrotechnika + Elektrisk + Elektrik + Ηλεκτρικά + Electric + Eléctrica + Sähkö + Electrique + Električno + Elektromos + Elettrica + 電気 + 전기 + Цахилгаан + Elektrisk + Elektrotechniek + Elektrisch + Elektrotechnika + Elétrico + Elétrica + Electric + Электротехника + Elektrické + Elektrika + Електрика + El + Elektrik + Електрика + 10_电气 + 电气工程 + + + + متعدد الأقطاب + Multifilar + Vícežilový + Flere ledere + Allpolig + Πολυγραμμικό + All-pole + Multifilar + Yleisnapa + Multifilaire + Univerzalni + Többvonalas szimbólumok + Multifilare + 複線 + 다-극 + Бүх туйл + Alle poler + Veel polig + All-pole + Schematy wieloliniowe + Multipolar + Multifilar + Bipolar + Многополюсные + Všepólové + Vsi poli + Свe-полни + Allpolig + Çok kutuplu + Багатополюсні + 10_全极 + + + + المرحّلات والموصّلات وجهات الاتصال + Relés, contactors i contactes + Relé, stykače a kontakty + Relæer, kontaktorer og kontakter + Relais, Schütze und Kontakte + Ηλεκτρονόμοι και επαφές + Relays, contactors and contacts + Relevadores, contactores y contactos + Releet, kontaktorit ja koskettimet + Relais, contacteurs et contacts + Releji, kontaktori i kontakti + Relék, kontaktorok és érintkezők + Relè, contattori e contatti + 継電器、接触器、接点 + 릴레이, 커넥터및 접점 + Реле, контактор болон контакт + Reléer, kontaktorere og kontakter + Relais, contactoren en contacten + Relais, contacteurs en contacten + Przekaźniki i styczniki + Relés, contatores e contactos + Relés, Contatores e contatos + Relee, contactoare și contacte + Реле, пускатели и контакты + Relé, stykače a kontakty + Releji, kontaktorji in kontakti + Релеји, контактори и контакти + Reläer, kontaktorer och kontakter + Röleler, kontaktörler ve kontaklar + Реле, контактори та контакти + 继电器、接触器与触点 + + + + تماسات مع إحالات + Contactes amb referències creuades + Kontakty s křížovými odkazy + Kontaker med krydsreference + Kontakte mit Querverweis + Επαφές με παραπομπές + Contacts with cross referencing + Contactos con referencia cruzada + Kosketimet lehtiviittauksella + Contacts avec référence croisée + Kontakti s unakrsnim referenciranjem + Érintkezők kereszthivatkozásokkal + Contatti con riferimenti incrociati + 相互参照付き接点 + 교차 참조 접점 + Харилцан холбоос бүхий контакт + Kontakter med kryssreferanse + Contacten met kruisreferentie + Contacten met kruisverwijzing + Zestyki (automatyczne oznaczenia) + Contactos com renvoi de folio + Contatos com referencia cruzada + Contacte cu renvoi de folio + Контакты с перекрестными ссылками + Kontakty s krížovým odkazom + Kontakti s sklici + Контакти са унакрсним референцирањем + Kontakter med bladhänvisning + Çapraz referanslı kontaklar + Контакти з перехресними посиланнями + 触点交叉引用 + + + + حمايات + Proteccions + Ochrany + Beskyttelsesfunktioner + Schutzfunktion + Προστασία + Protections + Protecciones + Suojaukset + Protections + Zaštite + Védelmek + Protezioni + 保護 + 보호 접점 + Хамгаалалт + Beskyttelser + Beveiliging + Beveiligingen + Zabezpieczenia + Protecções + Protecções + Protecții + Защитные уст-ва + Ochrany + Zaščite + Заштите + Skydd + Korumalar + Захисти + 保护触点 + + + + + + مُلامس مُرحّل حراري (NC) + Relé tèrmic de contacte (NC) + Kontakt (NC) tepelného jističe + Kontakt termisk relæ (NC) + Relaisöffner + Επαφή θερμικού κλειστή + Contact thermal relay (NC) + Contacto relé térmico NC + Kosketin lämpörele (NC) + Contact relais thermique (NF) + Kontakt termičkog releja (NC) + Hőrelé érintkező (NC) + Contatto relé térmico (NC) + 接点 熱動継電器 (NC) + 접점 열 릴레이 (NC) + Дулааны релений контакт (NC) + Kontakt termisk relé (NC) + Thermisch bediend contact (NC) + Contact thermisch relais (NC) + Zestyk rozwierny przekaźnika cieplnego + Contacto relé térmico (NC) + Contato de relé térmico (NF) + Contact releu termic (NC) + Контакт теплового реле (НЗ) + Kontakt tepelného relé (NC) + Kontakt termičnega releja (NC) + Контакт термичког релеја (NC) + Kontakt termiskt relä (NC) + Termik röle kontağı (NC) + Контакт теплового реле (NC) + 继电器常闭从属触点 + + + simple + NC + 1 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + + label + + + + + + + + + + + + + + + + diff --git a/tests/qttest/tst_terminalnoorientation.cpp b/tests/qttest/tst_terminalnoorientation.cpp new file mode 100644 index 000000000..e88bf2dc7 --- /dev/null +++ b/tests/qttest/tst_terminalnoorientation.cpp @@ -0,0 +1,89 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include +#include + +/** + A terminal in a symbol's definition with no orientation attribute + (hand-edited or generated symbols, a damaged file) used to crash + QElectroTech on opening the project: Qet::orientationFromString() read + the first character of an empty string. It now faces north, as any + other unknown orientation does. Runs the real binary's --resave on + fixtures/terminal_no_orientation.qet: unlinked_contact_label.qet with + the orientation of its contact's top terminal removed. +*/ +class tst_terminalnoorientation : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + +private slots: + void initTestCase() + { + QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found"); + QVERIFY(m_dir.isValid()); + } + + void opensAndSavesFacingNorth() + { + const QString fixture = QFINDTESTDATA("fixtures/terminal_no_orientation.qet"); + QVERIFY2(!fixture.isEmpty(), "fixture project not found"); + const QString out = m_dir.filePath(QStringLiteral("resaved.qet")); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), fixture, out}); + QVERIFY(proc.waitForFinished(60000)); + QCOMPARE(proc.exitStatus(), QProcess::NormalExit); + QCOMPARE(proc.exitCode(), 0); + + QFile file(out); + QVERIFY(file.open(QIODevice::ReadOnly)); + QDomDocument doc; + QVERIFY(doc.setContent(&file)); + + //The placed contact keeps both terminals; the one whose + //definition has no orientation faces north (0) + const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element")); + QDomElement contact; + for (int i = 0; i < elements.size(); ++i) { + const QDomElement e = elements.at(i).toElement(); + if (e.attribute(QStringLiteral("type")).endsWith(QStringLiteral("contact_relais_nf_esclave.elmt")) + && e.parentNode().nodeName() == QLatin1String("elements")) { + contact = e; + break; + } + } + QVERIFY2(!contact.isNull(), "the contact is not in the saved project"); + const QDomNodeList terminals = contact.elementsByTagName(QStringLiteral("terminal")); + QCOMPARE(terminals.size(), 2); + QStringList orientations; + for (int i = 0; i < terminals.size(); ++i) + orientations << terminals.at(i).toElement().attribute(QStringLiteral("orientation")); + orientations.sort(); + QCOMPARE(orientations, (QStringList{QStringLiteral("0"), QStringLiteral("2")})); + } +}; + +QTEST_GUILESS_MAIN(tst_terminalnoorientation) +#include "tst_terminalnoorientation.moc"