From c72f4e28d059769fe3aa062b15ae7e58776b90fd Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:22:14 -0400 Subject: [PATCH 01/21] Drop malformed OSC packets instead of unwinding to the event loop oscpack throws osc::Exception on any malformed input. The parse in OscReceiver::byteArrayToVariantList was unguarded, so a bad datagram unwound through the Qt event loop up to MainApplication::notify(), which aborted the entire readyRead batch and logged unhelpfully. Wrap the parse in a local try/catch: a malformed datagram is now dropped with a clear qWarning, the receive loop keeps draining pending datagrams, and a message is forwarded only when it parsed cleanly. --- src/control/qosc/oscreceiver.cpp | 72 +++++++++++++++++++------------- src/control/qosc/oscreceiver.h | 2 +- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/control/qosc/oscreceiver.cpp b/src/control/qosc/oscreceiver.cpp index 6f324944b..3be584c28 100644 --- a/src/control/qosc/oscreceiver.cpp +++ b/src/control/qosc/oscreceiver.cpp @@ -1,6 +1,7 @@ #include "oscreceiver.h" #include "contrib/oscpack/OscTypes.h" #include "contrib/oscpack/OscReceivedElements.h" +#include "contrib/oscpack/OscException.h" OscReceiver::OscReceiver(quint16 receivePort, QObject* parent) : QObject(parent) @@ -19,39 +20,50 @@ void OscReceiver::readyReadCb() { QByteArray data = datagram.data(); QVariantList arguments; QString oscAddress; - this->byteArrayToVariantList(arguments, oscAddress, data); - emit messageReceived(oscAddress, arguments); - qDebug() << "C++OscReceiver Received: " << oscAddress << arguments; + // A malformed datagram must never take the app down: only forward the + // message when it parsed cleanly, and keep draining the queue otherwise. + if (this->byteArrayToVariantList(arguments, oscAddress, data)) { + emit messageReceived(oscAddress, arguments); + qDebug() << "C++OscReceiver Received: " << oscAddress << arguments; + } } } -void OscReceiver::byteArrayToVariantList(QVariantList& outputVariantList, QString& outputOscAddress, const QByteArray& inputByteArray) { - osc::ReceivedPacket packet(inputByteArray.data(), static_cast(inputByteArray.size())); - // TODO: catch parsing exceptions - if (packet.IsMessage()) { - osc::ReceivedMessage message(packet); - // Get address pattern - QString address(message.AddressPattern()); - outputOscAddress.replace(0, address.size(), address); +bool OscReceiver::byteArrayToVariantList(QVariantList& outputVariantList, QString& outputOscAddress, const QByteArray& inputByteArray) { + // oscpack throws osc::Exception (derived from std::exception) on any + // malformed input. Catch it here so a single bad datagram is dropped + // rather than unwinding through the Qt event loop. + try { + osc::ReceivedPacket packet(inputByteArray.data(), static_cast(inputByteArray.size())); + if (packet.IsMessage()) { + osc::ReceivedMessage message(packet); + // Get address pattern + QString address(message.AddressPattern()); + outputOscAddress.replace(0, address.size(), address); - for (auto iter = message.ArgumentsBegin(); iter != message.ArgumentsEnd(); ++ iter) { - osc::ReceivedMessageArgument argument = (*iter); - if (argument.IsBool()) { - outputVariantList.append(QVariant(argument.AsBool())); - } else if (argument.IsString()) { - outputVariantList.append(QVariant(argument.AsString())); - } else if (argument.IsInt32()) { - outputVariantList.append(QVariant::fromValue(static_cast(argument.AsInt32()))); - } else if (argument.IsFloat()) { - outputVariantList.append(QVariant(argument.AsFloat())); - } else if (argument.IsChar()) { - outputVariantList.append(QVariant(argument.AsChar())); - //} else if (argument.IsInt64()) { - // outputVariantList.append(QVariant(argument.AsInt64())); - } else if (argument.IsDouble()) { - outputVariantList.append(QVariant(argument.AsDouble())); + for (auto iter = message.ArgumentsBegin(); iter != message.ArgumentsEnd(); ++ iter) { + osc::ReceivedMessageArgument argument = (*iter); + if (argument.IsBool()) { + outputVariantList.append(QVariant(argument.AsBool())); + } else if (argument.IsString()) { + outputVariantList.append(QVariant(argument.AsString())); + } else if (argument.IsInt32()) { + outputVariantList.append(QVariant::fromValue(static_cast(argument.AsInt32()))); + } else if (argument.IsFloat()) { + outputVariantList.append(QVariant(argument.AsFloat())); + } else if (argument.IsChar()) { + outputVariantList.append(QVariant(argument.AsChar())); + //} else if (argument.IsInt64()) { + // outputVariantList.append(QVariant(argument.AsInt64())); + } else if (argument.IsDouble()) { + outputVariantList.append(QVariant(argument.AsDouble())); + } + // TODO: support Array, Midi, Blob, Symbol, TimeTag, RGBA, Nil } - // TODO: support Array, Midi, Blob, Symbol, TimeTag, RGBA, Nil - } - } // TODO: also parse bundles + } // TODO: also parse bundles + } catch (const osc::Exception& e) { + qWarning() << "OscReceiver: dropped malformed OSC packet:" << e.what(); + return false; + } + return true; } diff --git a/src/control/qosc/oscreceiver.h b/src/control/qosc/oscreceiver.h index 06ea9ccd1..5e720af22 100644 --- a/src/control/qosc/oscreceiver.h +++ b/src/control/qosc/oscreceiver.h @@ -41,7 +41,7 @@ public slots: private: QUdpSocket* m_udpSocket; - void byteArrayToVariantList(QVariantList& outputVariantList, QString& outputOscAddress, const QByteArray& inputByteArray); + bool byteArrayToVariantList(QVariantList& outputVariantList, QString& outputOscAddress, const QByteArray& inputByteArray); }; #endif // OSCRECEIVER_H From 9db43f379fa255a4c765638342a1a5139d19899c Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:22:46 -0400 Subject: [PATCH 02/21] Harden the top-level exception guard in MainApplication::notify The event-loop guard already caught std::exception, but logged via qDebug (stripped from release builds) and let non-standard throws escape. Log at qCritical so incidents are visible in shipped builds, add a catch(...) so any exception type is contained, and null-guard the event pointer when reporting the type. --- src/app/MainApplication.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/MainApplication.cpp b/src/app/MainApplication.cpp index 86efb27b7..7647e4049 100644 --- a/src/app/MainApplication.cpp +++ b/src/app/MainApplication.cpp @@ -44,14 +44,23 @@ MainApplication::~MainApplication() bool MainApplication::notify(QObject *receiver, QEvent *event) { + // Last-resort guard: a stray exception during event delivery must never take + // down a running show. Log loudly (qCritical survives in release builds, + // unlike qDebug) and drop the offending event instead of terminating. + const int eventType = event ? static_cast(event->type()) : -1; try { return QApplication::notify(receiver, event); } - catch (std::exception &ex) + catch (const std::exception &ex) { - qDebug() << "std::exception was caught: " << ex.what() << Qt::endl; - qDebug() << "event type: " << event->type() << Qt::endl; + qCritical() << "Unhandled std::exception during event delivery:" << ex.what() + << "(event type" << eventType << ")"; + } + catch (...) + { + qCritical() << "Unhandled non-standard exception during event delivery" + << "(event type" << eventType << ")"; } return false; From 5038f0517693915cdd00c805f18e984024c05b1e Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:27:53 -0400 Subject: [PATCH 03/21] Bind OSC to loopback by default; make network access opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MapMap started its OSC receiver on all interfaces (QHostAddress::Any) at launch, so any device on the venue network could drive mappings or quit the show — OSC has no authentication. Bind to 127.0.0.1 by default and thread an acceptFromNetwork flag through OscInterface into OscReceiver, which widens the bind to all interfaces only when set. Expose it as an "Accept OSC from the network (less secure)" checkbox in Preferences > OSC Setup, off by default. The value is persisted and applied live: PreferenceDialog sets it before setOscPort() rebinds the receiver. Also check the bind() result and log the address. --- src/control/OscInterface.cpp | 4 ++-- src/control/OscInterface.h | 2 +- src/control/qosc/oscreceiver.cpp | 16 ++++++++++++---- src/control/qosc/oscreceiver.h | 2 +- src/gui/MainWindow.cpp | 4 +++- src/gui/MainWindow.h | 6 ++++++ src/gui/PreferenceDialog.cpp | 11 +++++++++++ src/gui/PreferenceDialog.h | 1 + 8 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/control/OscInterface.cpp b/src/control/OscInterface.cpp index 1e2711864..4ba0fe3aa 100644 --- a/src/control/OscInterface.cpp +++ b/src/control/OscInterface.cpp @@ -75,8 +75,8 @@ QVector resolveLayers(MappingManager& manager, const OscAction& acti } // namespace OscInterface::OscInterface( - int listen_port) : - receiver_(listen_port), + int listen_port, bool acceptFromNetwork) : + receiver_(listen_port, acceptFromNetwork), messaging_queue_() { receiving_enabled_ = true; if (receiving_enabled_) { diff --git a/src/control/OscInterface.h b/src/control/OscInterface.h index 93ffa8bc6..1558fc7b2 100644 --- a/src/control/OscInterface.h +++ b/src/control/OscInterface.h @@ -42,7 +42,7 @@ class OscInterface { public: typedef QSharedPointer ptr; - OscInterface(int listen_port); + OscInterface(int listen_port, bool acceptFromNetwork = false); ~OscInterface(); void setVerbose(bool verbose) { verbose_ = verbose; } diff --git a/src/control/qosc/oscreceiver.cpp b/src/control/qosc/oscreceiver.cpp index 3be584c28..bbef7f900 100644 --- a/src/control/qosc/oscreceiver.cpp +++ b/src/control/qosc/oscreceiver.cpp @@ -3,13 +3,21 @@ #include "contrib/oscpack/OscReceivedElements.h" #include "contrib/oscpack/OscException.h" -OscReceiver::OscReceiver(quint16 receivePort, QObject* parent) : +OscReceiver::OscReceiver(quint16 receivePort, bool acceptFromNetwork, QObject* parent) : QObject(parent) { m_udpSocket = new QUdpSocket(this); - // m_udpSocket->bind(QHostAddress::LocalHost, receivePort); - qDebug() << "Listening for OSC on " << receivePort; - m_udpSocket->bind(QHostAddress::Any, receivePort); + // OSC has no authentication, so bind to loopback by default: the control + // surface must not be reachable from the venue network unless the user + // explicitly opts in (Preferences > OSC Setup). + const QHostAddress bindAddress = acceptFromNetwork ? QHostAddress(QHostAddress::Any) + : QHostAddress(QHostAddress::LocalHost); + if (!m_udpSocket->bind(bindAddress, receivePort)) { + qWarning() << "OscReceiver: could not bind OSC port" << receivePort + << "on" << bindAddress.toString(); + } else { + qInfo() << "Listening for OSC on" << bindAddress.toString() << "port" << receivePort; + } connect(m_udpSocket, &QUdpSocket::readyRead, this, &OscReceiver::readyReadCb); } diff --git a/src/control/qosc/oscreceiver.h b/src/control/qosc/oscreceiver.h index 5e720af22..58bc2b8cd 100644 --- a/src/control/qosc/oscreceiver.h +++ b/src/control/qosc/oscreceiver.h @@ -26,7 +26,7 @@ class QOSC_EXPORT OscReceiver : public QObject * @brief Constructor. * @param receivePort Port number to listen to. */ - explicit OscReceiver(quint16 receivePort, QObject *parent = nullptr); + explicit OscReceiver(quint16 receivePort, bool acceptFromNetwork = false, QObject *parent = nullptr); signals: /** diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c1414c623..251f97d8b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -2713,6 +2713,7 @@ void MainWindow::readSettings() displayControlsAction->setChecked(settings.value("displayControls", MM::DISPLAY_CONTROLS).toBool()); outputWindow->setCanvasDisplayCrosshair(settings.value("displayControls", MM::DISPLAY_CONTROLS).toBool()); oscListeningPort = settings.value("oscListeningPort", MM::DEFAULT_OSC_PORT).toInt(); + oscAcceptNetwork = settings.value("oscAcceptNetwork", false).toBool(); #ifdef HAVE_MCP mcpListeningPort = settings.value("mcpListeningPort", MM::DEFAULT_MCP_PORT).toInt(); #endif @@ -2749,6 +2750,7 @@ void MainWindow::writeSettings() settings.setValue("displayControls", displayControlsAction->isChecked()); settings.setValue("displayAllControls", displaySourceControlsAction->isChecked()); settings.setValue("oscListeningPort", oscListeningPort); + settings.setValue("oscAcceptNetwork", oscAcceptNetwork); #ifdef HAVE_MCP settings.setValue("mcpListeningPort", mcpListeningPort); #endif @@ -3918,7 +3920,7 @@ void MainWindow::startOscReceiver() #else QMessageLogger(__FILE__, __LINE__, 0).debug() << "OSC port: " << oscListeningPort; #endif - osc_interface.reset(new OscInterface(oscListeningPort)); + osc_interface.reset(new OscInterface(oscListeningPort, oscAcceptNetwork)); if (oscListeningPort != 0) { osc_interface->start(); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 2a2a639b2..46f3a1cd6 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -543,6 +543,9 @@ public slots: // OSC. OscInterface::ptr osc_interface; int oscListeningPort; + // When false (default), OSC binds to loopback only; true widens it to all + // network interfaces. OSC is unauthenticated, so network access is opt-in. + bool oscAcceptNetwork; QTimer *osc_timer; #ifdef HAVE_MCP @@ -655,6 +658,9 @@ public slots: void startFullScreen(); bool setOscPort(QString portNumber); bool setOscPort(int portNumber); + // Stores whether OSC accepts datagrams from the network. Takes effect the + // next time startOscReceiver() runs (setOscPort triggers it on Apply). + void setOscAcceptNetwork(bool accept) { oscAcceptNetwork = accept; } int getOscPort() const; void setVerbose(bool verbose); #ifdef HAVE_MCP diff --git a/src/gui/PreferenceDialog.cpp b/src/gui/PreferenceDialog.cpp index ce330dfcf..bfff8fbf3 100644 --- a/src/gui/PreferenceDialog.cpp +++ b/src/gui/PreferenceDialog.cpp @@ -122,6 +122,8 @@ bool PreferenceDialog::loadSettings() // Allow OSC message with same media source _oscSameMediaSourceBox->setChecked(settings.value("oscSameMediaSource", MM::OSC_SAME_MEDIA_SOURCE).toBool()); + // Accept OSC from the network (off by default: OSC is unauthenticated) + _oscAcceptNetworkBox->setChecked(settings.value("oscAcceptNetwork", false).toBool()); #ifdef HAVE_MCP // MCP port _mcpPortNumber->setValue(settings.value("mcpListeningPort", MM::DEFAULT_MCP_PORT).toInt()); @@ -136,6 +138,9 @@ void PreferenceDialog::applySettings() { QSettings settings; MainWindow *mainWindow = MainWindow::window(); + // OSC network exposure. Set before setOscPort(), which rebinds the receiver. + settings.setValue("oscAcceptNetwork", _oscAcceptNetworkBox->isChecked()); + mainWindow->setOscAcceptNetwork(_oscAcceptNetworkBox->isChecked()); // Listen port settings.setValue("oscListeningPort", _listenPortNumber->value()); mainWindow->setOscPort(settings.value("oscListeningPort").toInt()); @@ -353,6 +358,11 @@ void PreferenceDialog::createControlsPage() _oscSameMediaSourceBox = new QCheckBox(tr("Allow message with existing media source")); _oscSameMediaSourceBox->setChecked(false); + _oscAcceptNetworkBox = new QCheckBox(tr("Accept OSC from the network (less secure)")); + _oscAcceptNetworkBox->setChecked(false); + _oscAcceptNetworkBox->setToolTip(tr("When off, OSC is only reachable from this computer (127.0.0.1). " + "Enable only on a trusted show network — OSC has no authentication.")); + QFormLayout *listenPortForm = new QFormLayout; listenPortForm->setContentsMargins(margins); listenPortForm->addRow(tr("on port"), _listenPortNumber); @@ -376,6 +386,7 @@ void PreferenceDialog::createControlsPage() oscLayout->addWidget(_listenMessageBox, 1); oscLayout->addLayout(listenPortForm, 1); oscLayout->addWidget(_oscSameMediaSourceBox, 1); + oscLayout->addWidget(_oscAcceptNetworkBox, 1); oscLayout->addLayout(listenAddressForm, 3); _oscWidget->setLayout(oscLayout); diff --git a/src/gui/PreferenceDialog.h b/src/gui/PreferenceDialog.h index 99641fe22..a6cfca980 100644 --- a/src/gui/PreferenceDialog.h +++ b/src/gui/PreferenceDialog.h @@ -109,6 +109,7 @@ private slots: QSpinBox *_listenPortNumber; QPushButton *_ipRefreshButton; QCheckBox *_oscSameMediaSourceBox; + QCheckBox *_oscAcceptNetworkBox; #ifdef HAVE_MCP // MCP QWidget *_mcpWidget; From 5e18db7de54cccb315b799fabf9d4b749f387a6a Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:29:58 -0400 Subject: [PATCH 04/21] Ignore the OSC quit command unless explicitly allowed /mapmap/quit closed the application with no guard, so any OSC sender could end a show. Gate it behind a new "Allow OSC to quit the application" preference (off by default): applyAction now checks isOscQuitAllowed() and drops the quit with a warning otherwise. Wire the setting through MainWindow (persisted) and Preferences > OSC Setup, and document the loopback-by-default and quit-disabled behavior in docs/informations/osc.md. --- docs/informations/osc.md | 11 +++++++++++ src/control/OscInterface.cpp | 6 +++++- src/gui/MainWindow.cpp | 2 ++ src/gui/MainWindow.h | 6 ++++++ src/gui/PreferenceDialog.cpp | 11 +++++++++++ src/gui/PreferenceDialog.h | 1 + 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/informations/osc.md b/docs/informations/osc.md index 58c951e2d..504465f8d 100644 --- a/docs/informations/osc.md +++ b/docs/informations/osc.md @@ -57,3 +57,14 @@ Pause: `/mapmap/pause` Play: `/mapmap/play` Rewind/reset: `/mapmap/rewind` Quit: `/mapmap/quit` + +## Security + +OSC has no authentication. By default MapMap listens on the loopback +interface only (`127.0.0.1`), so OSC is reachable solely from the same +computer. To control MapMap from another device, enable **Accept OSC from +the network** in *Preferences > OSC Setup* — only on a trusted show network. + +The `/mapmap/quit` command is ignored unless **Allow OSC to quit the +application** is enabled in the same panel, so a remote sender cannot close +the app during a show. diff --git a/src/control/OscInterface.cpp b/src/control/OscInterface.cpp index 4ba0fe3aa..d4415dc78 100644 --- a/src/control/OscInterface.cpp +++ b/src/control/OscInterface.cpp @@ -219,7 +219,11 @@ bool OscInterface::applyAction(MainWindow &main_window, const OscAction& action) return false; // Global transport. - case OscAction::Quit: main_window.close(); return true; + case OscAction::Quit: + if (main_window.isOscQuitAllowed()) { main_window.close(); return true; } + qWarning() << "Ignoring OSC /mapmap/quit: remote quit is disabled " + "(enable it in Preferences > OSC Setup)."; + return false; case OscAction::PlayAll: main_window.play(); return true; case OscAction::PauseAll: main_window.pause(); return true; case OscAction::RewindAll: main_window.rewind(); return true; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 251f97d8b..324b94e24 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -2714,6 +2714,7 @@ void MainWindow::readSettings() outputWindow->setCanvasDisplayCrosshair(settings.value("displayControls", MM::DISPLAY_CONTROLS).toBool()); oscListeningPort = settings.value("oscListeningPort", MM::DEFAULT_OSC_PORT).toInt(); oscAcceptNetwork = settings.value("oscAcceptNetwork", false).toBool(); + oscAllowQuit = settings.value("oscAllowQuit", false).toBool(); #ifdef HAVE_MCP mcpListeningPort = settings.value("mcpListeningPort", MM::DEFAULT_MCP_PORT).toInt(); #endif @@ -2751,6 +2752,7 @@ void MainWindow::writeSettings() settings.setValue("displayAllControls", displaySourceControlsAction->isChecked()); settings.setValue("oscListeningPort", oscListeningPort); settings.setValue("oscAcceptNetwork", oscAcceptNetwork); + settings.setValue("oscAllowQuit", oscAllowQuit); #ifdef HAVE_MCP settings.setValue("mcpListeningPort", mcpListeningPort); #endif diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 46f3a1cd6..8af4a4557 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -546,6 +546,9 @@ public slots: // When false (default), OSC binds to loopback only; true widens it to all // network interfaces. OSC is unauthenticated, so network access is opt-in. bool oscAcceptNetwork; + // When false (default), the OSC /mapmap/quit command is ignored so a remote + // sender cannot close the application mid-show. + bool oscAllowQuit; QTimer *osc_timer; #ifdef HAVE_MCP @@ -661,6 +664,9 @@ public slots: // Stores whether OSC accepts datagrams from the network. Takes effect the // next time startOscReceiver() runs (setOscPort triggers it on Apply). void setOscAcceptNetwork(bool accept) { oscAcceptNetwork = accept; } + // Whether the OSC /mapmap/quit command may close the application. + bool isOscQuitAllowed() const { return oscAllowQuit; } + void setOscAllowQuit(bool allow) { oscAllowQuit = allow; } int getOscPort() const; void setVerbose(bool verbose); #ifdef HAVE_MCP diff --git a/src/gui/PreferenceDialog.cpp b/src/gui/PreferenceDialog.cpp index bfff8fbf3..fd07f527c 100644 --- a/src/gui/PreferenceDialog.cpp +++ b/src/gui/PreferenceDialog.cpp @@ -124,6 +124,8 @@ bool PreferenceDialog::loadSettings() _oscSameMediaSourceBox->setChecked(settings.value("oscSameMediaSource", MM::OSC_SAME_MEDIA_SOURCE).toBool()); // Accept OSC from the network (off by default: OSC is unauthenticated) _oscAcceptNetworkBox->setChecked(settings.value("oscAcceptNetwork", false).toBool()); + // Allow the OSC quit command (off by default) + _oscAllowQuitBox->setChecked(settings.value("oscAllowQuit", false).toBool()); #ifdef HAVE_MCP // MCP port _mcpPortNumber->setValue(settings.value("mcpListeningPort", MM::DEFAULT_MCP_PORT).toInt()); @@ -141,6 +143,9 @@ void PreferenceDialog::applySettings() // OSC network exposure. Set before setOscPort(), which rebinds the receiver. settings.setValue("oscAcceptNetwork", _oscAcceptNetworkBox->isChecked()); mainWindow->setOscAcceptNetwork(_oscAcceptNetworkBox->isChecked()); + // Whether OSC may quit the application. + settings.setValue("oscAllowQuit", _oscAllowQuitBox->isChecked()); + mainWindow->setOscAllowQuit(_oscAllowQuitBox->isChecked()); // Listen port settings.setValue("oscListeningPort", _listenPortNumber->value()); mainWindow->setOscPort(settings.value("oscListeningPort").toInt()); @@ -363,6 +368,11 @@ void PreferenceDialog::createControlsPage() _oscAcceptNetworkBox->setToolTip(tr("When off, OSC is only reachable from this computer (127.0.0.1). " "Enable only on a trusted show network — OSC has no authentication.")); + _oscAllowQuitBox = new QCheckBox(tr("Allow OSC to quit the application")); + _oscAllowQuitBox->setChecked(false); + _oscAllowQuitBox->setToolTip(tr("When off, the /mapmap/quit OSC command is ignored so a remote " + "sender cannot close the application during a show.")); + QFormLayout *listenPortForm = new QFormLayout; listenPortForm->setContentsMargins(margins); listenPortForm->addRow(tr("on port"), _listenPortNumber); @@ -387,6 +397,7 @@ void PreferenceDialog::createControlsPage() oscLayout->addLayout(listenPortForm, 1); oscLayout->addWidget(_oscSameMediaSourceBox, 1); oscLayout->addWidget(_oscAcceptNetworkBox, 1); + oscLayout->addWidget(_oscAllowQuitBox, 1); oscLayout->addLayout(listenAddressForm, 3); _oscWidget->setLayout(oscLayout); diff --git a/src/gui/PreferenceDialog.h b/src/gui/PreferenceDialog.h index a6cfca980..36f6fbd17 100644 --- a/src/gui/PreferenceDialog.h +++ b/src/gui/PreferenceDialog.h @@ -110,6 +110,7 @@ private slots: QPushButton *_ipRefreshButton; QCheckBox *_oscSameMediaSourceBox; QCheckBox *_oscAcceptNetworkBox; + QCheckBox *_oscAllowQuitBox; #ifdef HAVE_MCP // MCP QWidget *_mcpWidget; From 6c06460ca38f1a49389d6d8337485e6bcf237a44 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:31:49 -0400 Subject: [PATCH 05/21] Gate per-message OSC receive logging behind verbose mode OscReceiver logged every incoming datagram unconditionally, spamming the log and costing CPU under high OSC rates (e.g. per-frame vertex control). Add a verbose flag to OscReceiver, propagated from OscInterface::setVerbose (driven by the --verbose flag), and only log received messages when it is set. The one-time bind message stays. --- src/control/OscInterface.h | 2 +- src/control/qosc/oscreceiver.cpp | 3 ++- src/control/qosc/oscreceiver.h | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/control/OscInterface.h b/src/control/OscInterface.h index 1558fc7b2..da799d6fd 100644 --- a/src/control/OscInterface.h +++ b/src/control/OscInterface.h @@ -45,7 +45,7 @@ class OscInterface { OscInterface(int listen_port, bool acceptFromNetwork = false); ~OscInterface(); - void setVerbose(bool verbose) { verbose_ = verbose; } + void setVerbose(bool verbose) { verbose_ = verbose; receiver_.setVerbose(verbose); } /// Starts listening if receiving is enabled. void start(); diff --git a/src/control/qosc/oscreceiver.cpp b/src/control/qosc/oscreceiver.cpp index bbef7f900..06ed493cd 100644 --- a/src/control/qosc/oscreceiver.cpp +++ b/src/control/qosc/oscreceiver.cpp @@ -32,7 +32,8 @@ void OscReceiver::readyReadCb() { // message when it parsed cleanly, and keep draining the queue otherwise. if (this->byteArrayToVariantList(arguments, oscAddress, data)) { emit messageReceived(oscAddress, arguments); - qDebug() << "C++OscReceiver Received: " << oscAddress << arguments; + if (m_verbose) + qDebug() << "OscReceiver received:" << oscAddress << arguments; } } } diff --git a/src/control/qosc/oscreceiver.h b/src/control/qosc/oscreceiver.h index 58bc2b8cd..15ef2ea37 100644 --- a/src/control/qosc/oscreceiver.h +++ b/src/control/qosc/oscreceiver.h @@ -28,6 +28,10 @@ class QOSC_EXPORT OscReceiver : public QObject */ explicit OscReceiver(quint16 receivePort, bool acceptFromNetwork = false, QObject *parent = nullptr); + /// When false (default), per-message receive logging is suppressed to keep + /// the hot path quiet under high OSC rates. + void setVerbose(bool verbose) { m_verbose = verbose; } + signals: /** * @brief Signal triggered each time we receive a message. @@ -41,6 +45,7 @@ public slots: private: QUdpSocket* m_udpSocket; + bool m_verbose = false; bool byteArrayToVariantList(QVariantList& outputVariantList, QString& outputOscAddress, const QByteArray& inputByteArray); }; From b759cefb7737fe8785232d7fe93bb5e23c928ad4 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:48:28 -0400 Subject: [PATCH 06/21] Bump version to 1.0.0-alpha.1 from a single source of truth MM::VERSION was hardcoded ("0.6.3") and duplicated the qmake VERSION. Define the human-facing version once (MAPMAP_VERSION in mapmap.pro), inject it via DEFINES, and have MM::VERSION read it (with a matching literal fallback for direct compiles). qmake VERSION stays numeric (1.0.0) so bundle and library versioning remain valid. Relax SUPPORTED_FILE_VERSIONS to accept an optional -prerelease/+build suffix and anchor it, so projects saved by 1.0.0-alpha.1 still load and malformed version strings are rejected explicitly. --- mapmap.pro | 9 +++++++-- src/core/MM.cpp | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/mapmap.pro b/mapmap.pro index 4444fe50e..b5f2b039c 100644 --- a/mapmap.pro +++ b/mapmap.pro @@ -4,11 +4,16 @@ CONFIG += c++17 TEMPLATE = app -# Always use major.minor.micro version number format -VERSION = 0.6.3 +# Always use major.minor.micro version number format (kept numeric so bundle +# and library versioning stay valid). +VERSION = 1.0.0 +# Human-facing version shown in-app (About box, window title, app version). +# Single source of truth for MM::VERSION, injected via DEFINES below. +MAPMAP_VERSION = 1.0.0-alpha.1 TARGET = mapmap DEFINES += UNICODE QT_THREAD_SUPPORT QT_CORE_LIB QT_GUI_LIB QT_MESSAGELOGCONTEXT +DEFINES += MAPMAP_VERSION_STRING=\\\"$$MAPMAP_VERSION\\\" include(src/core/core.pri) include(src/shape/shape.pri) diff --git a/src/core/MM.cpp b/src/core/MM.cpp index e5b3321f0..ff57bc392 100644 --- a/src/core/MM.cpp +++ b/src/core/MM.cpp @@ -23,7 +23,13 @@ namespace mmp { const QString MM::APPLICATION_NAME = "MapMap"; -const QString MM::VERSION = "0.6.3"; +// Single source of truth is MAPMAP_VERSION in mapmap.pro, injected via DEFINES. +// The literal fallback keeps direct compiles working and must match the .pro. +#ifdef MAPMAP_VERSION_STRING +const QString MM::VERSION = MAPMAP_VERSION_STRING; +#else +const QString MM::VERSION = "1.0.0-alpha.1"; +#endif const QString MM::COPYRIGHT_OWNERS = "Alexandre Quessy, Sofian Audry, Dame Diongue, Mike Latona, Vasilis Liaskovitis"; const QString MM::ORGANIZATION_NAME = "MapMap"; const QString MM::ORGANIZATION_DOMAIN = "artpluscode.com"; @@ -42,7 +48,9 @@ const QString MM::VIDEO_FILES_FILTER = "*.mov *.mp4 *.avi *.ogg *.ogv *.mpeg *.m const QString MM::IMAGE_FILES_FILTER = "*.jpg *.jpeg *.gif *.png *.tiff *.tif *.bmp"; const QString MM::NAMESPACE_PREFIX = QString("%1::").arg(TOSTRING(MM_NAMESPACE)); const QString MM::SUPPORTED_LANGUAGES = "en, es, fr, zh_CN, zh_TW"; -const QString MM::SUPPORTED_FILE_VERSIONS = "\\d+\\.\\d+\\.\\d+"; // regex +// x.y.z with an optional -prerelease / +build suffix (e.g. 1.0.0-alpha.1), so +// projects saved by pre-release builds remain loadable. Anchored to reject junk. +const QString MM::SUPPORTED_FILE_VERSIONS = "^\\d+\\.\\d+\\.\\d+([-+][0-9A-Za-z.-]+)?$"; // regex const QColor MM::WHITE("#f6f5f5"); const QColor MM::BLUE_GRAY("#323541"); From ac2a0bdff2c6cf7e0034096a2087300458c5c104 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 00:52:51 -0400 Subject: [PATCH 07/21] Add file-version compatibility tests (QUA-4 seed) Guard the project-file version contract ProjectReader uses to accept or reject a .mmp: MM::SUPPORTED_FILE_VERSIONS must keep matching x.y.z and the pre-release/build suffixes that 1.0.0-alpha.1 writes, while rejecting malformed strings. The unit-test harness deliberately excludes the GUI/multimedia layer, so MappingManager and a full ProjectReader/Writer round-trip cannot link here without a larger harness change; those remain open under QUA-4 in AUDIT.md. --- tests/TestFileVersion.cpp | 51 +++++++++++++++++++++++++++++++++++++++ tests/TestFileVersion.h | 30 +++++++++++++++++++++++ tests/main.cpp | 5 ++++ tests/tests.pro | 6 +++-- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/TestFileVersion.cpp create mode 100644 tests/TestFileVersion.h diff --git a/tests/TestFileVersion.cpp b/tests/TestFileVersion.cpp new file mode 100644 index 000000000..29fe339c4 --- /dev/null +++ b/tests/TestFileVersion.cpp @@ -0,0 +1,51 @@ +/* + * TestFileVersion.cpp + * + * This program 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 3 of the License, or + * (at your option) any later version. + */ + +#include "TestFileVersion.h" + +#include + +#include "MM.h" + +using namespace mmp; + +namespace { +// Mirrors ProjectReader::isValidVersion(): a file version is accepted iff it +// matches MM::SUPPORTED_FILE_VERSIONS. Kept in sync with that method by design. +bool isValidVersion(const QString& version) +{ + return QRegularExpression(MM::SUPPORTED_FILE_VERSIONS).match(version).hasMatch(); +} +} + +void TestFileVersion::acceptsReleaseVersions() +{ + QVERIFY(isValidVersion("0.6.3")); // previous stable format + QVERIFY(isValidVersion("1.0.0")); + QVERIFY(isValidVersion("2.10.15")); +} + +void TestFileVersion::acceptsPreReleaseAndBuildSuffixes() +{ + // The 1.0 series ships pre-release builds; projects they save must reload. + QVERIFY(isValidVersion("1.0.0-alpha.1")); + QVERIFY(isValidVersion("1.0.0-rc.2")); + QVERIFY(isValidVersion("1.0.0+build.5")); +} + +void TestFileVersion::rejectsMalformedVersions() +{ + QVERIFY(!isValidVersion("")); + QVERIFY(!isValidVersion("abc")); + QVERIFY(!isValidVersion("1.0")); // too few components + QVERIFY(!isValidVersion("1.0.0.0")); // too many components + QVERIFY(!isValidVersion("1.0.0-")); // empty suffix + QVERIFY(!isValidVersion("v1.0.0")); // stray prefix + QVERIFY(!isValidVersion(" 1.0.0")); // leading whitespace +} diff --git a/tests/TestFileVersion.h b/tests/TestFileVersion.h new file mode 100644 index 000000000..51a8348ac --- /dev/null +++ b/tests/TestFileVersion.h @@ -0,0 +1,30 @@ +/* + * TestFileVersion.h + * + * Unit tests for the project-file version compatibility contract + * (MM::SUPPORTED_FILE_VERSIONS), which ProjectReader uses to decide whether a + * .mmp file is loadable. Regression guard for the 1.0.0-alpha.1 version bump: + * a saved pre-release project must remain loadable. + * + * This program 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 3 of the License, or + * (at your option) any later version. + */ + +#ifndef TEST_FILE_VERSION_H_ +#define TEST_FILE_VERSION_H_ + +#include + +class TestFileVersion : public QObject +{ + Q_OBJECT + +private slots: + void acceptsReleaseVersions(); + void acceptsPreReleaseAndBuildSuffixes(); + void rejectsMalformedVersions(); +}; + +#endif /* TEST_FILE_VERSION_H_ */ diff --git a/tests/main.cpp b/tests/main.cpp index be7939613..e36ea902c 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -18,6 +18,7 @@ #include "TestUidAllocator.h" #include "TestShape.h" #include "TestOsc.h" +#include "TestFileVersion.h" int main(int argc, char** argv) { @@ -44,6 +45,10 @@ int main(int argc, char** argv) TestOsc test; status |= QTest::qExec(&test, argc, argv); } + { + TestFileVersion test; + status |= QTest::qExec(&test, argc, argv); + } return status; } diff --git a/tests/tests.pro b/tests/tests.pro index d7b00a44e..ffe692ab0 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -40,7 +40,8 @@ HEADERS += \ TestUtil.h \ TestUidAllocator.h \ TestShape.h \ - TestOsc.h + TestOsc.h \ + TestFileVersion.h SOURCES += \ $$CORE/MM.cpp \ @@ -58,4 +59,5 @@ SOURCES += \ TestUtil.cpp \ TestUidAllocator.cpp \ TestShape.cpp \ - TestOsc.cpp + TestOsc.cpp \ + TestFileVersion.cpp From c28a4f28b34664e71279b9c33b397d893215048e Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 01:24:37 -0400 Subject: [PATCH 08/21] Check file-open results instead of discarding them Seven call sites opened a QFile with the result cast to (void), so a missing or locked file silently yielded empty content. Check each open() and qWarning() with the file name on failure. ConsoleWindow::writeLogFile is the exception: it is reached from the installed message handler, so it returns quietly on failure rather than warning (which would recurse). --- src/app/main.cpp | 3 ++- src/gui/AboutDialog.cpp | 15 ++++++++++----- src/gui/ConsoleWindow.cpp | 5 ++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 23bb4c6a5..83211378f 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -195,7 +195,8 @@ int main(int argc, char *argv[]) // Load stylesheet. QFile stylesheet(":/stylesheet"); - (void)stylesheet.open(QFile::ReadOnly); + if (!stylesheet.open(QFile::ReadOnly)) + qWarning() << "Could not open embedded stylesheet" << stylesheet.fileName(); app.setStyleSheet(QLatin1String(stylesheet.readAll())); // read positional argument: diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index d13f8ad0c..84d78fe28 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -95,11 +95,13 @@ void AboutDialog::createAboutTab() QString copyrightText = "

" + tr("Copyright © 2013 %1.").arg(MM::COPYRIGHT_OWNERS) + "

"; // License short notice QFile licenseShortFile(":/license-short"); - (void)licenseShortFile.open(QIODevice::ReadOnly | QIODevice::Text); + if (!licenseShortFile.open(QIODevice::ReadOnly | QIODevice::Text)) + qWarning() << "AboutDialog: could not open" << licenseShortFile.fileName(); QString licenseNoticeText = Qt::convertFromPlainText(QString::fromUtf8(licenseShortFile.readAll()), Qt::WhiteSpaceNormal); // About projection mapping QFile aboutMappingFile(":/projection-mapping"); - (void)aboutMappingFile.open(QIODevice::ReadOnly | QIODevice::Text); + if (!aboutMappingFile.open(QIODevice::ReadOnly | QIODevice::Text)) + qWarning() << "AboutDialog: could not open" << aboutMappingFile.fileName(); QString aboutMappingText = QString::fromUtf8(aboutMappingFile.readAll()); // Visit our website for more information QString projectWebsiteText = "

" + tr("See the ") + QString("").arg(MM::WEBSITE_URL) + @@ -136,7 +138,8 @@ void AboutDialog::createChangelogTab() changelogTextBrowser->setOpenExternalLinks(true); QFile changelogFile(":/changelog_md"); - (void)changelogFile.open(QIODevice::ReadOnly | QIODevice::Text); + if (!changelogFile.open(QIODevice::ReadOnly | QIODevice::Text)) + qWarning() << "AboutDialog: could not open" << changelogFile.fileName(); changelogTextBrowser->setMarkdown(QString::fromUtf8(changelogFile.readAll())); _tabWidget->addTab(changelogTextBrowser, tr("Changelog")); } @@ -173,7 +176,8 @@ void AboutDialog::createLicenseTab() licenseTextBrowser->setOpenExternalLinks(true); QFile licenseFile(":/license"); - (void)licenseFile.open(QIODevice::ReadOnly | QIODevice::Text); + if (!licenseFile.open(QIODevice::ReadOnly | QIODevice::Text)) + qWarning() << "AboutDialog: could not open" << licenseFile.fileName(); licenseTextBrowser->setText(QString::fromUtf8(licenseFile.readAll())); _tabWidget->addTab(licenseTextBrowser, tr("License")); @@ -186,7 +190,8 @@ void AboutDialog::createOscTab() oscBrowser->setOpenExternalLinks(true); QFile oscFile(":/osc-documentation_md"); - (void)oscFile.open(QIODevice::ReadOnly | QIODevice::Text); + if (!oscFile.open(QIODevice::ReadOnly | QIODevice::Text)) + qWarning() << "AboutDialog: could not open" << oscFile.fileName(); oscBrowser->setMarkdown(QString::fromUtf8(oscFile.readAll())); _tabWidget->addTab(oscBrowser, tr("OSC Commands")); } diff --git a/src/gui/ConsoleWindow.cpp b/src/gui/ConsoleWindow.cpp index bd12036e3..87813f824 100644 --- a/src/gui/ConsoleWindow.cpp +++ b/src/gui/ConsoleWindow.cpp @@ -86,7 +86,10 @@ void ConsoleWindow::writeLogFile(const QString &message) { QString logFilePath = QDir(QDir::tempPath()).filePath("mapmap.log"); QFile logFile(logFilePath); - (void)logFile.open(QIODevice::Append); + // Don't qWarning() on failure: printMessage() routes warnings back here and + // would recurse. Silently skip writing if the log file can't be opened. + if (!logFile.open(QIODevice::Append)) + return; QTextStream stream(&logFile); stream << message << Qt::endl; } From 7587b2246e1cd92c73c4678ee4b3a42230c7121c Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 01:25:58 -0400 Subject: [PATCH 09/21] Emit the Syphon.framework bundle rule only once src.pri is pulled in once per sub-project (.pri), so QMAKE_BUNDLE_DATA gained the syphon_framework entry five times, producing five identical copy rules and make's "overriding commands for target ...Syphon.framework" warnings. Guard the entry with !contains() so it is added once. Regenerating the Makefile now yields a single rule and no warning. --- src/src.pri | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/src.pri b/src/src.pri index 466f1c897..d5019448e 100644 --- a/src/src.pri +++ b/src/src.pri @@ -53,7 +53,10 @@ macx { QMAKE_LFLAGS += -Wl,-rpath,@executable_path/../Frameworks syphon_framework.files = $$SYPHON_FRAMEWORK_DIR/Syphon.framework syphon_framework.path = Contents/Frameworks - QMAKE_BUNDLE_DATA += syphon_framework + # src.pri is pulled in once per sub-project (.pri), so guard the bundle-data + # entry to avoid emitting duplicate copy rules for Syphon.framework (which + # qmake reports as "overriding commands for target ...Syphon.framework"). + !contains(QMAKE_BUNDLE_DATA, syphon_framework): QMAKE_BUNDLE_DATA += syphon_framework # Syphon OUTPUT (publishing MapMap's output as a Syphon server) is EXPERIMENTAL # and DISABLED by default: SyphonOpenGLServer cannot create its IOSurface From a39887930aa621b08dfaf3dfe0a224edcce4738b Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 01:32:36 -0400 Subject: [PATCH 10/21] Complete the Spanish (es) translation The Spanish catalogue shipped with 0 of 298 strings translated while the app still advertised Spanish support. Translate all 298 UI strings; lrelease now reports 298 finished, 0 unfinished. Note: a few OSC-preference strings added on this branch are not yet in any .ts catalogue; a future lupdate pass is needed to extract and translate them across languages (tracked under QUA-5 in AUDIT.md). --- translations/mapmap_es.ts | 596 +++++++++++++++++++------------------- 1 file changed, 298 insertions(+), 298 deletions(-) diff --git a/translations/mapmap_es.ts b/translations/mapmap_es.ts index a03c2f39e..5a8ca0585 100644 --- a/translations/mapmap_es.ts +++ b/translations/mapmap_es.ts @@ -6,7 +6,7 @@ Choose a file - Seleccionar archivo + Elegir un archivo @@ -14,147 +14,147 @@ Initiating program... - + Iniciando el programa... Done. - + Listo. Add source - + Añadir fuente Add layer - + Añadir capa Duplicate layer - + Duplicar capa Move vertex - + Mover el vértice Rotate shape - + Rotar la forma Scale shape - + Escalar la forma Move shape - + Mover la forma Remove media - + Quitar multimedia Delete layer - + Eliminar capa Flipped Horizontally - + Volteado horizontalmente Flipped Vertically - + Volteado verticalmente Problem at creation of shape. - + Problema al crear la forma. Unable to create paint of type '%1'. - + No se puede crear una pintura del tipo '%1'. The contents of this file does not look like a MapMap project. - + El contenido de este archivo no parece un proyecto de MapMap. The version of MapMap %1 used to save this file is not readable by this MapMap version %2. - + La versión de MapMap %1 usada para guardar este archivo no se puede leer con esta versión de MapMap %2. %1 Line %2, column %3 - + La versión de MapMap %1 usada para guardar este archivo no se puede leer con esta versión de MapMap %2. Problem at creation of paint. - + Problema al crear la pintura. Problem at creation of mapping. - + Problema al crear el mapeo. ID - + ID Opacity (%) - + Opacidad (%) Output shape - + Forma de salida Point %1 - + Punto %1 Mesh Subdivisions - + Subdivisiones de la malla Subdivisions - + Subdivisiones Input shape - + Forma de entrada Color - + Color @@ -164,13 +164,13 @@ Line %2, column %3 True - + Verdadero False - + Falso @@ -178,12 +178,12 @@ Line %2, column %3 True - + Verdadero False - + Falso @@ -191,7 +191,7 @@ Line %2, column %3 Clear Char - + Borrar carácter @@ -199,7 +199,7 @@ Line %2, column %3 ... - + ... @@ -207,22 +207,22 @@ Line %2, column %3 Red - + Rojo Green - + Verde Blue - + Azul Alpha - + Alfa @@ -230,97 +230,97 @@ Line %2, column %3 Arrow - + Flecha Up Arrow - + Flecha arriba Cross - + Cruz Wait - + Espera IBeam - + Cursor de texto Size Vertical - + Redimensionar vertical Size Horizontal - + Redimensionar horizontal Size Backslash - + Redimensionar (barra invertida) Size Slash - + Redimensionar (barra) Size All - + Redimensionar en todas direcciones Blank - + En blanco Split Vertical - + Dividir verticalmente Split Horizontal - + Dividir horizontalmente Pointing Hand - + Mano señaladora Forbidden - + Prohibido Open Hand - + Mano abierta Closed Hand - + Mano cerrada What's This - + Qué es esto Busy - + Ocupado @@ -328,12 +328,12 @@ Line %2, column %3 ... - + ... Select Font - + Seleccionar fuente tipográfica @@ -341,37 +341,37 @@ Line %2, column %3 Family - + Familia Point Size - + Tamaño en puntos Bold - + Negrita Italic - + Cursiva Underline - + Subrayado Strikeout - + Tachado Kerning - + Interletraje @@ -379,7 +379,7 @@ Line %2, column %3 Clear Shortcut - + Borrar atajo @@ -387,17 +387,17 @@ Line %2, column %3 %1, %2 - + %1, %2 Language - + Idioma Country - + País @@ -405,17 +405,17 @@ Line %2, column %3 (%1, %2) - + (%1, %2) X - + X Y - + Y @@ -423,17 +423,17 @@ Line %2, column %3 (%1, %2) - + (%1, %2) X - + X Y - + Y @@ -441,12 +441,12 @@ Line %2, column %3 [%1, %2, %3] (%4) - + [%1, %2, %3] (%4) [%1, %2] - + [%1, %2] @@ -454,27 +454,27 @@ Line %2, column %3 [(%1, %2), %3 x %4] - + [(%1, %2), %3 x %4] X - + X Y - + Y Width - + Anchura Height - + Altura @@ -482,27 +482,27 @@ Line %2, column %3 [(%1, %2), %3 x %4] - + [(%1, %2), %3 x %4] X - + X Y - + Y Width - + Anchura Height - + Altura @@ -510,17 +510,17 @@ Line %2, column %3 %1 x %2 - + %1 x %2 Width - + Anchura Height - + Altura @@ -529,32 +529,32 @@ Line %2, column %3 <Invalid> - + <No válido> [%1, %2, %3, %4] - + [%1, %2, %3, %4] Horizontal Policy - + Política horizontal Vertical Policy - + Política vertical Horizontal Stretch - + Estiramiento horizontal Vertical Stretch - + Estiramiento vertical @@ -562,17 +562,17 @@ Line %2, column %3 %1 x %2 - + %1 x %2 Width - + Anchura Height - + Altura @@ -580,12 +580,12 @@ Line %2, column %3 Property - + Propiedad Value - + Valor @@ -593,57 +593,57 @@ Line %2, column %3 About %1 - + Acerca de %1 MapMap is a free/open source video mapping software. - + MapMap es un software libre y de código abierto de mapeo de vídeo (video mapping). Copyright &copy; 2013 %1. - + Derechos de autor &copy; 2013 %1. See the - + Consulte el %1 website - + sitio web de %1 About - + Acerca de Changelog - + Registro de cambios Libraries - + Bibliotecas Contributors - + Colaboradores License - + Licencia OSC - + OSC @@ -651,27 +651,27 @@ Line %2, column %3 Message Log Output - Mapmap - + Registro de mensajes - Mapmap &Close - + &Cerrar Close the console - + Cerrar la consola &File - + &Archivo MMM dd yy HH:mm - + MMM dd yy HH:mm @@ -679,17 +679,17 @@ Line %2, column %3 Image file - + Archivo de imagen Image files (%1);;All files (*) - + Archivos de imagen (%1);;Todos los archivos (*) Speed (%) - + Velocidad (%) @@ -698,7 +698,7 @@ Line %2, column %3 Open project - + Abrir proyecto @@ -706,691 +706,691 @@ Line %2, column %3 MapMap files (*.%1) - + Archivos de MapMap (*.%1) Save project - + Guardar proyecto Import media source file - + Importar archivo de fuente multimedia Media files (%1 %2);;All files (*) - + Archivos multimedia (%1 %2);;Todos los archivos (*) Camera device - + Dispositivo de cámara Select camera - + Seleccionar cámara No camera available - + No hay cámara disponible You can not use this feature! No camera available in your system - + No hay cámara disponible Select Color - + Seleccionar color MapMap - + MapMap &New - + &Nuevo Create a new project - + Crear un nuevo proyecto &Open... - + &Abrir... Open an existing project - + Abrir un proyecto existente &Save - + &Guardar Save the project - + Guardar el proyecto Save &As... - + Guardar &como... Save the project as... - + Guardar el proyecto como... No Recents Videos - + No hay vídeos recientes &Import Media File... - + &Importar archivo multimedia... Import a video or image file... - + Importar un archivo de vídeo o imagen... Open &Camera Device... - + Abrir dispositivo de &cámara... Choose your camera device... - + Elija su dispositivo de cámara... Add a color paint... - + Añadir una pintura de color... E&xit - + S&alir Exit the application - + Salir de la aplicación &Undo - + &Deshacer &Redo - + &Rehacer &About MapMap - + &Acerca de MapMap Show the application's About box - + Mostrar el cuadro Acerca de de la aplicación &Preferences... - + &Preferencias... Configure preferences... - + Configurar preferencias... Play - + Reproducir Pause - + Pausa Toggle &Fullscreen - + Alternar &pantalla completa Toggle Fullscreen - + Alternar pantalla completa &Display Controls - + &Mostrar controles Display canvas controls - + Mostrar los controles del lienzo &Sticky Vertices - + &Vértices magnéticos Enable sticky vertices - + Activar vértices magnéticos Show &Test Signal - + Mostrar señal de &prueba Show Test signal - + Mostrar señal de prueba Display &Undo History - + Mostrar &historial de deshacer Open Conso&le - + Abrir conso&la Display &Zoom Toolbar - + Mostrar barra de &zoom &Menu Bar - + Barra de &menús Main Layout - + Disposición principal Switch to the Main layout. - + Cambiar a la disposición principal. Remove this source and all its associated layers? - + ¿Quitar esta fuente y todas sus capas asociadas? Input Editor - + Editor de entrada Output Editor - + Editor de salida Library - + Biblioteca Layers - + Capas Add &Color Source... - + Añadir fuente de &color... Duplicate Layer - + Duplicar capa Duplicate layer item - + Duplicar elemento de capa Delete Layer - + Eliminar capa Delete layer item - + Eliminar elemento de capa Rename Layer - + Renombrar capa Rename layer item - + Renombrar elemento de capa Lock Layer - + Bloquear capa Lock layer item - + Bloquear elemento de capa Hide Layer - + Ocultar capa Hide layer item - + Ocultar elemento de capa Solo Layer - + Capa en solo solo layer item - + elemento de capa en solo Flip Horizontally - + Voltear horizontalmente Flip Vertically - + Voltear verticalmente Delete Source - + Eliminar fuente Delete source item - + Eliminar elemento de fuente Rename Source - + Renombrar fuente Rename source item - + Renombrar elemento de fuente Import New Media - + Importar nuevo archivo multimedia Import new media file if not exists on the list - + Importar un nuevo archivo multimedia si no está en la lista Add &Mesh Layer - + Añadir capa de &malla Add mesh layer - + Añadir capa de malla Add &Triangle Layer - + Añadir capa de &triángulo Add triangle layer - + Añadir capa de triángulo Add &Ellipse Layer - + Añadir capa de &elipse Add ellipse layer - + Añadir capa de elipse Restart - + Reiniciar &Display Controls of Layers of a Source - + &Mostrar controles de las capas de una fuente Display all canvas controls related to current source - + Mostrar todos los controles del lienzo relacionados con la fuente actual Input editor Layout - + Disposición del editor de entrada Switch to the Input editor Layout. - + Cambiar a la disposición del editor de entrada. Output Editor Layout - + Disposición del editor de salida Switch to the Output Editors Layout. - + Cambiar a la disposición del editor de salida. Zoom In - + Acercar Zoom Out - + Alejar Original Size - + Tamaño original Reset zoom to original size - + Restablecer el zoom al tamaño original Fit To View - + Ajustar a la vista Fit to viewport - + Ajustar a la ventana gráfica Report an issue - + Informar de un problema Technical support - + Soporte técnico Documentation - + Documentación Submit feedback via email - + Enviar comentarios por correo electrónico &File - + &Archivo Open Recents Projects - + Abrir proyectos recientes Open Recents Videos - + Abrir vídeos recientes &Edit - + &Editar &View - + &Ver &Output screen - + Pantalla de &salida &Window - + Ve&ntana &Help - + A&yuda Change Layer Source - + Cambiar la fuente de la capa &Toolbar - + Barra de &herramientas The document has been modified. Do you want to save your changes? - + Barra de &herramientas Error reading mapping project file - + Error al leer el archivo de proyecto Cannot read file %1: %2. - + Error al leer el archivo de proyecto Parse error in file %1: %2 - + Error al leer el archivo de proyecto File loaded - + Archivo cargado Error saving mapping project - + Error al guardar el proyecto Cannot write file %1: %2. - + Error al guardar el proyecto File saved - + Archivo guardado Untitled - + Sin título %1[*] - %2 - + %1[*] - %2 MapMap Project - + Proyecto de MapMap &%1 %2 - + &%1 %2 Clear List - + Vaciar lista No Recents Projects - + No hay proyectos recientes %1 - %2x%3 - + %1 - %2x%3 - Primary - + - Principal File imported - + Archivo importado Color source added - + Fuente de color añadida Warning - + Advertencia The following file is not supported: %1 - + El siguiente archivo no es compatible: %1 Cannot load movie - + No se puede cargar el vídeo Unable to use file %1. The original file is not found. Please locate. - + No se puede cargar el vídeo Locate file %1 - + Localizar el archivo %1 %1 files (%2) - + Archivos %1 (%2) Undo history - + Historial de deshacer @@ -1398,22 +1398,22 @@ The original file is not found. Please locate. Enlarge the shape - + Agrandar la forma Shrink the shape - + Encoger la forma Reset the shape to the normal size - + Restablecer la forma a su tamaño normal Fit the shape to content view - + Ajustar la forma a la vista @@ -1421,22 +1421,22 @@ The original file is not found. Please locate. Solo mapping - + Mapeo en solo Lock mapping - + Bloquear mapeo Duplicate mapping - + Duplicar mapeo Delete mapping - + Eliminar mapeo @@ -1444,12 +1444,12 @@ The original file is not found. Please locate. Horizontal - + Horizontal Vertical - + Vertical @@ -1457,147 +1457,147 @@ The original file is not found. Please locate. Preferences - + Preferencias Large - + Grande Medium - + Mediano Small - + Pequeño Language (requires restart) - + Idioma (requiere reiniciar) Toolbar icon size (requires restart) - + Tamaño de los iconos de la barra de herramientas (requiere reiniciar) Enable Sticky vertices - + Activar vértices magnéticos Sensitivity - + Sensibilidad Vertices - + Vértices Only show output controls on mouse over - + Mostrar los controles de salida solo al pasar el ratón Output Layers - + Capas de salida Show resolution on output test cards - + Mostrar la resolución en las cartas de ajuste de salida Classic test card - + Carta de ajuste clásica PAL test card - + Carta de ajuste PAL NTSC test card - + Carta de ajuste NTSC Test Card - + Carta de ajuste Listen to OSC messages - + Escuchar mensajes OSC Allow message with existing media source - + Permitir mensaje con fuente multimedia existente on port - + en el puerto Refresh - + Actualizar Local IP - + IP local OSC Setup - + Configuración de OSC Play in loop (requires restart) - + Reproducir en bucle (requiere reiniciar) Playback - + Reproducción Interface - + Interfaz Layers - + Capas Output - + Salida Controls - + Controles Advanced - + Avanzado @@ -1605,22 +1605,22 @@ The original file is not found. Please locate. Source - + Fuente Video files (%1);;All files (*) - + Archivos de vídeo (%1);;Todos los archivos (*) Speed (%) - + Velocidad (%) Volume (%) - + Volumen (%) From 76dd57b0754412528bad2326fa2df175b1729f5b Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 01:33:49 -0400 Subject: [PATCH 11/21] Fail cleanly on unconstructable elements when loading a project parseSource() and parseLayer() logged a null result from newInstance() and then dereferenced it via ->read(obj), so a corrupt or incompatible .mmp entry whose registered class cannot be instantiated crashed the load. Bail out on a null source/layer instead of calling read(): skip the source with a warning, and report a clear error string for a failed layer so readFile() returns false and the user sees a message. --- src/core/ProjectReader.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/ProjectReader.cpp b/src/core/ProjectReader.cpp index 8dcc2714c..1a7be864b 100644 --- a/src/core/ProjectReader.cpp +++ b/src/core/ProjectReader.cpp @@ -143,13 +143,14 @@ Source::ptr ProjectReader::parseSource(const QJsonObject& obj) if (source.isNull()) { - qDebug() << QObject::tr("Problem at creation of source.") << Qt::endl; + // Registered type that could not be instantiated (malformed or + // incompatible entry). Bail out instead of dereferencing a null source. + qWarning() << "Could not instantiate source of type" << className; + return Source::ptr(); } - else - qDebug() << "Created new instance with id: " << source->getId(); + qDebug() << "Created new instance with id: " << source->getId(); source->read(obj); - return source; } else @@ -172,11 +173,13 @@ Layer::ptr ProjectReader::parseLayer(const QJsonObject& obj) Layer::ptr layer(qobject_cast(metaObject->newInstance(Q_ARG(int, id)))); if (layer.isNull()) { - qDebug() << QObject::tr("Problem at creation of layer.") << Qt::endl; + // Registered type that could not be instantiated. Report and bail out + // instead of dereferencing a null layer. + _errorString = QObject::tr("Unable to create layer of type '%1'.").arg(className); + return Layer::ptr(); } layer->read(obj); - return layer; } else From 609603fe509a86951d9fec721b57feaba8ccf8e1 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 01:37:19 -0400 Subject: [PATCH 12/21] Require loopback origin and a bearer token for the MCP server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded MCP HTTP server accepted any request on its loopback port, so any local process — or a web page the operator visited, via a spoofed Host (DNS-rebinding) — could drive MapMap and read project/file metadata. Reject requests whose Host (or Origin, when present) is not loopback, and require an "Authorization: Bearer " header. The token is generated per process at startup and logged alongside the endpoint so a local MCP client can be configured. Note: MCP still starts by default on its port; making it opt-in (port 0 default) remains a separate product decision, tracked under SEC-4 in AUDIT.md. --- src/control/McpServer.cpp | 46 +++++++++++++++++++++++++++++++++++++++ src/control/McpServer.h | 9 ++++++++ src/gui/MainWindow.cpp | 3 ++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/control/McpServer.cpp b/src/control/McpServer.cpp index 1accace41..d7781b31f 100644 --- a/src/control/McpServer.cpp +++ b/src/control/McpServer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -40,9 +41,38 @@ namespace mmp { +namespace { +// Host/Origin values that identify the local machine, used to reject +// DNS-rebinding and cross-origin requests to the control surface. +bool isLoopbackHost(QByteArray host) +{ + host = host.trimmed(); + if (host.startsWith('[')) // IPv6 literal, e.g. [::1]:port + { + const int end = host.indexOf(']'); + if (end > 0) host = host.mid(1, end - 1); + } + else // host:port + { + const int colon = host.indexOf(':'); + if (colon >= 0) host = host.left(colon); + } + return host == "localhost" || host == "127.0.0.1" || host == "::1"; +} + +bool isLoopbackOrigin(QByteArray origin) +{ + const int scheme = origin.indexOf("://"); + if (scheme >= 0) origin = origin.mid(scheme + 3); + return isLoopbackHost(origin); +} +} // namespace + McpServer::McpServer(MainWindow* mainWindow, QObject* parent) : QObject(parent), _mainWindow(mainWindow), _httpServer(nullptr), _port(0) { + // Per-process shared secret required on every request (see isAuthorized). + _token = QUuid::createUuid().toString(QUuid::WithoutBraces); } McpServer::~McpServer() @@ -61,6 +91,9 @@ quint16 McpServer::start(quint16 port) if (request.method() != QHttpServerRequest::Method::Post) return QHttpServerResponse("text/plain", QByteArray("Method Not Allowed"), QHttpServerResponse::StatusCode::MethodNotAllowed); + if (!isAuthorized(request)) + return QHttpServerResponse("text/plain", QByteArray("Forbidden"), + QHttpServerResponse::StatusCode::Forbidden); const QByteArray out = handleRpc(request.body()); if (out.isEmpty()) // notification: acknowledge with no body return QHttpServerResponse(QHttpServerResponse::StatusCode::Accepted); @@ -85,6 +118,19 @@ bool McpServer::isListening() const return _httpServer != nullptr && _port != 0; } +bool McpServer::isAuthorized(const QHttpServerRequest& request) const +{ + // 1. DNS-rebinding guard: the Host header must name the loopback interface. + if (!isLoopbackHost(request.value("Host"))) + return false; + // 2. If a browser sent an Origin, it must be loopback too ("null" is allowed). + const QByteArray origin = request.value("Origin"); + if (!origin.isEmpty() && origin != "null" && !isLoopbackOrigin(origin)) + return false; + // 3. Shared-secret bearer token generated at startup. + return request.value("Authorization") == QByteArray("Bearer ") + _token.toUtf8(); +} + QByteArray McpServer::handleRpc(const QByteArray& body) { QJsonParseError parseError; diff --git a/src/control/McpServer.h b/src/control/McpServer.h index ee518ecd3..1da54462f 100644 --- a/src/control/McpServer.h +++ b/src/control/McpServer.h @@ -30,6 +30,7 @@ QT_BEGIN_NAMESPACE class QHttpServer; +class QHttpServerRequest; QT_END_NAMESPACE namespace mmp { @@ -56,12 +57,19 @@ class McpServer : public QObject { /// Currently bound port (0 if not listening). quint16 port() const { return _port; } + /// Bearer token that every request must present in its Authorization header. + QString token() const { return _token; } + private: // --- JSON-RPC plumbing --- // Returns the response body, or an empty array for notifications (no reply). QByteArray handleRpc(const QByteArray& body); QJsonObject dispatch(const QJsonObject& request); + // Rejects requests that are non-loopback (DNS-rebinding), cross-origin, or + // missing the bearer token. + bool isAuthorized(const QHttpServerRequest& request) const; + static QJsonObject makeResult(const QJsonValue& id, const QJsonValue& result); static QJsonObject makeError(const QJsonValue& id, int code, const QString& message); @@ -83,6 +91,7 @@ class McpServer : public QObject { MainWindow* _mainWindow; QHttpServer* _httpServer; quint16 _port; + QString _token; }; } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 324b94e24..22aa5cc6b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3986,7 +3986,8 @@ void MainWindow::startMcpServer() quint16 boundPort = mcp_server->start(static_cast(mcpListeningPort)); if (boundPort != 0) QMessageLogger(__FILE__, __LINE__, 0).info() - << "MCP server listening on http://localhost:" << boundPort << "/mcp"; + << "MCP server listening on http://localhost:" << boundPort + << "/mcp (Authorization: Bearer " << qUtf8Printable(mcp_server->token()) << ")"; else qWarning() << "MCP server could not start on port" << mcpListeningPort; } From 57e8bc617419b8eec22d26eca75a00cc9483cd95 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 02:35:48 -0400 Subject: [PATCH 13/21] Document the project format as JSON, not XML Both ProjectReader and ProjectWriter use QJsonDocument, so .mmp files are JSON. The datasheet and the English manual still described the save format as XML; correct them. Historical CHANGELOG entries about the original XML-based v0.1 format are left as-is. --- docs/datasheet.rst | 4 ++-- docs/manual/manual_en.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/datasheet.rst b/docs/datasheet.rst index 8830420bc..43f23ba2f 100644 --- a/docs/datasheet.rst +++ b/docs/datasheet.rst @@ -9,8 +9,8 @@ MapMap allows its users to: * Create and destroy an unlimited amount of mappings. Each mapping is a shape on which a source paint is drawn. * Color paints can be used as masks. * Move layers. -* Save the project to a human-readable XML file. -* Load the project from a human-readable XML file. +* Save the project to a human-readable JSON file. +* Load the project from a human-readable JSON file. * Turn any quad into a mesh. * Inspect properties. diff --git a/docs/manual/manual_en.rst b/docs/manual/manual_en.rst index cfd582304..653c6d161 100644 --- a/docs/manual/manual_en.rst +++ b/docs/manual/manual_en.rst @@ -49,7 +49,7 @@ Paints can be destroyed simply by selecting it in the paint list, and then choos Save the project to a file -------------------------- -To save the current project, choose "File > Save as..." and then choose a file name. The extension file is ".mmp", but the file format is simply XML, a very common one. +To save the current project, choose "File > Save as..." and then choose a file name. The extension file is ".mmp", but the file format is simply JSON, a very common one. Load a project from a file -------------------------- From a17dd11318b01f905d09bc0b69df2f8acff0a75f Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 02:37:44 -0400 Subject: [PATCH 14/21] Disable the MCP server by default (opt-in) DEFAULT_MCP_PORT was 49452, so the MCP HTTP control surface started on every launch. Default it to 0 (disabled); the user opts in by setting a port in Preferences > MCP Setup, which already renders 0 as "Disabled". Also reorder startMcpServer() so port 0 tears down any running server, making a runtime switch to "Disabled" actually stop listening. --- src/core/MM.h | 7 ++++--- src/gui/MainWindow.cpp | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/MM.h b/src/core/MM.h index ea27955fd..9e80d84a4 100644 --- a/src/core/MM.h +++ b/src/core/MM.h @@ -79,9 +79,10 @@ class MM // OSC static const int DEFAULT_OSC_PORT = 12345; - // MCP (Model Context Protocol) server. Uses a port in the IANA - // dynamic/private range (49152-65535) to minimise collisions. - static const int DEFAULT_MCP_PORT = 49452; + // MCP (Model Context Protocol) server. Disabled by default (0): the user + // opts in by setting a port in Preferences > MCP Setup. A good enabled value + // is one in the IANA dynamic/private range (49152-65535), e.g. 49452. + static const int DEFAULT_MCP_PORT = 0; // Default values static const bool DISPLAY_TEST_SIGNAL = false; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 22aa5cc6b..20c18cd2c 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3974,15 +3974,18 @@ bool MainWindow::setOscPort(QString portNumber) #ifdef HAVE_MCP void MainWindow::startMcpServer() { - if (mcp_server.isNull()) - mcp_server.reset(new McpServer(this)); - if (mcpListeningPort == 0) { + // Disabled (the default): tear down any running server so that setting the + // port to 0 at runtime actually stops listening. + mcp_server.reset(); QMessageLogger(__FILE__, __LINE__, 0).info() << "MCP server disabled (port 0)."; return; } + if (mcp_server.isNull()) + mcp_server.reset(new McpServer(this)); + quint16 boundPort = mcp_server->start(static_cast(mcpListeningPort)); if (boundPort != 0) QMessageLogger(__FILE__, __LINE__, 0).info() From ca2e48d3b19836c2e4b40429db208720f841eba6 Mon Sep 17 00:00:00 2001 From: Alexandre Quessy Date: Thu, 2 Jul 2026 02:39:49 -0400 Subject: [PATCH 15/21] Validate vertex and media fields when loading a project Two unchecked reads in the load path: - MShape::read indexed each vertex sub-array as vertex[0]/vertex[1] with no size check, so a malformed [x] or [] entry read out of bounds. Skip vertices that do not have at least two components. - parseProject cast Video/Image sources with Q_CHECK_PTR (a no-op in release builds) and then dereferenced the result, so a corrupt source claiming a media type it cannot cast to would crash. Null-check the cast. Deeper fuzzing of ProjectReader still needs a headless model-layer test harness (tracked with the QUA-4 remainder). --- src/core/ProjectReader.cpp | 10 +++++----- src/shape/Shape.cpp | 9 +++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/ProjectReader.cpp b/src/core/ProjectReader.cpp index 1a7be864b..337467c76 100644 --- a/src/core/ProjectReader.cpp +++ b/src/core/ProjectReader.cpp @@ -87,19 +87,19 @@ void ProjectReader::parseProject(const QJsonObject& project) manager.addSource(source); _window->addSourceItem(source->getId(), source->getIcon(), source->getName()); - // Locate media file if not found + // Locate media file if not found. Guard the cast: a source that claims + // to be Video/Image but cannot be cast (corrupt entry) must not be + // dereferenced — Q_CHECK_PTR is a no-op in release builds. if (source->getSourceType() == Source::SourceType::Video) { QSharedPointer