summaryrefslogtreecommitdiff
path: root/meshmc/libraries
diff options
context:
space:
mode:
Diffstat (limited to 'meshmc/libraries')
-rw-r--r--meshmc/libraries/LocalPeer/CMakeLists.txt28
-rw-r--r--meshmc/libraries/LocalPeer/include/LocalPeer.h101
-rw-r--r--meshmc/libraries/LocalPeer/src/LocalPeer.cpp235
-rw-r--r--meshmc/libraries/LocalPeer/src/LockedFile.cpp191
-rw-r--r--meshmc/libraries/LocalPeer/src/LockedFile.h76
-rw-r--r--meshmc/libraries/LocalPeer/src/LockedFile_unix.cpp112
-rw-r--r--meshmc/libraries/LocalPeer/src/LockedFile_win.cpp204
-rw-r--r--meshmc/libraries/README.md188
-rw-r--r--meshmc/libraries/classparser/CMakeLists.txt41
-rw-r--r--meshmc/libraries/classparser/include/classparser.h49
-rw-r--r--meshmc/libraries/classparser/include/classparser_config.h45
-rw-r--r--meshmc/libraries/classparser/src/annotations.cpp105
-rw-r--r--meshmc/libraries/classparser/src/annotations.h296
-rw-r--r--meshmc/libraries/classparser/src/classfile.h169
-rw-r--r--meshmc/libraries/classparser/src/classparser.cpp116
-rw-r--r--meshmc/libraries/classparser/src/constants.h241
-rw-r--r--meshmc/libraries/classparser/src/errors.h29
-rw-r--r--meshmc/libraries/classparser/src/javaendian.h84
-rw-r--r--meshmc/libraries/classparser/src/membuffer.h84
-rw-r--r--meshmc/libraries/ganalytics/CMakeLists.txt17
-rw-r--r--meshmc/libraries/ganalytics/LICENSE.txt24
-rw-r--r--meshmc/libraries/ganalytics/README.md34
-rw-r--r--meshmc/libraries/ganalytics/include/ganalytics.h92
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics.cpp262
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics_worker.cpp267
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics_worker.h84
-rw-r--r--meshmc/libraries/hoedown/CMakeLists.txt26
-rw-r--r--meshmc/libraries/hoedown/LICENSE15
-rw-r--r--meshmc/libraries/hoedown/README.md9
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/autolink.h65
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/buffer.h153
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/document.h221
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/escape.h46
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/html.h102
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/stack.h68
-rw-r--r--meshmc/libraries/hoedown/include/hoedown/version.h49
-rw-r--r--meshmc/libraries/hoedown/src/autolink.c292
-rw-r--r--meshmc/libraries/hoedown/src/buffer.c307
-rw-r--r--meshmc/libraries/hoedown/src/document.c3102
-rw-r--r--meshmc/libraries/hoedown/src/escape.c191
-rw-r--r--meshmc/libraries/hoedown/src/html.c823
-rw-r--r--meshmc/libraries/hoedown/src/html_blocks.c241
-rw-r--r--meshmc/libraries/hoedown/src/html_smartypants.c512
-rw-r--r--meshmc/libraries/hoedown/src/stack.c94
-rw-r--r--meshmc/libraries/hoedown/src/version.c29
-rw-r--r--meshmc/libraries/iconfix/CMakeLists.txt28
-rw-r--r--meshmc/libraries/iconfix/internal/qhexstring_p.h92
-rw-r--r--meshmc/libraries/iconfix/internal/qiconloader.cpp646
-rw-r--r--meshmc/libraries/iconfix/internal/qiconloader_p.h216
-rw-r--r--meshmc/libraries/iconfix/xdgicon.cpp142
-rw-r--r--meshmc/libraries/iconfix/xdgicon.h51
-rw-r--r--meshmc/libraries/javacheck/.gitignore6
-rw-r--r--meshmc/libraries/javacheck/CMakeLists.txt14
-rw-r--r--meshmc/libraries/javacheck/JavaCheck.java45
-rw-r--r--meshmc/libraries/katabasis/.gitignore2
-rw-r--r--meshmc/libraries/katabasis/CMakeLists.txt53
-rw-r--r--meshmc/libraries/katabasis/LICENSE23
-rw-r--r--meshmc/libraries/katabasis/README.md36
-rw-r--r--meshmc/libraries/katabasis/acknowledgements.md110
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Bits.h57
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h178
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Globals.h82
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/PollServer.h73
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/Reply.h92
-rw-r--r--meshmc/libraries/katabasis/include/katabasis/RequestParameter.h42
-rw-r--r--meshmc/libraries/katabasis/src/DeviceFlow.cpp539
-rw-r--r--meshmc/libraries/katabasis/src/JsonResponse.cpp51
-rw-r--r--meshmc/libraries/katabasis/src/JsonResponse.h34
-rw-r--r--meshmc/libraries/katabasis/src/PollServer.cpp147
-rw-r--r--meshmc/libraries/katabasis/src/Reply.cpp96
-rw-r--r--meshmc/libraries/launcher/.gitignore6
-rw-r--r--meshmc/libraries/launcher/CMakeLists.txt22
-rw-r--r--meshmc/libraries/launcher/net/minecraft/MeshMC.java208
-rw-r--r--meshmc/libraries/launcher/org/projecttick/EntryPoint.java200
-rw-r--r--meshmc/libraries/launcher/org/projecttick/LegacyFrame.java217
-rw-r--r--meshmc/libraries/launcher/org/projecttick/MeshMC.java61
-rw-r--r--meshmc/libraries/launcher/org/projecttick/NotFoundException.java60
-rw-r--r--meshmc/libraries/launcher/org/projecttick/ParamBucket.java125
-rw-r--r--meshmc/libraries/launcher/org/projecttick/ParseException.java64
-rw-r--r--meshmc/libraries/launcher/org/projecttick/Utils.java158
-rw-r--r--meshmc/libraries/launcher/org/projecttick/modern/ModernLauncher.java309
-rw-r--r--meshmc/libraries/launcher/org/projecttick/onesix/OneSixLauncher.java288
m---------meshmc/libraries/libnbtplusplus15
-rw-r--r--meshmc/libraries/optional-bare/CMakeLists.txt5
-rw-r--r--meshmc/libraries/optional-bare/LICENSE.txt23
-rw-r--r--meshmc/libraries/optional-bare/README.md5
-rw-r--r--meshmc/libraries/optional-bare/include/nonstd/optional508
-rw-r--r--meshmc/libraries/rainbow/CMakeLists.txt22
-rw-r--r--meshmc/libraries/rainbow/COPYING.LIB0
-rw-r--r--meshmc/libraries/rainbow/include/rainbow.h167
-rw-r--r--meshmc/libraries/rainbow/include/rainbow_config.h49
-rw-r--r--meshmc/libraries/rainbow/src/rainbow.cpp320
-rw-r--r--meshmc/libraries/systeminfo/CMakeLists.txt29
-rw-r--r--meshmc/libraries/systeminfo/include/distroutils.h44
-rw-r--r--meshmc/libraries/systeminfo/include/sys.h71
-rw-r--r--meshmc/libraries/systeminfo/src/distroutils.cpp267
-rw-r--r--meshmc/libraries/systeminfo/src/sys_apple.cpp93
-rw-r--r--meshmc/libraries/systeminfo/src/sys_test.cpp52
-rw-r--r--meshmc/libraries/systeminfo/src/sys_unix.cpp128
-rw-r--r--meshmc/libraries/systeminfo/src/sys_win32.cpp79
-rw-r--r--meshmc/libraries/tomlc99/CMakeLists.txt10
-rw-r--r--meshmc/libraries/tomlc99/LICENSE22
-rw-r--r--meshmc/libraries/tomlc99/README.md194
-rw-r--r--meshmc/libraries/tomlc99/include/toml.h192
-rw-r--r--meshmc/libraries/tomlc99/src/toml.c2468
-rw-r--r--meshmc/libraries/xz-embedded/CMakeLists.txt26
-rw-r--r--meshmc/libraries/xz-embedded/include/xz.h315
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_config.h120
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_crc32.c60
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_crc64.c51
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_dec_bcj.c565
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_dec_lzma2.c1149
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_dec_stream.c832
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_lzma2.h205
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_private.h155
-rw-r--r--meshmc/libraries/xz-embedded/src/xz_stream.h62
-rw-r--r--meshmc/libraries/xz-embedded/xzminidec.c136
117 files changed, 22531 insertions, 0 deletions
diff --git a/meshmc/libraries/LocalPeer/CMakeLists.txt b/meshmc/libraries/LocalPeer/CMakeLists.txt
new file mode 100644
index 0000000000..60eb1930ac
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required(VERSION 3.25)
+project(LocalPeer)
+
+find_package(Qt6 COMPONENTS Core Network REQUIRED)
+
+set(SINGLE_SOURCES
+src/LocalPeer.cpp
+src/LockedFile.cpp
+src/LockedFile.h
+include/LocalPeer.h
+)
+
+if(UNIX)
+ list(APPEND SINGLE_SOURCES
+ src/LockedFile_unix.cpp
+ )
+endif()
+
+if(WIN32)
+ list(APPEND SINGLE_SOURCES
+ src/LockedFile_win.cpp
+ )
+endif()
+
+add_library(LocalPeer STATIC ${SINGLE_SOURCES})
+target_include_directories(LocalPeer PUBLIC include)
+
+target_link_libraries(LocalPeer Qt6::Core Qt6::Network)
diff --git a/meshmc/libraries/LocalPeer/include/LocalPeer.h b/meshmc/libraries/LocalPeer/include/LocalPeer.h
new file mode 100644
index 0000000000..13117c6ac4
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/include/LocalPeer.h
@@ -0,0 +1,101 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#pragma once
+#include <QObject>
+#include <QString>
+#include <memory>
+
+class QLocalServer;
+class LockedFile;
+
+class ApplicationId
+{
+ public: /* methods */
+ // traditional app = installed system wide and used in a multi-user
+ // environment
+ static ApplicationId fromTraditionalApp();
+ // ID based on a path with all the application data (no two instances with
+ // the same data path should run)
+ static ApplicationId fromPathAndVersion(const QString& dataPath,
+ const QString& version);
+ // custom ID
+ static ApplicationId fromCustomId(const QString& id);
+ // custom ID, based on a raw string previously acquired from 'toString'
+ static ApplicationId fromRawString(const QString& id);
+
+ QString toString()
+ {
+ return m_id;
+ }
+
+ private: /* methods */
+ ApplicationId(const QString& value)
+ {
+ m_id = value;
+ }
+
+ private: /* data */
+ QString m_id;
+};
+
+class LocalPeer : public QObject
+{
+ Q_OBJECT
+
+ public:
+ LocalPeer(QObject* parent, const ApplicationId& appId);
+ ~LocalPeer();
+ bool isClient();
+ bool sendMessage(const QByteArray& message, int timeout);
+ ApplicationId applicationId() const;
+
+ Q_SIGNALS:
+ void messageReceived(const QByteArray& message);
+
+ protected Q_SLOTS:
+ void receiveConnection();
+
+ protected:
+ ApplicationId id;
+ QString socketName;
+ std::unique_ptr<QLocalServer> server;
+ std::unique_ptr<LockedFile> lockFile;
+};
diff --git a/meshmc/libraries/LocalPeer/src/LocalPeer.cpp b/meshmc/libraries/LocalPeer/src/LocalPeer.cpp
new file mode 100644
index 0000000000..1ae9694b9c
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/src/LocalPeer.cpp
@@ -0,0 +1,235 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "LocalPeer.h"
+#include <QCoreApplication>
+#include <QDataStream>
+#include <QTime>
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QDir>
+#include "LockedFile.h"
+
+#if defined(Q_OS_WIN)
+#include <QLibrary>
+#include <qt_windows.h>
+typedef BOOL(WINAPI* PProcessIdToSessionId)(DWORD, DWORD*);
+static PProcessIdToSessionId pProcessIdToSessionId = 0;
+#endif
+#if defined(Q_OS_UNIX)
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+#include <chrono>
+#include <thread>
+#include <QCryptographicHash>
+#include <QRegularExpression>
+
+static const char* ack = "ack";
+
+ApplicationId ApplicationId::fromTraditionalApp()
+{
+ QString protoId = QCoreApplication::applicationFilePath();
+#if defined(Q_OS_WIN)
+ protoId = protoId.toLower();
+#endif
+ auto prefix = protoId.section(QLatin1Char('/'), -1);
+ prefix.remove(QRegularExpression("[^a-zA-Z]"));
+ prefix.truncate(6);
+ QByteArray idc = protoId.toUtf8();
+ quint16 idNum = qChecksum(idc.constData(), idc.size());
+ auto socketName = QLatin1String("qtsingleapp-") + prefix +
+ QLatin1Char('-') + QString::number(idNum, 16);
+#if defined(Q_OS_WIN)
+ if (!pProcessIdToSessionId) {
+ QLibrary lib("kernel32");
+ pProcessIdToSessionId =
+ (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId");
+ }
+ if (pProcessIdToSessionId) {
+ DWORD sessionId = 0;
+ pProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
+ socketName += QLatin1Char('-') + QString::number(sessionId, 16);
+ }
+#else
+ socketName += QLatin1Char('-') + QString::number(::getuid(), 16);
+#endif
+ return ApplicationId(socketName);
+}
+
+ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath,
+ const QString& version)
+{
+ QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1);
+ QString result = dataPath + QLatin1Char('-') + version;
+ shasum.addData(result.toUtf8());
+ return ApplicationId(QLatin1String("qtsingleapp-") +
+ QString::fromLatin1(shasum.result().toHex()));
+}
+
+ApplicationId ApplicationId::fromCustomId(const QString& id)
+{
+ return ApplicationId(QLatin1String("qtsingleapp-") + id);
+}
+
+ApplicationId ApplicationId::fromRawString(const QString& id)
+{
+ return ApplicationId(id);
+}
+
+LocalPeer::LocalPeer(QObject* parent, const ApplicationId& appId)
+ : QObject(parent), id(appId)
+{
+ socketName = id.toString();
+ server.reset(new QLocalServer());
+ QString lockName = QDir(QDir::tempPath()).absolutePath() +
+ QLatin1Char('/') + socketName +
+ QLatin1String("-lockfile");
+ lockFile.reset(new LockedFile(lockName));
+ lockFile->open(QIODevice::ReadWrite);
+}
+
+LocalPeer::~LocalPeer() {}
+
+ApplicationId LocalPeer::applicationId() const
+{
+ return id;
+}
+
+bool LocalPeer::isClient()
+{
+ if (lockFile->isLocked())
+ return false;
+
+ if (!lockFile->lock(LockedFile::WriteLock, false))
+ return true;
+
+ bool res = server->listen(socketName);
+#if defined(Q_OS_UNIX)
+ // ### Workaround
+ if (!res && server->serverError() == QAbstractSocket::AddressInUseError) {
+ QFile::remove(QDir::cleanPath(QDir::tempPath()) + QLatin1Char('/') +
+ socketName);
+ res = server->listen(socketName);
+ }
+#endif
+ if (!res)
+ qWarning("QtSingleCoreApplication: listen on local socket failed, %s",
+ qPrintable(server->errorString()));
+ QObject::connect(server.get(), SIGNAL(newConnection()),
+ SLOT(receiveConnection()));
+ return false;
+}
+
+bool LocalPeer::sendMessage(const QByteArray& message, int timeout)
+{
+ if (!isClient())
+ return false;
+
+ QLocalSocket socket;
+ bool connOk = false;
+ for (int i = 0; i < 2; i++) {
+ // Try twice, in case the other instance is just starting up
+ socket.connectToServer(socketName);
+ connOk = socket.waitForConnected(timeout / 2);
+ if (connOk || i) {
+ break;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ }
+ if (!connOk) {
+ return false;
+ }
+
+ QByteArray uMsg(message);
+ QDataStream ds(&socket);
+
+ ds.writeBytes(uMsg.constData(), uMsg.size());
+ if (!socket.waitForBytesWritten(timeout)) {
+ return false;
+ }
+
+ // wait for 'ack'
+ if (!socket.waitForReadyRead(timeout)) {
+ return false;
+ }
+
+ // make sure we got 'ack'
+ if (!(socket.read(qstrlen(ack)) == ack)) {
+ return false;
+ }
+ return true;
+}
+
+void LocalPeer::receiveConnection()
+{
+ QLocalSocket* socket = server->nextPendingConnection();
+ if (!socket) {
+ return;
+ }
+
+ while (socket->bytesAvailable() < (int)sizeof(quint32)) {
+ socket->waitForReadyRead();
+ }
+ QDataStream ds(socket);
+ QByteArray uMsg;
+ quint32 remaining;
+ ds >> remaining;
+ uMsg.resize(remaining);
+ int got = 0;
+ char* uMsgBuf = uMsg.data();
+ do {
+ got = ds.readRawData(uMsgBuf, remaining);
+ remaining -= got;
+ uMsgBuf += got;
+ } while (remaining && got >= 0 && socket->waitForReadyRead(2000));
+ if (got < 0) {
+ qWarning("QtLocalPeer: Message reception failed %s",
+ socket->errorString().toLatin1().constData());
+ delete socket;
+ return;
+ }
+ socket->write(ack, qstrlen(ack));
+ socket->waitForBytesWritten(1000);
+ socket->waitForDisconnected(1000); // make sure client reads ack
+ delete socket;
+ emit messageReceived(uMsg); // ### (might take a long time to return)
+}
diff --git a/meshmc/libraries/LocalPeer/src/LockedFile.cpp b/meshmc/libraries/LocalPeer/src/LockedFile.cpp
new file mode 100644
index 0000000000..fdb3c62bf9
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/src/LockedFile.cpp
@@ -0,0 +1,191 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "LockedFile.h"
+
+/*!
+ \class QtLockedFile
+
+ \brief The QtLockedFile class extends QFile with advisory locking
+ functions.
+
+ A file may be locked in read or write mode. Multiple instances of
+ \e QtLockedFile, created in multiple processes running on the same
+ machine, may have a file locked in read mode. Exactly one instance
+ may have it locked in write mode. A read and a write lock cannot
+ exist simultaneously on the same file.
+
+ The file locks are advisory. This means that nothing prevents
+ another process from manipulating a locked file using QFile or
+ file system functions offered by the OS. Serialization is only
+ guaranteed if all processes that access the file use
+ QLockedFile. Also, while holding a lock on a file, a process
+ must not open the same file again (through any API), or locks
+ can be unexpectedly lost.
+
+ The lock provided by an instance of \e QtLockedFile is released
+ whenever the program terminates. This is true even when the
+ program crashes and no destructors are called.
+*/
+
+/*! \enum QtLockedFile::LockMode
+
+ This enum describes the available lock modes.
+
+ \value ReadLock A read lock.
+ \value WriteLock A write lock.
+ \value NoLock Neither a read lock nor a write lock.
+*/
+
+/*!
+ Constructs an unlocked \e QtLockedFile object. This constructor
+ behaves in the same way as \e QFile::QFile().
+
+ \sa QFile::QFile()
+*/
+LockedFile::LockedFile() : QFile()
+{
+#ifdef Q_OS_WIN
+ wmutex = 0;
+ rmutex = 0;
+#endif
+ m_lock_mode = NoLock;
+}
+
+/*!
+ Constructs an unlocked QtLockedFile object with file \a name. This
+ constructor behaves in the same way as \e QFile::QFile(const
+ QString&).
+
+ \sa QFile::QFile()
+*/
+LockedFile::LockedFile(const QString& name) : QFile(name)
+{
+#ifdef Q_OS_WIN
+ wmutex = 0;
+ rmutex = 0;
+#endif
+ m_lock_mode = NoLock;
+}
+
+/*!
+Opens the file in OpenMode \a mode.
+
+This is identical to QFile::open(), with the one exception that the
+Truncate mode flag is disallowed. Truncation would conflict with the
+advisory file locking, since the file would be modified before the
+write lock is obtained. If truncation is required, use resize(0)
+after obtaining the write lock.
+
+Returns true if successful; otherwise false.
+
+\sa QFile::open(), QFile::resize()
+*/
+bool LockedFile::open(OpenMode mode)
+{
+ if (mode & QIODevice::Truncate) {
+ qWarning("QtLockedFile::open(): Truncate mode not allowed.");
+ return false;
+ }
+ return QFile::open(mode);
+}
+
+/*!
+ Returns \e true if this object has a in read or write lock;
+ otherwise returns \e false.
+
+ \sa lockMode()
+*/
+bool LockedFile::isLocked() const
+{
+ return m_lock_mode != NoLock;
+}
+
+/*!
+ Returns the type of lock currently held by this object, or \e
+ QtLockedFile::NoLock.
+
+ \sa isLocked()
+*/
+LockedFile::LockMode LockedFile::lockMode() const
+{
+ return m_lock_mode;
+}
+
+/*!
+ \fn bool QtLockedFile::lock(LockMode mode, bool block = true)
+
+ Obtains a lock of type \a mode. The file must be opened before it
+ can be locked.
+
+ If \a block is true, this function will block until the lock is
+ aquired. If \a block is false, this function returns \e false
+ immediately if the lock cannot be aquired.
+
+ If this object already has a lock of type \a mode, this function
+ returns \e true immediately. If this object has a lock of a
+ different type than \a mode, the lock is first released and then a
+ new lock is obtained.
+
+ This function returns \e true if, after it executes, the file is
+ locked by this object, and \e false otherwise.
+
+ \sa unlock(), isLocked(), lockMode()
+*/
+
+/*!
+ \fn bool QtLockedFile::unlock()
+
+ Releases a lock.
+
+ If the object has no lock, this function returns immediately.
+
+ This function returns \e true if, after it executes, the file is
+ not locked by this object, and \e false otherwise.
+
+ \sa lock(), isLocked(), lockMode()
+*/
+
+/*!
+ \fn QtLockedFile::~QtLockedFile()
+
+ Destroys the \e QtLockedFile object. If any locks were held, they
+ are released.
+*/
diff --git a/meshmc/libraries/LocalPeer/src/LockedFile.h b/meshmc/libraries/LocalPeer/src/LockedFile.h
new file mode 100644
index 0000000000..661624885c
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/src/LockedFile.h
@@ -0,0 +1,76 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QFile>
+#ifdef Q_OS_WIN
+#include <QVector>
+#endif
+
+class LockedFile : public QFile
+{
+ public:
+ enum LockMode { NoLock = 0, ReadLock, WriteLock };
+
+ LockedFile();
+ LockedFile(const QString& name);
+ ~LockedFile();
+
+ bool open(OpenMode mode);
+
+ bool lock(LockMode mode, bool block = true);
+ bool unlock();
+ bool isLocked() const;
+ LockMode lockMode() const;
+
+ private:
+#ifdef Q_OS_WIN
+ Qt::HANDLE wmutex;
+ Qt::HANDLE rmutex;
+ QVector<Qt::HANDLE> rmutexes;
+ QString mutexname;
+
+ Qt::HANDLE getMutexHandle(int idx, bool doCreate);
+ bool waitMutex(Qt::HANDLE mutex, bool doBlock);
+#endif
+
+ LockMode m_lock_mode;
+};
diff --git a/meshmc/libraries/LocalPeer/src/LockedFile_unix.cpp b/meshmc/libraries/LocalPeer/src/LockedFile_unix.cpp
new file mode 100644
index 0000000000..10041c0f80
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/src/LockedFile_unix.cpp
@@ -0,0 +1,112 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "LockedFile.h"
+
+bool LockedFile::lock(LockMode mode, bool block)
+{
+ if (!isOpen()) {
+ qWarning("QtLockedFile::lock(): file is not opened");
+ return false;
+ }
+
+ if (mode == NoLock)
+ return unlock();
+
+ if (mode == m_lock_mode)
+ return true;
+
+ if (m_lock_mode != NoLock)
+ unlock();
+
+ struct flock fl;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+ fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK;
+ int cmd = block ? F_SETLKW : F_SETLK;
+ int ret = fcntl(handle(), cmd, &fl);
+
+ if (ret == -1) {
+ if (errno != EINTR && errno != EAGAIN)
+ qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno));
+ return false;
+ }
+
+ m_lock_mode = mode;
+ return true;
+}
+
+bool LockedFile::unlock()
+{
+ if (!isOpen()) {
+ qWarning("QtLockedFile::unlock(): file is not opened");
+ return false;
+ }
+
+ if (!isLocked())
+ return true;
+
+ struct flock fl;
+ fl.l_whence = SEEK_SET;
+ fl.l_start = 0;
+ fl.l_len = 0;
+ fl.l_type = F_UNLCK;
+ int ret = fcntl(handle(), F_SETLKW, &fl);
+
+ if (ret == -1) {
+ qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno));
+ return false;
+ }
+
+ m_lock_mode = NoLock;
+ return true;
+}
+
+LockedFile::~LockedFile()
+{
+ if (isOpen())
+ unlock();
+}
diff --git a/meshmc/libraries/LocalPeer/src/LockedFile_win.cpp b/meshmc/libraries/LocalPeer/src/LockedFile_win.cpp
new file mode 100644
index 0000000000..db29662b9e
--- /dev/null
+++ b/meshmc/libraries/LocalPeer/src/LockedFile_win.cpp
@@ -0,0 +1,204 @@
+/****************************************************************************
+** SPDX-License-Identifier: BSD-3-Clause
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Solutions component.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
+** of its contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "LockedFile.h"
+#include <qt_windows.h>
+#include <QFileInfo>
+
+#define MUTEX_PREFIX "QtLockedFile mutex "
+// Maximum number of concurrent read locks. Must not be greater than
+// MAXIMUM_WAIT_OBJECTS
+#define MAX_READERS MAXIMUM_WAIT_OBJECTS
+
+Qt::HANDLE LockedFile::getMutexHandle(int idx, bool doCreate)
+{
+ if (mutexname.isEmpty()) {
+ QFileInfo fi(*this);
+ mutexname =
+ QString::fromLatin1(MUTEX_PREFIX) + fi.absoluteFilePath().toLower();
+ }
+ QString mname(mutexname);
+ if (idx >= 0)
+ mname += QString::number(idx);
+
+ Qt::HANDLE mutex;
+ if (doCreate) {
+ mutex = CreateMutexW(NULL, FALSE, (LPCWSTR)mname.utf16());
+ if (!mutex) {
+ qErrnoWarning("QtLockedFile::lock(): CreateMutex failed");
+ return 0;
+ }
+ } else {
+ mutex = OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE,
+ (LPCWSTR)mname.utf16());
+ if (!mutex) {
+ if (GetLastError() != ERROR_FILE_NOT_FOUND)
+ qErrnoWarning("QtLockedFile::lock(): OpenMutex failed");
+ return 0;
+ }
+ }
+ return mutex;
+}
+
+bool LockedFile::waitMutex(Qt::HANDLE mutex, bool doBlock)
+{
+ Q_ASSERT(mutex);
+ DWORD res = WaitForSingleObject(mutex, doBlock ? INFINITE : 0);
+ switch (res) {
+ case WAIT_OBJECT_0:
+ case WAIT_ABANDONED:
+ return true;
+ break;
+ case WAIT_TIMEOUT:
+ break;
+ default:
+ qErrnoWarning("QtLockedFile::lock(): WaitForSingleObject failed");
+ }
+ return false;
+}
+
+bool LockedFile::lock(LockMode mode, bool block)
+{
+ if (!isOpen()) {
+ qWarning("QtLockedFile::lock(): file is not opened");
+ return false;
+ }
+
+ if (mode == NoLock)
+ return unlock();
+
+ if (mode == m_lock_mode)
+ return true;
+
+ if (m_lock_mode != NoLock)
+ unlock();
+
+ if (!wmutex && !(wmutex = getMutexHandle(-1, true)))
+ return false;
+
+ if (!waitMutex(wmutex, block))
+ return false;
+
+ if (mode == ReadLock) {
+ int idx = 0;
+ for (; idx < MAX_READERS; idx++) {
+ rmutex = getMutexHandle(idx, false);
+ if (!rmutex || waitMutex(rmutex, false))
+ break;
+ CloseHandle(rmutex);
+ }
+ bool ok = true;
+ if (idx >= MAX_READERS) {
+ qWarning("QtLockedFile::lock(): too many readers");
+ rmutex = 0;
+ ok = false;
+ } else if (!rmutex) {
+ rmutex = getMutexHandle(idx, true);
+ if (!rmutex || !waitMutex(rmutex, false))
+ ok = false;
+ }
+ if (!ok && rmutex) {
+ CloseHandle(rmutex);
+ rmutex = 0;
+ }
+ ReleaseMutex(wmutex);
+ if (!ok)
+ return false;
+ } else {
+ Q_ASSERT(rmutexes.isEmpty());
+ for (int i = 0; i < MAX_READERS; i++) {
+ Qt::HANDLE mutex = getMutexHandle(i, false);
+ if (mutex)
+ rmutexes.append(mutex);
+ }
+ if (rmutexes.size()) {
+ DWORD res =
+ WaitForMultipleObjects(rmutexes.size(), rmutexes.constData(),
+ TRUE, block ? INFINITE : 0);
+ if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED) {
+ if (res != WAIT_TIMEOUT)
+ qErrnoWarning(
+ "QtLockedFile::lock(): WaitForMultipleObjects failed");
+ m_lock_mode =
+ WriteLock; // trick unlock() to clean up - semiyucky
+ unlock();
+ return false;
+ }
+ }
+ }
+
+ m_lock_mode = mode;
+ return true;
+}
+
+bool LockedFile::unlock()
+{
+ if (!isOpen()) {
+ qWarning("QtLockedFile::unlock(): file is not opened");
+ return false;
+ }
+
+ if (!isLocked())
+ return true;
+
+ if (m_lock_mode == ReadLock) {
+ ReleaseMutex(rmutex);
+ CloseHandle(rmutex);
+ rmutex = 0;
+ } else {
+ foreach (Qt::HANDLE mutex, rmutexes) {
+ ReleaseMutex(mutex);
+ CloseHandle(mutex);
+ }
+ rmutexes.clear();
+ ReleaseMutex(wmutex);
+ }
+
+ m_lock_mode = LockedFile::NoLock;
+ return true;
+}
+
+LockedFile::~LockedFile()
+{
+ if (isOpen())
+ unlock();
+ if (wmutex)
+ CloseHandle(wmutex);
+}
diff --git a/meshmc/libraries/README.md b/meshmc/libraries/README.md
new file mode 100644
index 0000000000..0b79ccf653
--- /dev/null
+++ b/meshmc/libraries/README.md
@@ -0,0 +1,188 @@
+# Third-party libraries
+
+This folder has third-party or otherwise external libraries needed for other parts to work.
+
+## classparser
+A simplistic parser for Java class files.
+
+This library has served as a base for some (much more full-featured and advanced) work under NDA for AVG. It, however, should NOT be confused with that work.
+
+Copyright belongs to Petr Mrázek, unless explicitly stated otherwise in the source files. Available under the Apache 2.0 license.
+
+## ganalytics
+A Google Analytics library for Qt.
+
+BSD licensed, derived from [qt-google-analytics](https://github.com/HSAnet/qt-google-analytics).
+
+Modifications include better handling of IP anonymization (can be enabled) and general improvements of the API (application handles persistence and ID generation instead of the library).
+
+## hoedown
+Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté.
+
+See [github repo](https://github.com/hoedown/hoedown).
+
+## iconfix
+This was originally part of the razor-qt project and the Qt toolkit, respecitvely. Its sole purpose is to reimplement Qt's icon loading logic to prevent it from using any platform plugins that could break icon loading.
+
+Licensed under LGPL 2.1
+
+## javacheck
+Simple Java tool that prints the JVM details - version and platform bitness.
+
+Do what you want with it. It is so trivial that noone cares.
+
+## Katabasis
+Oauth2 library customized for Microsoft authentication.
+
+This is a fork of the [O2 library](https://github.com/pipacs/o2).
+
+MIT licensed.
+
+## launcher
+Java launcher part for Minecraft.
+
+It:
+* Starts a process
+* Waits for a launch script on stdin
+* Consumes the launch script you feed it
+* Proceeds with launch when it gets the `launcher` command
+
+This means the process is essentially idle until the final command is sent. You can, for example, attach a profiler before you send it.
+
+A `legacy` and `onesix` launchers are available.
+
+* `legacy` is intended for use with Minecraft versions < 1.6 and is deprecated.
+* `onesix` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title).
+
+Example (some parts have been censored):
+```
+mod legacyjavafixer-1.0
+mainClass net.minecraft.launchwrapper.Launch
+param --username
+param CENSORED
+param --version
+param MeshMC
+param --gameDir
+param /home/peterix/minecraft/FTB/17ForgeTest/minecraft
+param --assetsDir
+param /home/peterix/minecraft/mmc5/assets
+param --assetIndex
+param 1.7.10
+param --uuid
+param CENSORED
+param --accessToken
+param CENSORED
+param --userProperties
+param {}
+param --userType
+param mojang
+param --tweakClass
+param cpw.mods.fml.common.launcher.FMLTweaker
+windowTitle MeshMC: 172ForgeTest
+windowParams 854x480
+userName CENSORED
+sessionId token:CENSORED:CENSORED
+cp /home/peterix/minecraft/FTB/libraries/com/mojang/realms/1.3.5/realms-1.3.5.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar
+cp /home/peterix/minecraft/FTB/libraries/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar
+cp /home/peterix/minecraft/FTB/libraries/java3d/vecmath/1.3.1/vecmath-1.3.1.jar
+cp /home/peterix/minecraft/FTB/libraries/net/sf/trove4j/trove4j/3.0.3/trove4j-3.0.3.jar
+cp /home/peterix/minecraft/FTB/libraries/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar
+cp /home/peterix/minecraft/FTB/libraries/net/sf/jopt-simple/jopt-simple/4.5/jopt-simple-4.5.jar
+cp /home/peterix/minecraft/FTB/libraries/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar
+cp /home/peterix/minecraft/FTB/libraries/com/paulscode/codecwav/20101023/codecwav-20101023.jar
+cp /home/peterix/minecraft/FTB/libraries/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar
+cp /home/peterix/minecraft/FTB/libraries/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar
+cp /home/peterix/minecraft/FTB/libraries/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar
+cp /home/peterix/minecraft/FTB/libraries/io/netty/netty-all/4.0.10.Final/netty-all-4.0.10.Final.jar
+cp /home/peterix/minecraft/FTB/libraries/com/google/guava/guava/16.0/guava-16.0.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/commons/commons-lang3/3.2.1/commons-lang3-3.2.1.jar
+cp /home/peterix/minecraft/FTB/libraries/commons-io/commons-io/2.4/commons-io-2.4.jar
+cp /home/peterix/minecraft/FTB/libraries/commons-codec/commons-codec/1.9/commons-codec-1.9.jar
+cp /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar
+cp /home/peterix/minecraft/FTB/libraries/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar
+cp /home/peterix/minecraft/FTB/libraries/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar
+cp /home/peterix/minecraft/FTB/libraries/com/mojang/authlib/1.5.16/authlib-1.5.16.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar
+cp /home/peterix/minecraft/FTB/libraries/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar
+cp /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl/2.9.1/lwjgl-2.9.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl_util/2.9.1/lwjgl_util-2.9.1.jar
+cp /home/peterix/minecraft/FTB/libraries/tv/twitch/twitch/5.16/twitch-5.16.jar
+cp /home/peterix/minecraft/FTB/libraries/net/minecraftforge/forge/1.7.10-10.13.0.1178/forge-1.7.10-10.13.0.1178.jar
+cp /home/peterix/minecraft/FTB/libraries/net/minecraft/launchwrapper/1.9/launchwrapper-1.9.jar
+cp /home/peterix/minecraft/FTB/libraries/org/ow2/asm/asm-all/4.1/asm-all-4.1.jar
+cp /home/peterix/minecraft/FTB/libraries/com/typesafe/akka/akka-actor_2.11/2.3.3/akka-actor_2.11-2.3.3.jar
+cp /home/peterix/minecraft/FTB/libraries/com/typesafe/config/1.2.1/config-1.2.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-actors-migration_2.11/1.1.0/scala-actors-migration_2.11-1.1.0.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-compiler/2.11.1/scala-compiler-2.11.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/plugins/scala-continuations-library_2.11/1.0.2/scala-continuations-library_2.11-1.0.2.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/plugins/scala-continuations-plugin_2.11.1/1.0.2/scala-continuations-plugin_2.11.1-1.0.2.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-library/2.11.1/scala-library-2.11.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-parser-combinators_2.11/1.0.1/scala-parser-combinators_2.11-1.0.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-reflect/2.11.1/scala-reflect-2.11.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-swing_2.11/1.0.1/scala-swing_2.11-1.0.1.jar
+cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-xml_2.11/1.0.2/scala-xml_2.11-1.0.2.jar
+cp /home/peterix/minecraft/FTB/libraries/lzma/lzma/0.0.1/lzma-0.0.1.jar
+ext /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl-platform/2.9.1/lwjgl-platform-2.9.1-natives-linux.jar
+ext /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar
+natives /home/peterix/minecraft/FTB/17ForgeTest/natives
+cp /home/peterix/minecraft/FTB/versions/1.7.10/1.7.10.jar
+launcher onesix
+```
+
+Available under the Apache 2.0 license.
+
+## libnbtplusplus
+libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag (NBT). It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data.
+
+See [github repo](https://github.com/ljfa-ag/libnbtplusplus).
+
+Available either under LGPL version 3 or later.
+
+## LocalPeer
+Library for making only one instance of the application run at all times.
+
+BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication).
+
+Changes are made to make the code more generic and useful in less usual conditions.
+
+## optional-bare
+
+A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later.
+
+Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81
+
+Boost Software License - Version 1.0
+
+## quazip
+
+A zip manipulation library, forked for MeshMC's use.
+
+LGPL 2.1
+
+## rainbow
+Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring.
+
+Available either under LGPL version 2.1 or later.
+
+## systeminfo
+
+A MeshMC-specific library for probing system information.
+
+Apache 2.0
+
+## tomlc99
+
+A TOML language parser. Used by Forge 1.14+ to store mod metadata.
+
+See [github repo](https://github.com/cktan/tomlc99).
+
+Licenced under the MIT licence.
+
+## xz-embedded
+
+Tiny implementation of LZMA2 de/compression. This format is only used by Forge to save bandwidth.
+
+Public domain.
diff --git a/meshmc/libraries/classparser/CMakeLists.txt b/meshmc/libraries/classparser/CMakeLists.txt
new file mode 100644
index 0000000000..f4776902cf
--- /dev/null
+++ b/meshmc/libraries/classparser/CMakeLists.txt
@@ -0,0 +1,41 @@
+project(classparser)
+
+set(CMAKE_AUTOMOC ON)
+
+######## Check endianness ########
+include(TestBigEndian)
+test_big_endian(BIGENDIAN)
+if(${BIGENDIAN})
+ add_definitions(-DMULTIMC_BIG_ENDIAN)
+endif(${BIGENDIAN})
+
+# Find Qt
+find_package(Qt6Core REQUIRED)
+
+# Include Qt headers.
+include_directories(${Qt6Base_INCLUDE_DIRS})
+
+set(CLASSPARSER_HEADERS
+# Public headers
+include/classparser_config.h
+include/classparser.h
+
+# Private headers
+src/annotations.h
+src/classfile.h
+src/constants.h
+src/errors.h
+src/javaendian.h
+src/membuffer.h
+)
+
+set(CLASSPARSER_SOURCES
+src/classparser.cpp
+src/annotations.cpp
+)
+
+add_definitions(-DCLASSPARSER_LIBRARY)
+
+add_library(MeshMC_classparser STATIC ${CLASSPARSER_SOURCES} ${CLASSPARSER_HEADERS})
+target_include_directories(MeshMC_classparser PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
+target_link_libraries(MeshMC_classparser PRIVATE LibArchive::LibArchive Qt6::Core)
diff --git a/meshmc/libraries/classparser/include/classparser.h b/meshmc/libraries/classparser/include/classparser.h
new file mode 100644
index 0000000000..c39e4e800e
--- /dev/null
+++ b/meshmc/libraries/classparser/include/classparser.h
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+#include <QString>
+#include "classparser_config.h"
+
+namespace classparser
+{
+ /**
+ * @brief Get the version from a minecraft.jar by parsing its class files.
+ * Expensive!
+ */
+ QString GetMinecraftJarVersion(QString jar);
+} // namespace classparser
diff --git a/meshmc/libraries/classparser/include/classparser_config.h b/meshmc/libraries/classparser/include/classparser_config.h
new file mode 100644
index 0000000000..644c392143
--- /dev/null
+++ b/meshmc/libraries/classparser/include/classparser_config.h
@@ -0,0 +1,45 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QtCore/QtGlobal>
+
+#ifdef CLASSPARSER_LIBRARY
+#define CLASSPARSER_EXPORT Q_DECL_EXPORT
+#else
+#define CLASSPARSER_EXPORT Q_DECL_IMPORT
+#endif
diff --git a/meshmc/libraries/classparser/src/annotations.cpp b/meshmc/libraries/classparser/src/annotations.cpp
new file mode 100644
index 0000000000..93288a8f0b
--- /dev/null
+++ b/meshmc/libraries/classparser/src/annotations.cpp
@@ -0,0 +1,105 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "classfile.h"
+#include "annotations.h"
+#include <sstream>
+
+namespace java
+{
+ std::string annotation::toString()
+ {
+ std::ostringstream ss;
+ ss << "Annotation type : " << type_index << " - "
+ << pool[type_index].str_data << std::endl;
+ ss << "Contains " << name_val_pairs.size() << " pairs:" << std::endl;
+ for (unsigned i = 0; i < name_val_pairs.size(); i++) {
+ std::pair<uint16_t, element_value*>& val = name_val_pairs[i];
+ auto name_idx = val.first;
+ ss << pool[name_idx].str_data << "(" << name_idx << ")"
+ << " = " << val.second->toString() << std::endl;
+ }
+ return ss.str();
+ }
+
+ annotation* annotation::read(util::membuffer& input, constant_pool& pool)
+ {
+ uint16_t type_index = 0;
+ input.read_be(type_index);
+ annotation* ann = new annotation(type_index, pool);
+
+ uint16_t num_pairs = 0;
+ input.read_be(num_pairs);
+ while (num_pairs) {
+ uint16_t name_idx = 0;
+ // read name index
+ input.read_be(name_idx);
+ auto elem = element_value::readElementValue(input, pool);
+ // read value
+ ann->add_pair(name_idx, elem);
+ num_pairs--;
+ }
+ return ann;
+ }
+
+ element_value* element_value::readElementValue(util::membuffer& input,
+ java::constant_pool& pool)
+ {
+ element_value_type type = INVALID;
+ input.read(type);
+ uint16_t index = 0;
+ uint16_t index2 = 0;
+ std::vector<element_value*> vals;
+ switch (type) {
+ case PRIMITIVE_BYTE:
+ case PRIMITIVE_CHAR:
+ case PRIMITIVE_DOUBLE:
+ case PRIMITIVE_FLOAT:
+ case PRIMITIVE_INT:
+ case PRIMITIVE_LONG:
+ case PRIMITIVE_SHORT:
+ case PRIMITIVE_BOOLEAN:
+ case STRING:
+ input.read_be(index);
+ return new element_value_simple(type, index, pool);
+ case ENUM_CONSTANT:
+ input.read_be(index);
+ input.read_be(index2);
+ return new element_value_enum(type, index, index2, pool);
+ case CLASS: // Class
+ input.read_be(index);
+ return new element_value_class(type, index, pool);
+ case ANNOTATION: // Annotation
+ // FIXME: runtime visibility info needs to be passed from parent
+ return new element_value_annotation(
+ ANNOTATION, annotation::read(input, pool), pool);
+ case ARRAY: // Array
+ input.read_be(index);
+ for (int i = 0; i < index; i++) {
+ vals.push_back(
+ element_value::readElementValue(input, pool));
+ }
+ return new element_value_array(ARRAY, vals, pool);
+ default:
+ throw new java::classfile_exception();
+ }
+ }
+} // namespace java \ No newline at end of file
diff --git a/meshmc/libraries/classparser/src/annotations.h b/meshmc/libraries/classparser/src/annotations.h
new file mode 100644
index 0000000000..644d28c0d4
--- /dev/null
+++ b/meshmc/libraries/classparser/src/annotations.h
@@ -0,0 +1,296 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "classfile.h"
+#include <map>
+#include <vector>
+
+namespace java
+{
+ enum element_value_type : uint8_t {
+ INVALID = 0,
+ STRING = 's',
+ ENUM_CONSTANT = 'e',
+ CLASS = 'c',
+ ANNOTATION = '@',
+ ARRAY = '[', // one array dimension
+ PRIMITIVE_INT = 'I', // integer
+ PRIMITIVE_BYTE = 'B', // signed byte
+ PRIMITIVE_CHAR = 'C', // Unicode character code point in the Basic
+ // Multilingual Plane, encoded with UTF-16
+ PRIMITIVE_DOUBLE = 'D', // double-precision floating-point value
+ PRIMITIVE_FLOAT = 'F', // single-precision floating-point value
+ PRIMITIVE_LONG = 'J', // long integer
+ PRIMITIVE_SHORT = 'S', // signed short
+ PRIMITIVE_BOOLEAN = 'Z' // true or false
+ };
+ /**
+ * The element_value structure is a discriminated union representing the
+ * value of an element-value pair. It is used to represent element values in
+ * all attributes that describe annotations
+ * - RuntimeVisibleAnnotations
+ * - RuntimeInvisibleAnnotations
+ * - RuntimeVisibleParameterAnnotations
+ * - RuntimeInvisibleParameterAnnotations).
+ *
+ * The element_value structure has the following format:
+ */
+ class element_value
+ {
+ protected:
+ element_value_type type;
+ constant_pool& pool;
+
+ public:
+ element_value(element_value_type type, constant_pool& pool)
+ : type(type), pool(pool) {};
+ virtual ~element_value() {}
+
+ element_value_type getElementValueType()
+ {
+ return type;
+ }
+
+ virtual std::string toString() = 0;
+
+ static element_value* readElementValue(util::membuffer& input,
+ constant_pool& pool);
+ };
+
+ /**
+ * Each value of the annotations table represents a single runtime-visible
+ * annotation on a program element. The annotation structure has the
+ * following format:
+ */
+ class annotation
+ {
+ public:
+ typedef std::vector<std::pair<uint16_t, element_value*>> value_list;
+
+ protected:
+ /**
+ * The value of the type_index item must be a valid index into the
+ * constant_pool table. The constant_pool entry at that index must be a
+ * CONSTANT_Utf8_info (§4.4.7) structure representing a field descriptor
+ * representing the annotation type corresponding to the annotation
+ * represented by this annotation structure.
+ */
+ uint16_t type_index;
+ /**
+ * map between element_name_index and value.
+ *
+ * The value of the element_name_index item must be a valid index into
+ * the constant_pool table. The constant_pool entry at that index must
+ * be a CONSTANT_Utf8_info (§4.4.7) structure representing a valid field
+ * descriptor (§4.3.2) that denotes the name of the annotation type
+ * element represented by this element_value_pairs entry.
+ */
+ value_list name_val_pairs;
+ /**
+ * Reference to the parent constant pool
+ */
+ constant_pool& pool;
+
+ public:
+ annotation(uint16_t type_index, constant_pool& pool)
+ : type_index(type_index), pool(pool) {};
+ ~annotation()
+ {
+ for (unsigned i = 0; i < name_val_pairs.size(); i++) {
+ delete name_val_pairs[i].second;
+ }
+ }
+ void add_pair(uint16_t key, element_value* value)
+ {
+ name_val_pairs.push_back(std::make_pair(key, value));
+ };
+ value_list::const_iterator begin()
+ {
+ return name_val_pairs.cbegin();
+ }
+ value_list::const_iterator end()
+ {
+ return name_val_pairs.cend();
+ }
+ std::string toString();
+ static annotation* read(util::membuffer& input, constant_pool& pool);
+ };
+ typedef std::vector<annotation*> annotation_table;
+
+ /// type for simple value annotation elements
+ class element_value_simple : public element_value
+ {
+ protected:
+ /// index of the constant in the constant pool
+ uint16_t index;
+
+ public:
+ element_value_simple(element_value_type type, uint16_t index,
+ constant_pool& pool)
+ : element_value(type, pool), index(index) {
+ // TODO: verify consistency
+ };
+ uint16_t getIndex()
+ {
+ return index;
+ }
+ virtual std::string toString()
+ {
+ return pool[index].toString();
+ };
+ };
+ /// The enum_const_value item is used if the tag item is 'e'.
+ class element_value_enum : public element_value
+ {
+ protected:
+ /**
+ * The value of the type_name_index item must be a valid index into the
+ * constant_pool table. The constant_pool entry at that index must be a
+ * CONSTANT_Utf8_info (§4.4.7) structure representing a valid field
+ * descriptor (§4.3.2) that denotes the internal form of the binary name
+ * (§4.2.1) of the type of the enum constant represented by this
+ * element_value structure.
+ */
+ uint16_t typeIndex;
+ /**
+ * The value of the const_name_index item must be a valid index into the
+ * constant_pool table. The constant_pool entry at that index must be a
+ * CONSTANT_Utf8_info (§4.4.7) structure representing the simple name of
+ * the enum constant represented by this element_value structure.
+ */
+ uint16_t valueIndex;
+
+ public:
+ element_value_enum(element_value_type type, uint16_t typeIndex,
+ uint16_t valueIndex, constant_pool& pool)
+ : element_value(type, pool), typeIndex(typeIndex),
+ valueIndex(valueIndex)
+ {
+ // TODO: verify consistency
+ }
+ uint16_t getValueIndex()
+ {
+ return valueIndex;
+ }
+ uint16_t getTypeIndex()
+ {
+ return typeIndex;
+ }
+ virtual std::string toString()
+ {
+ return "enum value";
+ };
+ };
+
+ class element_value_class : public element_value
+ {
+ protected:
+ /**
+ * The class_info_index item must be a valid index into the
+ * constant_pool table. The constant_pool entry at that index must be a
+ * CONSTANT_Utf8_info (§4.4.7) structure representing the return
+ * descriptor (§4.3.3) of the type that is reified by the class
+ * represented by this element_value structure.
+ *
+ * For example, 'V' for Void.class, 'Ljava/lang/Object;' for Object,
+ * etc.
+ *
+ * Or in plain english, you can store type information in annotations.
+ * Yay.
+ */
+ uint16_t classIndex;
+
+ public:
+ element_value_class(element_value_type type, uint16_t classIndex,
+ constant_pool& pool)
+ : element_value(type, pool), classIndex(classIndex)
+ {
+ // TODO: verify consistency
+ }
+ uint16_t getIndex()
+ {
+ return classIndex;
+ }
+ virtual std::string toString()
+ {
+ return "class";
+ };
+ };
+
+ /// nested annotations... yay
+ class element_value_annotation : public element_value
+ {
+ private:
+ annotation* nestedAnnotation;
+
+ public:
+ element_value_annotation(element_value_type type,
+ annotation* nestedAnnotation,
+ constant_pool& pool)
+ : element_value(type, pool), nestedAnnotation(nestedAnnotation) {};
+ ~element_value_annotation()
+ {
+ if (nestedAnnotation) {
+ delete nestedAnnotation;
+ nestedAnnotation = nullptr;
+ }
+ }
+ virtual std::string toString()
+ {
+ return "nested annotation";
+ };
+ };
+
+ /// and arrays!
+ class element_value_array : public element_value
+ {
+ public:
+ typedef std::vector<element_value*> elem_vec;
+
+ protected:
+ elem_vec values;
+
+ public:
+ element_value_array(element_value_type type,
+ std::vector<element_value*>& values,
+ constant_pool& pool)
+ : element_value(type, pool), values(values) {};
+ ~element_value_array()
+ {
+ for (unsigned i = 0; i < values.size(); i++) {
+ delete values[i];
+ }
+ };
+ elem_vec::const_iterator begin()
+ {
+ return values.cbegin();
+ }
+ elem_vec::const_iterator end()
+ {
+ return values.cend();
+ }
+ virtual std::string toString()
+ {
+ return "array";
+ };
+ };
+} // namespace java \ No newline at end of file
diff --git a/meshmc/libraries/classparser/src/classfile.h b/meshmc/libraries/classparser/src/classfile.h
new file mode 100644
index 0000000000..0832c8039d
--- /dev/null
+++ b/meshmc/libraries/classparser/src/classfile.h
@@ -0,0 +1,169 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "membuffer.h"
+#include "constants.h"
+#include "annotations.h"
+#include <map>
+namespace java
+{
+ /**
+ * Class representing a Java .class file
+ */
+ class classfile : public util::membuffer
+ {
+ public:
+ classfile(char* data, std::size_t size) : membuffer(data, size)
+ {
+ valid = false;
+ is_synthetic = false;
+ read_be(magic);
+ if (magic != 0xCAFEBABE)
+ throw new classfile_exception();
+ read_be(minor_version);
+ read_be(major_version);
+ constants.load(*this);
+ read_be(access_flags);
+ read_be(this_class);
+ read_be(super_class);
+
+ // Interfaces
+ uint16_t iface_count = 0;
+ read_be(iface_count);
+ while (iface_count) {
+ uint16_t iface;
+ read_be(iface);
+ interfaces.push_back(iface);
+ iface_count--;
+ }
+
+ // Fields
+ // read fields (and attributes from inside fields) (and possible
+ // inner classes. yay for recursion!) for now though, we will ignore
+ // all attributes
+ /*
+ * field_info
+ * {
+ * u2 access_flags;
+ * u2 name_index;
+ * u2 descriptor_index;
+ * u2 attributes_count;
+ * attribute_info attributes[attributes_count];
+ * }
+ */
+ uint16_t field_count = 0;
+ read_be(field_count);
+ while (field_count) {
+ // skip field stuff
+ skip(6);
+ // and skip field attributes
+ uint16_t attr_count = 0;
+ read_be(attr_count);
+ while (attr_count) {
+ skip(2);
+ uint32_t attr_length = 0;
+ read_be(attr_length);
+ skip(attr_length);
+ attr_count--;
+ }
+ field_count--;
+ }
+
+ // class methods
+ /*
+ * method_info
+ * {
+ * u2 access_flags;
+ * u2 name_index;
+ * u2 descriptor_index;
+ * u2 attributes_count;
+ * attribute_info attributes[attributes_count];
+ * }
+ */
+ uint16_t method_count = 0;
+ read_be(method_count);
+ while (method_count) {
+ skip(6);
+ // and skip method attributes
+ uint16_t attr_count = 0;
+ read_be(attr_count);
+ while (attr_count) {
+ skip(2);
+ uint32_t attr_length = 0;
+ read_be(attr_length);
+ skip(attr_length);
+ attr_count--;
+ }
+ method_count--;
+ }
+
+ // class attributes
+ // there are many kinds of attributes. this is just the generic
+ // wrapper structure. type is decided by attribute name. extensions
+ // to the standard are *possible* class annotations are one kind of
+ // a attribute (one per class)
+ /*
+ * attribute_info
+ * {
+ * u2 attribute_name_index;
+ * u4 attribute_length;
+ * u1 info[attribute_length];
+ * }
+ */
+ uint16_t class_attr_count = 0;
+ read_be(class_attr_count);
+ while (class_attr_count) {
+ uint16_t name_idx = 0;
+ read_be(name_idx);
+ uint32_t attr_length = 0;
+ read_be(attr_length);
+
+ auto name = constants[name_idx];
+ if (name.str_data == "RuntimeVisibleAnnotations") {
+ uint16_t num_annotations = 0;
+ read_be(num_annotations);
+ while (num_annotations) {
+ visible_class_annotations.push_back(
+ annotation::read(*this, constants));
+ num_annotations--;
+ }
+ } else
+ skip(attr_length);
+ class_attr_count--;
+ }
+ valid = true;
+ };
+ bool valid;
+ bool is_synthetic;
+ uint32_t magic;
+ uint16_t minor_version;
+ uint16_t major_version;
+ constant_pool constants;
+ uint16_t access_flags;
+ uint16_t this_class;
+ uint16_t super_class;
+ // interfaces this class implements ? must be. investigate.
+ std::vector<uint16_t> interfaces;
+ // FIXME: doesn't free up memory on delete
+ java::annotation_table visible_class_annotations;
+ };
+} // namespace java \ No newline at end of file
diff --git a/meshmc/libraries/classparser/src/classparser.cpp b/meshmc/libraries/classparser/src/classparser.cpp
new file mode 100644
index 0000000000..2dd4bdfba2
--- /dev/null
+++ b/meshmc/libraries/classparser/src/classparser.cpp
@@ -0,0 +1,116 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "classfile.h"
+#include "classparser.h"
+
+#include <QFile>
+#include <archive.h>
+#include <archive_entry.h>
+#include <QDebug>
+
+namespace classparser
+{
+
+ QString GetMinecraftJarVersion(QString jarName)
+ {
+ QString version;
+
+ // check if minecraft.jar exists
+ QFile jar(jarName);
+ if (!jar.exists())
+ return version;
+
+ // open jar with libarchive
+ struct archive* a = archive_read_new();
+ archive_read_support_format_zip(a);
+ if (archive_read_open_filename(a, jarName.toUtf8().constData(),
+ 10240) != ARCHIVE_OK) {
+ archive_read_free(a);
+ return version;
+ }
+
+ // find and read Minecraft.class
+ QByteArray classData;
+ struct archive_entry* entry;
+ while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == "net/minecraft/client/Minecraft.class") {
+ la_int64_t sz = archive_entry_size(entry);
+ if (sz > 0) {
+ classData.resize(sz);
+ archive_read_data(a, classData.data(), sz);
+ } else {
+ char buf[8192];
+ la_ssize_t r;
+ while ((r = archive_read_data(a, buf, sizeof(buf))) > 0)
+ classData.append(buf, r);
+ }
+ break;
+ }
+ archive_read_data_skip(a);
+ }
+ archive_read_free(a);
+
+ if (classData.isEmpty())
+ return version;
+
+ // parse Minecraft.class
+ try {
+ char* temp = classData.data();
+ qint64 size = classData.size();
+ java::classfile MinecraftClass(temp, size);
+ java::constant_pool constants = MinecraftClass.constants;
+ for (java::constant_pool::container_type::const_iterator iter =
+ constants.begin();
+ iter != constants.end(); iter++) {
+ const java::constant& constant = *iter;
+ if (constant.type != java::constant_type_t::j_string_data)
+ continue;
+ const std::string& str = constant.str_data;
+ qDebug() << QString::fromStdString(str);
+ if (str.compare(0, 20, "Minecraft Minecraft ") == 0) {
+ version = str.substr(20).data();
+ break;
+ }
+ }
+ } catch (const java::classfile_exception&) {
+ }
+
+ return version;
+ }
+} // namespace classparser
diff --git a/meshmc/libraries/classparser/src/constants.h b/meshmc/libraries/classparser/src/constants.h
new file mode 100644
index 0000000000..251026fcfc
--- /dev/null
+++ b/meshmc/libraries/classparser/src/constants.h
@@ -0,0 +1,241 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "errors.h"
+#include <sstream>
+
+namespace java
+{
+ enum class constant_type_t : uint8_t {
+ j_hole = 0, // HACK: this is a hole in the array, because java is crazy
+ j_string_data = 1,
+ j_int = 3,
+ j_float = 4,
+ j_long = 5,
+ j_double = 6,
+ j_class = 7,
+ j_string = 8,
+ j_fieldref = 9,
+ j_methodref = 10,
+ j_interface_methodref = 11,
+ j_nameandtype = 12
+ // FIXME: missing some constant types, see
+ // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4
+ };
+
+ struct ref_type_t {
+ /**
+ * Class reference:
+ * an index within the constant pool to a UTF-8 string containing
+ * the fully qualified class name (in internal format)
+ * Used for j_class, j_fieldref, j_methodref and j_interface_methodref
+ */
+ uint16_t class_idx;
+ // used for j_fieldref, j_methodref and j_interface_methodref
+ uint16_t name_and_type_idx;
+ };
+
+ struct name_and_type_t {
+ uint16_t name_index;
+ uint16_t descriptor_index;
+ };
+
+ class constant
+ {
+ public:
+ constant_type_t type = constant_type_t::j_hole;
+
+ constant(util::membuffer& buf)
+ {
+ buf.read(type);
+
+ // load data depending on type
+ switch (type) {
+ case constant_type_t::j_float:
+ buf.read_be(data.int_data);
+ break;
+ case constant_type_t::j_int:
+ buf.read_be(data.int_data); // same as float data really
+ break;
+ case constant_type_t::j_double:
+ buf.read_be(data.long_data);
+ break;
+ case constant_type_t::j_long:
+ buf.read_be(data.long_data); // same as double
+ break;
+ case constant_type_t::j_class:
+ buf.read_be(data.ref_type.class_idx);
+ break;
+ case constant_type_t::j_fieldref:
+ case constant_type_t::j_methodref:
+ case constant_type_t::j_interface_methodref:
+ buf.read_be(data.ref_type.class_idx);
+ buf.read_be(data.ref_type.name_and_type_idx);
+ break;
+ case constant_type_t::j_string:
+ buf.read_be(data.index);
+ break;
+ case constant_type_t::j_string_data:
+ // HACK HACK: for now, we call these UTF-8 and do no further
+ // processing. Later, we should do some decoding. It's
+ // really modified UTF-8
+ // * U+0000 is represented as 0xC0,0x80 invalid character
+ // * any single zero byte ends the string
+ // * characters above U+10000 are encoded like in CESU-8
+ buf.read_jstr(str_data);
+ break;
+ case constant_type_t::j_nameandtype:
+ buf.read_be(data.name_and_type.name_index);
+ buf.read_be(data.name_and_type.descriptor_index);
+ break;
+ default:
+ // invalid constant type!
+ throw new classfile_exception();
+ }
+ }
+ constant(int) {}
+
+ std::string toString()
+ {
+ std::ostringstream ss;
+ switch (type) {
+ case constant_type_t::j_hole:
+ ss << "Fake legacy entry";
+ break;
+ case constant_type_t::j_float:
+ ss << "Float: " << data.float_data;
+ break;
+ case constant_type_t::j_double:
+ ss << "Double: " << data.double_data;
+ break;
+ case constant_type_t::j_int:
+ ss << "Int: " << data.int_data;
+ break;
+ case constant_type_t::j_long:
+ ss << "Long: " << data.long_data;
+ break;
+ case constant_type_t::j_string_data:
+ ss << "StrData: " << str_data;
+ break;
+ case constant_type_t::j_string:
+ ss << "Str: " << data.index;
+ break;
+ case constant_type_t::j_fieldref:
+ ss << "FieldRef: " << data.ref_type.class_idx << " "
+ << data.ref_type.name_and_type_idx;
+ break;
+ case constant_type_t::j_methodref:
+ ss << "MethodRef: " << data.ref_type.class_idx << " "
+ << data.ref_type.name_and_type_idx;
+ break;
+ case constant_type_t::j_interface_methodref:
+ ss << "IfMethodRef: " << data.ref_type.class_idx << " "
+ << data.ref_type.name_and_type_idx;
+ break;
+ case constant_type_t::j_class:
+ ss << "Class: " << data.ref_type.class_idx;
+ break;
+ case constant_type_t::j_nameandtype:
+ ss << "NameAndType: " << data.name_and_type.name_index
+ << " " << data.name_and_type.descriptor_index;
+ break;
+ default:
+ ss << "Invalid entry (" << int(type) << ")";
+ break;
+ }
+ return ss.str();
+ }
+
+ std::string str_data; /** String data in 'modified utf-8'.*/
+
+ // store everything here.
+ union {
+ int32_t int_data;
+ int64_t long_data;
+ float float_data;
+ double double_data;
+ uint16_t index;
+ ref_type_t ref_type;
+ name_and_type_t name_and_type;
+ } data = {0};
+ };
+
+ /**
+ * A helper class that represents the custom container used in Java class
+ * file for storage of constants
+ */
+ class constant_pool
+ {
+ public:
+ /**
+ * Create a pool of constants
+ */
+ constant_pool() {}
+ /**
+ * Load a java constant pool
+ */
+ void load(util::membuffer& buf)
+ {
+ // FIXME: @SANITY this should check for the end of buffer.
+ uint16_t length = 0;
+ buf.read_be(length);
+ length--;
+ const constant* last_constant = nullptr;
+ while (length) {
+ const constant& cnst = constant(buf);
+ constants.push_back(cnst);
+ last_constant = &constants[constants.size() - 1];
+ if (last_constant->type == constant_type_t::j_double ||
+ last_constant->type == constant_type_t::j_long) {
+ // push in a fake constant to preserve indexing
+ constants.push_back(constant(0));
+ length -= 2;
+ } else {
+ length--;
+ }
+ }
+ }
+ typedef std::vector<java::constant> container_type;
+ /**
+ * Access constants based on jar file index numbers (index of the first
+ * element is 1)
+ */
+ java::constant& operator[](std::size_t constant_index)
+ {
+ if (constant_index == 0 || constant_index > constants.size()) {
+ throw new classfile_exception();
+ }
+ return constants[constant_index - 1];
+ };
+ container_type::const_iterator begin() const
+ {
+ return constants.begin();
+ };
+ container_type::const_iterator end() const
+ {
+ return constants.end();
+ }
+
+ private:
+ container_type constants;
+ };
+} // namespace java
diff --git a/meshmc/libraries/classparser/src/errors.h b/meshmc/libraries/classparser/src/errors.h
new file mode 100644
index 0000000000..95a6aee575
--- /dev/null
+++ b/meshmc/libraries/classparser/src/errors.h
@@ -0,0 +1,29 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <exception>
+namespace java
+{
+ class classfile_exception : public std::exception
+ {
+ };
+} // namespace java
diff --git a/meshmc/libraries/classparser/src/javaendian.h b/meshmc/libraries/classparser/src/javaendian.h
new file mode 100644
index 0000000000..693df89964
--- /dev/null
+++ b/meshmc/libraries/classparser/src/javaendian.h
@@ -0,0 +1,84 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <stdint.h>
+
+/**
+ * Swap bytes between big endian and local number representation
+ */
+namespace util
+{
+#ifdef MULTIMC_BIG_ENDIAN
+ inline uint64_t bigswap(uint64_t x)
+ {
+ return x;
+ }
+
+ inline uint32_t bigswap(uint32_t x)
+ {
+ return x;
+ }
+
+ inline uint16_t bigswap(uint16_t x)
+ {
+ return x;
+ }
+
+#else
+ inline uint64_t bigswap(uint64_t x)
+ {
+ return (x >> 56) | ((x << 40) & 0x00FF000000000000) |
+ ((x << 24) & 0x0000FF0000000000) |
+ ((x << 8) & 0x000000FF00000000) |
+ ((x >> 8) & 0x00000000FF000000) |
+ ((x >> 24) & 0x0000000000FF0000) |
+ ((x >> 40) & 0x000000000000FF00) | (x << 56);
+ }
+
+ inline uint32_t bigswap(uint32_t x)
+ {
+ return (x >> 24) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) |
+ (x << 24);
+ }
+
+ inline uint16_t bigswap(uint16_t x)
+ {
+ return (x >> 8) | (x << 8);
+ }
+
+#endif
+
+ inline int64_t bigswap(int64_t x)
+ {
+ return static_cast<int64_t>(bigswap(static_cast<uint64_t>(x)));
+ }
+
+ inline int32_t bigswap(int32_t x)
+ {
+ return static_cast<int32_t>(bigswap(static_cast<uint32_t>(x)));
+ }
+
+ inline int16_t bigswap(int16_t x)
+ {
+ return static_cast<int16_t>(bigswap(static_cast<uint16_t>(x)));
+ }
+} // namespace util
diff --git a/meshmc/libraries/classparser/src/membuffer.h b/meshmc/libraries/classparser/src/membuffer.h
new file mode 100644
index 0000000000..bbfc1c3984
--- /dev/null
+++ b/meshmc/libraries/classparser/src/membuffer.h
@@ -0,0 +1,84 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <stdint.h>
+#include <string>
+#include <vector>
+#include <exception>
+#include "javaendian.h"
+
+namespace util
+{
+ class membuffer
+ {
+ public:
+ membuffer(char* buffer, std::size_t size)
+ {
+ current = start = buffer;
+ end = start + size;
+ }
+ ~membuffer()
+ {
+ // maybe? possibly? left out to avoid confusion. for now.
+ // delete start;
+ }
+ /**
+ * Read some value. That's all ;)
+ */
+ template <class T> void read(T& val)
+ {
+ val = *(T*)current;
+ current += sizeof(T);
+ }
+ /**
+ * Read a big-endian number
+ * valid for 2-byte, 4-byte and 8-byte variables
+ */
+ template <class T> void read_be(T& val)
+ {
+ val = util::bigswap(*(T*)current);
+ current += sizeof(T);
+ }
+ /**
+ * Read a string in the format:
+ * 2B length (big endian, unsigned)
+ * length bytes data
+ */
+ void read_jstr(std::string& str)
+ {
+ uint16_t length = 0;
+ read_be(length);
+ str.append(current, length);
+ current += length;
+ }
+ /**
+ * Skip N bytes
+ */
+ void skip(std::size_t N)
+ {
+ current += N;
+ }
+
+ private:
+ char *start, *end, *current;
+ };
+} // namespace util
diff --git a/meshmc/libraries/ganalytics/CMakeLists.txt b/meshmc/libraries/ganalytics/CMakeLists.txt
new file mode 100644
index 0000000000..a5c3125ddd
--- /dev/null
+++ b/meshmc/libraries/ganalytics/CMakeLists.txt
@@ -0,0 +1,17 @@
+project(ganalytics)
+
+find_package(Qt6Core)
+find_package(Qt6Gui)
+find_package(Qt6Network)
+
+set(ganalytics_SOURCES
+src/ganalytics.cpp
+src/ganalytics_worker.cpp
+src/ganalytics_worker.h
+include/ganalytics.h
+)
+
+add_library(ganalytics STATIC ${ganalytics_SOURCES})
+target_link_libraries(ganalytics Qt6::Core Qt6::Gui Qt6::Network)
+target_include_directories(ganalytics PUBLIC include)
+target_link_libraries(ganalytics systeminfo)
diff --git a/meshmc/libraries/ganalytics/LICENSE.txt b/meshmc/libraries/ganalytics/LICENSE.txt
new file mode 100644
index 0000000000..795497ffe6
--- /dev/null
+++ b/meshmc/libraries/ganalytics/LICENSE.txt
@@ -0,0 +1,24 @@
+Copyright (c) 2014-2015, University of Applied Sciences Augsburg
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the University of Applied Sciences Augsburg nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+OODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+UT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/meshmc/libraries/ganalytics/README.md b/meshmc/libraries/ganalytics/README.md
new file mode 100644
index 0000000000..d7e1e33c7d
--- /dev/null
+++ b/meshmc/libraries/ganalytics/README.md
@@ -0,0 +1,34 @@
+qt-google-analytics
+================
+
+Qt5 classes for providing google analytics usage in a Qt/QML application.
+
+## Building
+Include ```qt-google-analytics.pri``` in your .pro file.
+
+## Using
+Please make sure you have set your application information using ```QApplication::setApplicationName``` and ```QApplication::setApplicationVersion```.
+
+### In C++:
+```
+GAnalytics tracker("UA-my-id");
+tracker.sendScreenView("Main Screen");
+```
+
+### In QtQuick:
+Register the class on the C++ side using ```qmlRegisterType<GAnalytics>("analytics", 0, 1, "Tracker");```
+```
+Tracker {
+ id: tracker
+ trackingID: "UA-my-id"
+}
+
+[...]
+tracker.sendScreenView("Main Screen")
+```
+
+There is also an example application in the examples folder.
+
+## License
+Copyright (c) 2014-2016, University of Applied Sciences Augsburg.
+All rights reserved. Distributed under the terms and conditions of the BSD License. See separate LICENSE.txt.
diff --git a/meshmc/libraries/ganalytics/include/ganalytics.h b/meshmc/libraries/ganalytics/include/ganalytics.h
new file mode 100644
index 0000000000..8c6550a922
--- /dev/null
+++ b/meshmc/libraries/ganalytics/include/ganalytics.h
@@ -0,0 +1,92 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariantMap>
+
+class QNetworkAccessManager;
+class GAnalyticsWorker;
+
+class GAnalytics : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(LogLevel)
+
+ public:
+ explicit GAnalytics(const QString& trackingID, const QString& clientID,
+ const int version, QObject* parent = 0);
+ ~GAnalytics();
+
+ public:
+ enum LogLevel { Debug, Info, Error };
+
+ int version();
+
+ void setLogLevel(LogLevel logLevel);
+ LogLevel logLevel() const;
+
+ // Getter and Setters
+ void setViewportSize(const QString& viewportSize);
+ QString viewportSize() const;
+
+ void setLanguage(const QString& language);
+ QString language() const;
+
+ void setAnonymizeIPs(bool anonymize);
+ bool anonymizeIPs();
+
+ void setSendInterval(int milliseconds);
+ int sendInterval() const;
+
+ void enable(bool state = true);
+ bool isEnabled();
+
+ /// Get or set the network access manager. If none is set, the class creates
+ /// its own on the first request
+ void setNetworkAccessManager(QNetworkAccessManager* networkAccessManager);
+ QNetworkAccessManager* networkAccessManager() const;
+
+ public slots:
+ void sendScreenView(const QString& screenName,
+ const QVariantMap& customValues = QVariantMap());
+ void sendEvent(const QString& category, const QString& action,
+ const QString& label = QString(),
+ const QVariant& value = QVariant(),
+ const QVariantMap& customValues = QVariantMap());
+ void sendException(const QString& exceptionDescription,
+ bool exceptionFatal = true,
+ const QVariantMap& customValues = QVariantMap());
+ void startSession();
+ void endSession();
+
+ private:
+ GAnalyticsWorker* d;
+
+ friend QDataStream& operator<<(QDataStream& outStream,
+ const GAnalytics& analytics);
+ friend QDataStream& operator>>(QDataStream& inStream,
+ GAnalytics& analytics);
+};
+
+QDataStream& operator<<(QDataStream& outStream, const GAnalytics& analytics);
+QDataStream& operator>>(QDataStream& inStream, GAnalytics& analytics);
diff --git a/meshmc/libraries/ganalytics/src/ganalytics.cpp b/meshmc/libraries/ganalytics/src/ganalytics.cpp
new file mode 100644
index 0000000000..fe4b7b77b0
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics.cpp
@@ -0,0 +1,262 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ganalytics.h"
+#include "ganalytics_worker.h"
+#include "sys.h"
+
+#include <QDataStream>
+#include <QDebug>
+#include <QLocale>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QQueue>
+#include <QSettings>
+#include <QTimer>
+#include <QUrlQuery>
+#include <QUuid>
+
+GAnalytics::GAnalytics(const QString& trackingID, const QString& clientID,
+ const int version, QObject* parent)
+ : QObject(parent)
+{
+ d = new GAnalyticsWorker(this);
+ d->m_trackingID = trackingID;
+ d->m_clientID = clientID;
+ d->m_version = version;
+}
+
+/**
+ * Destructor of class GAnalytics.
+ */
+GAnalytics::~GAnalytics()
+{
+ delete d;
+}
+
+void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel)
+{
+ d->m_logLevel = logLevel;
+}
+
+GAnalytics::LogLevel GAnalytics::logLevel() const
+{
+ return d->m_logLevel;
+}
+
+// SETTER and GETTER
+void GAnalytics::setViewportSize(const QString& viewportSize)
+{
+ d->m_viewportSize = viewportSize;
+}
+
+QString GAnalytics::viewportSize() const
+{
+ return d->m_viewportSize;
+}
+
+void GAnalytics::setLanguage(const QString& language)
+{
+ d->m_language = language;
+}
+
+QString GAnalytics::language() const
+{
+ return d->m_language;
+}
+
+void GAnalytics::setAnonymizeIPs(bool anonymize)
+{
+ d->m_anonymizeIPs = anonymize;
+}
+
+bool GAnalytics::anonymizeIPs()
+{
+ return d->m_anonymizeIPs;
+}
+
+void GAnalytics::setSendInterval(int milliseconds)
+{
+ d->m_timer.setInterval(milliseconds);
+}
+
+int GAnalytics::sendInterval() const
+{
+ return (d->m_timer.interval());
+}
+
+bool GAnalytics::isEnabled()
+{
+ return d->m_isEnabled;
+}
+
+void GAnalytics::enable(bool state)
+{
+ d->enable(state);
+}
+
+int GAnalytics::version()
+{
+ return d->m_version;
+}
+
+void GAnalytics::setNetworkAccessManager(
+ QNetworkAccessManager* networkAccessManager)
+{
+ if (d->networkManager != networkAccessManager) {
+ // Delete the old network manager if it was our child
+ if (d->networkManager && d->networkManager->parent() == this) {
+ d->networkManager->deleteLater();
+ }
+
+ d->networkManager = networkAccessManager;
+ }
+}
+
+QNetworkAccessManager* GAnalytics::networkAccessManager() const
+{
+ return d->networkManager;
+}
+
+static void appendCustomValues(QUrlQuery& query,
+ const QVariantMap& customValues)
+{
+ for (QVariantMap::const_iterator iter = customValues.begin();
+ iter != customValues.end(); ++iter) {
+ query.addQueryItem(iter.key(), iter.value().toString());
+ }
+}
+
+/**
+ * Sent screen view is called when the user changed the applications view.
+ * These action of the user should be noticed and reported. Therefore
+ * a QUrlQuery is build in this method. It holts all the parameter for
+ * a http POST. The UrlQuery will be stored in a message Queue.
+ */
+void GAnalytics::sendScreenView(const QString& screenName,
+ const QVariantMap& customValues)
+{
+ d->logMessage(Info, QString("ScreenView: %1").arg(screenName));
+
+ QUrlQuery query = d->buildStandardPostQuery("screenview");
+ query.addQueryItem("cd", screenName);
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * This method is called whenever a button was pressed in the application.
+ * A query for a POST message will be created to report this event. The
+ * created query will be stored in a message queue.
+ */
+void GAnalytics::sendEvent(const QString& category, const QString& action,
+ const QString& label, const QVariant& value,
+ const QVariantMap& customValues)
+{
+ QUrlQuery query = d->buildStandardPostQuery("event");
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+ query.addQueryItem("ec", category);
+ query.addQueryItem("ea", action);
+ if (!label.isEmpty())
+ query.addQueryItem("el", label);
+ if (value.isValid())
+ query.addQueryItem("ev", value.toString());
+
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * Method is called after an exception was raised. It builds a
+ * query for a POST message. These query will be stored in a
+ * message queue.
+ */
+void GAnalytics::sendException(const QString& exceptionDescription,
+ bool exceptionFatal,
+ const QVariantMap& customValues)
+{
+ QUrlQuery query = d->buildStandardPostQuery("exception");
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+
+ query.addQueryItem("exd", exceptionDescription);
+
+ if (exceptionFatal) {
+ query.addQueryItem("exf", "1");
+ } else {
+ query.addQueryItem("exf", "0");
+ }
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * Session starts. This event will be sent by a POST message.
+ * Query is setup in this method and stored in the message
+ * queue.
+ */
+void GAnalytics::startSession()
+{
+ QVariantMap customValues;
+ customValues.insert("sc", "start");
+ sendEvent("Session", "Start", QString(), QVariant(), customValues);
+}
+
+/**
+ * Session ends. This event will be sent by a POST message.
+ * Query is setup in this method and stored in the message
+ * queue.
+ */
+void GAnalytics::endSession()
+{
+ QVariantMap customValues;
+ customValues.insert("sc", "end");
+ sendEvent("Session", "End", QString(), QVariant(), customValues);
+}
+
+/**
+ * Qut stream to persist class GAnalytics.
+ */
+QDataStream& operator<<(QDataStream& outStream, const GAnalytics& analytics)
+{
+ outStream << analytics.d->persistMessageQueue();
+
+ return outStream;
+}
+
+/**
+ * In stream to read GAnalytics from file.
+ */
+QDataStream& operator>>(QDataStream& inStream, GAnalytics& analytics)
+{
+ QList<QString> dataList;
+ inStream >> dataList;
+ analytics.d->readMessagesFromFile(dataList);
+
+ return inStream;
+}
diff --git a/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp b/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp
new file mode 100644
index 0000000000..115b39d5d4
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp
@@ -0,0 +1,267 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ganalytics.h"
+#include "ganalytics_worker.h"
+#include "sys.h"
+
+#include <QCoreApplication>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include <QGuiApplication>
+#include <QScreen>
+
+const QLatin1String
+ GAnalyticsWorker::dateTimeFormat("yyyy,MM,dd-hh:mm::ss:zzz");
+
+GAnalyticsWorker::GAnalyticsWorker(GAnalytics* parent)
+ : QObject(parent), q(parent), m_logLevel(GAnalytics::Error)
+{
+ m_appName = QCoreApplication::instance()->applicationName();
+ m_appVersion = QCoreApplication::instance()->applicationVersion();
+ m_request.setUrl(QUrl("https://www.google-analytics.com/collect"));
+ m_request.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+ m_request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
+
+ m_language = QLocale::system().name().toLower().replace("_", "-");
+ m_screenResolution = getScreenResolution();
+
+ m_timer.setInterval(m_timerInterval);
+ connect(&m_timer, &QTimer::timeout, this, &GAnalyticsWorker::postMessage);
+}
+
+void GAnalyticsWorker::enable(bool state)
+{
+ // state change to the same is not valid.
+ if (m_isEnabled == state) {
+ return;
+ }
+
+ m_isEnabled = state;
+ if (m_isEnabled) {
+ // enable -> start doing things :)
+ m_timer.start();
+ } else {
+ // disable -> stop the timer
+ m_timer.stop();
+ }
+}
+
+void GAnalyticsWorker::logMessage(GAnalytics::LogLevel level,
+ const QString& message)
+{
+ if (m_logLevel > level) {
+ return;
+ }
+
+ qDebug() << "[Analytics]" << message;
+}
+
+/**
+ * Build the POST query. Adds all parameter to the query
+ * which are used in every POST.
+ * @param type Type of POST message. The event which is to post.
+ * @return query Most used parameter in a query for a POST.
+ */
+QUrlQuery GAnalyticsWorker::buildStandardPostQuery(const QString& type)
+{
+ QUrlQuery query;
+ query.addQueryItem("v", "1");
+ query.addQueryItem("tid", m_trackingID);
+ query.addQueryItem("cid", m_clientID);
+ if (!m_userID.isEmpty()) {
+ query.addQueryItem("uid", m_userID);
+ }
+ query.addQueryItem("t", type);
+ query.addQueryItem("ul", m_language);
+ query.addQueryItem("vp", m_viewportSize);
+ query.addQueryItem("sr", m_screenResolution);
+ if (m_anonymizeIPs) {
+ query.addQueryItem("aip", "1");
+ }
+ return query;
+}
+
+/**
+ * Get primary screen resolution.
+ * @return A QString like "800x600".
+ */
+QString GAnalyticsWorker::getScreenResolution()
+{
+ QScreen* screen = QGuiApplication::primaryScreen();
+ QSize size = screen->size();
+
+ return QString("%1x%2").arg(size.width()).arg(size.height());
+}
+
+/**
+ * Try to gain information about the system where this application
+ * is running. It needs to get the name and version of the operating
+ * system, the language and screen resolution.
+ * All this information will be send in POST messages.
+ * @return agent A QString with all the information formatted for a POST
+ * message.
+ */
+QString GAnalyticsWorker::getUserAgent()
+{
+ return QString("%1/%2").arg(m_appName).arg(m_appVersion);
+}
+
+/**
+ * The message queue contains a list of QueryBuffer object.
+ * QueryBuffer holds a QUrlQuery object and a QDateTime object.
+ * These both object are freed from the buffer object and
+ * inserted as QString objects in a QList.
+ * @return dataList The list with concartinated queue data.
+ */
+QList<QString> GAnalyticsWorker::persistMessageQueue()
+{
+ QList<QString> dataList;
+ foreach (QueryBuffer buffer, m_messageQueue) {
+ dataList << buffer.postQuery.toString();
+ dataList << buffer.time.toString(dateTimeFormat);
+ }
+ return dataList;
+}
+
+/**
+ * Reads persistent messages from a file.
+ * Gets all message data as a QList<QString>.
+ * Two lines in the list build a QueryBuffer object.
+ */
+void GAnalyticsWorker::readMessagesFromFile(const QList<QString>& dataList)
+{
+ QListIterator<QString> iter(dataList);
+ while (iter.hasNext()) {
+ QString queryString = iter.next();
+ QString dateString = iter.next();
+ QUrlQuery query;
+ query.setQuery(queryString);
+ QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat);
+ QueryBuffer buffer;
+ buffer.postQuery = query;
+ buffer.time = dateTime;
+ m_messageQueue.enqueue(buffer);
+ }
+}
+
+/**
+ * Takes a QUrlQuery object and wrapp it together with
+ * a QTime object into a QueryBuffer struct. These struct
+ * will be stored in the message queue.
+ */
+void GAnalyticsWorker::enqueQueryWithCurrentTime(const QUrlQuery& query)
+{
+ QueryBuffer buffer;
+ buffer.postQuery = query;
+ buffer.time = QDateTime::currentDateTime();
+
+ m_messageQueue.enqueue(buffer);
+}
+
+/**
+ * This function is called by a timer interval.
+ * The function tries to send a messages from the queue.
+ * If message was successfully send then this function
+ * will be called back to send next message.
+ * If message queue contains more than one message then
+ * the connection will kept open.
+ * The message POST is asyncroniously when the server
+ * answered a signal will be emitted.
+ */
+void GAnalyticsWorker::postMessage()
+{
+ if (m_messageQueue.isEmpty()) {
+ // queue empty -> try sending later
+ m_timer.start();
+ return;
+ } else {
+ // queue has messages -> stop timer and start sending
+ m_timer.stop();
+ }
+
+ QString connection = "close";
+ if (m_messageQueue.count() > 1) {
+ connection = "keep-alive";
+ }
+
+ QueryBuffer buffer = m_messageQueue.head();
+ QDateTime sendTime = QDateTime::currentDateTime();
+ qint64 timeDiff = buffer.time.msecsTo(sendTime);
+
+ if (timeDiff > fourHours) {
+ // too old.
+ m_messageQueue.dequeue();
+ emit postMessage();
+ return;
+ }
+
+ buffer.postQuery.addQueryItem("qt", QString::number(timeDiff));
+ m_request.setRawHeader("Connection", connection.toUtf8());
+ m_request.setHeader(QNetworkRequest::ContentLengthHeader,
+ buffer.postQuery.toString().length());
+
+ logMessage(GAnalytics::Debug,
+ "Query string = " + buffer.postQuery.toString());
+
+ // Create a new network access manager if we don't have one yet
+ if (networkManager == NULL) {
+ networkManager = new QNetworkAccessManager(this);
+ }
+
+ QNetworkReply* reply = networkManager->post(
+ m_request, buffer.postQuery.query(QUrl::EncodeUnicode).toUtf8());
+ connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished()));
+}
+
+/**
+ * NetworkAccsessManager has finished to POST a message.
+ * If POST message was successfully send then the message
+ * query should be removed from queue.
+ * SIGNAL "postMessage" will be emitted to send next message
+ * if there is any.
+ * If message couldn't be send then next try is when the
+ * timer emits its signal.
+ */
+void GAnalyticsWorker::postMessageFinished()
+{
+ QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
+
+ int httpStausCode =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (httpStausCode < 200 || httpStausCode > 299) {
+ logMessage(
+ GAnalytics::Error,
+ QString("Error posting message: %1").arg(reply->errorString()));
+
+ // An error ocurred. Try sending later.
+ m_timer.start();
+ return;
+ } else {
+ logMessage(GAnalytics::Debug, "Message sent");
+ }
+
+ m_messageQueue.dequeue();
+ postMessage();
+ reply->deleteLater();
+}
diff --git a/meshmc/libraries/ganalytics/src/ganalytics_worker.h b/meshmc/libraries/ganalytics/src/ganalytics_worker.h
new file mode 100644
index 0000000000..8acace3759
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics_worker.h
@@ -0,0 +1,84 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QUrlQuery>
+#include <QDateTime>
+#include <QTimer>
+#include <QNetworkRequest>
+#include <QQueue>
+
+struct QueryBuffer {
+ QUrlQuery postQuery;
+ QDateTime time;
+};
+
+class GAnalyticsWorker : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit GAnalyticsWorker(GAnalytics* parent = 0);
+
+ GAnalytics* q;
+
+ QNetworkAccessManager* networkManager = nullptr;
+
+ QQueue<QueryBuffer> m_messageQueue;
+ QTimer m_timer;
+ QNetworkRequest m_request;
+ GAnalytics::LogLevel m_logLevel;
+
+ QString m_trackingID;
+ QString m_clientID;
+ QString m_userID;
+ QString m_appName;
+ QString m_appVersion;
+ QString m_language;
+ QString m_screenResolution;
+ QString m_viewportSize;
+
+ bool m_anonymizeIPs = false;
+ bool m_isEnabled = false;
+ int m_timerInterval = 30000;
+ int m_version = 0;
+
+ const static int fourHours = 4 * 60 * 60 * 1000;
+ const static QLatin1String dateTimeFormat;
+
+ public:
+ void logMessage(GAnalytics::LogLevel level, const QString& message);
+
+ QUrlQuery buildStandardPostQuery(const QString& type);
+ QString getScreenResolution();
+ QString getUserAgent();
+ QList<QString> persistMessageQueue();
+ void readMessagesFromFile(const QList<QString>& dataList);
+
+ void enqueQueryWithCurrentTime(const QUrlQuery& query);
+ void setIsSending(bool doSend);
+ void enable(bool state);
+
+ public slots:
+ void postMessage();
+ void postMessageFinished();
+};
diff --git a/meshmc/libraries/hoedown/CMakeLists.txt b/meshmc/libraries/hoedown/CMakeLists.txt
new file mode 100644
index 0000000000..7902e734de
--- /dev/null
+++ b/meshmc/libraries/hoedown/CMakeLists.txt
@@ -0,0 +1,26 @@
+# hoedown 3.0.2 - https://github.com/hoedown/hoedown/archive/3.0.2.tar.gz
+project(hoedown LANGUAGES C VERSION 3.0.2)
+
+set(HOEDOWN_SOURCES
+include/hoedown/autolink.h
+include/hoedown/buffer.h
+include/hoedown/document.h
+include/hoedown/escape.h
+include/hoedown/html.h
+include/hoedown/stack.h
+include/hoedown/version.h
+src/autolink.c
+src/buffer.c
+src/document.c
+src/escape.c
+src/html.c
+src/html_blocks.c
+src/html_smartypants.c
+src/stack.c
+src/version.c
+)
+
+# Include self.
+add_library(hoedown STATIC ${HOEDOWN_SOURCES})
+
+target_include_directories(hoedown PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
diff --git a/meshmc/libraries/hoedown/LICENSE b/meshmc/libraries/hoedown/LICENSE
new file mode 100644
index 0000000000..4e75de4dfe
--- /dev/null
+++ b/meshmc/libraries/hoedown/LICENSE
@@ -0,0 +1,15 @@
+Copyright (c) 2008, Natacha Porté
+Copyright (c) 2011, Vicent Martí
+Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/meshmc/libraries/hoedown/README.md b/meshmc/libraries/hoedown/README.md
new file mode 100644
index 0000000000..abe2b6ca08
--- /dev/null
+++ b/meshmc/libraries/hoedown/README.md
@@ -0,0 +1,9 @@
+Hoedown
+=======
+
+This is Hoedown 3.0.2, taken from [the hoedown github repo](https://github.com/hoedown/hoedown).
+
+`Hoedown` is a revived fork of [Sundown](https://github.com/vmg/sundown),
+the Markdown parser based on the original code of the
+[Upskirt library](http://fossil.instinctive.eu/libupskirt/index)
+by Natacha Porté.
diff --git a/meshmc/libraries/hoedown/include/hoedown/autolink.h b/meshmc/libraries/hoedown/include/hoedown/autolink.h
new file mode 100644
index 0000000000..04015ffc44
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/autolink.h
@@ -0,0 +1,65 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* autolink.h - versatile autolinker */
+
+#ifndef HOEDOWN_AUTOLINK_H
+#define HOEDOWN_AUTOLINK_H
+
+#include "buffer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*************
+ * CONSTANTS *
+ *************/
+
+typedef enum hoedown_autolink_flags {
+ HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0)
+} hoedown_autolink_flags;
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */
+int hoedown_autolink_is_safe(const uint8_t* data, size_t size);
+
+/* hoedown_autolink__www: search for the next www link in data */
+size_t hoedown_autolink__www(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t offset, size_t size,
+ hoedown_autolink_flags flags);
+
+/* hoedown_autolink__email: search for the next email in data */
+size_t hoedown_autolink__email(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t offset, size_t size,
+ hoedown_autolink_flags flags);
+
+/* hoedown_autolink__url: search for the next URL in data */
+size_t hoedown_autolink__url(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t offset, size_t size,
+ hoedown_autolink_flags flags);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_AUTOLINK_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/buffer.h b/meshmc/libraries/hoedown/include/hoedown/buffer.h
new file mode 100644
index 0000000000..df2d17acda
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/buffer.h
@@ -0,0 +1,153 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* buffer.h - simple, fast buffers */
+
+#ifndef HOEDOWN_BUFFER_H
+#define HOEDOWN_BUFFER_H
+
+#include <stdio.h>
+#include <stddef.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(_MSC_VER)
+#define __attribute__(x)
+#define inline __inline
+#define __builtin_expect(x, n) x
+#endif
+
+/*********
+ * TYPES *
+ *********/
+
+typedef void* (*hoedown_realloc_callback)(void*, size_t);
+typedef void (*hoedown_free_callback)(void*);
+
+struct hoedown_buffer {
+ uint8_t* data; /* actual character data */
+ size_t size; /* size of the string */
+ size_t asize; /* allocated size (0 = volatile buffer) */
+ size_t unit; /* reallocation unit size (0 = read-only buffer) */
+
+ hoedown_realloc_callback data_realloc;
+ hoedown_free_callback data_free;
+ hoedown_free_callback buffer_free;
+};
+
+typedef struct hoedown_buffer hoedown_buffer;
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* allocation wrappers */
+void* hoedown_malloc(size_t size) __attribute__((malloc));
+void* hoedown_calloc(size_t nmemb, size_t size) __attribute__((malloc));
+void* hoedown_realloc(void* ptr, size_t size) __attribute__((malloc));
+
+/* hoedown_buffer_init: initialize a buffer with custom allocators */
+void hoedown_buffer_init(hoedown_buffer* buffer, size_t unit,
+ hoedown_realloc_callback data_realloc,
+ hoedown_free_callback data_free,
+ hoedown_free_callback buffer_free);
+
+/* hoedown_buffer_uninit: uninitialize an existing buffer */
+void hoedown_buffer_uninit(hoedown_buffer* buf);
+
+/* hoedown_buffer_new: allocate a new buffer */
+hoedown_buffer* hoedown_buffer_new(size_t unit) __attribute__((malloc));
+
+/* hoedown_buffer_reset: free internal data of the buffer */
+void hoedown_buffer_reset(hoedown_buffer* buf);
+
+/* hoedown_buffer_grow: increase the allocated size to the given value */
+void hoedown_buffer_grow(hoedown_buffer* buf, size_t neosz);
+
+/* hoedown_buffer_put: append raw data to a buffer */
+void hoedown_buffer_put(hoedown_buffer* buf, const uint8_t* data, size_t size);
+
+/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */
+void hoedown_buffer_puts(hoedown_buffer* buf, const char* str);
+
+/* hoedown_buffer_putc: append a single char to a buffer */
+void hoedown_buffer_putc(hoedown_buffer* buf, uint8_t c);
+
+/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or
+ * error */
+int hoedown_buffer_putf(hoedown_buffer* buf, FILE* file);
+
+/* hoedown_buffer_set: replace the buffer's contents with raw data */
+void hoedown_buffer_set(hoedown_buffer* buf, const uint8_t* data, size_t size);
+
+/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated
+ * string */
+void hoedown_buffer_sets(hoedown_buffer* buf, const char* str);
+
+/* hoedown_buffer_eq: compare a buffer's data with other data for equality */
+int hoedown_buffer_eq(const hoedown_buffer* buf, const uint8_t* data,
+ size_t size);
+
+/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for
+ * equality */
+int hoedown_buffer_eqs(const hoedown_buffer* buf, const char* str);
+
+/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */
+int hoedown_buffer_prefix(const hoedown_buffer* buf, const char* prefix);
+
+/* hoedown_buffer_slurp: remove a given number of bytes from the head of the
+ * buffer */
+void hoedown_buffer_slurp(hoedown_buffer* buf, size_t size);
+
+/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string)
+ */
+const char* hoedown_buffer_cstr(hoedown_buffer* buf);
+
+/* hoedown_buffer_printf: formatted printing to a buffer */
+void hoedown_buffer_printf(hoedown_buffer* buf, const char* fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */
+void hoedown_buffer_put_utf8(hoedown_buffer* buf, unsigned int codepoint);
+
+/* hoedown_buffer_free: free the buffer */
+void hoedown_buffer_free(hoedown_buffer* buf);
+
+/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */
+#define HOEDOWN_BUFPUTSL(output, literal) \
+ hoedown_buffer_put(output, (const uint8_t*)literal, sizeof(literal) - 1)
+
+/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */
+#define HOEDOWN_BUFSETSL(output, literal) \
+ hoedown_buffer_set(output, (const uint8_t*)literal, sizeof(literal) - 1)
+
+/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */
+#define HOEDOWN_BUFEQSL(output, literal) \
+ hoedown_buffer_eq(output, (const uint8_t*)literal, sizeof(literal) - 1)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_BUFFER_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/document.h b/meshmc/libraries/hoedown/include/hoedown/document.h
new file mode 100644
index 0000000000..f88eef4f3e
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/document.h
@@ -0,0 +1,221 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* document.h - generic markdown parser */
+
+#ifndef HOEDOWN_DOCUMENT_H
+#define HOEDOWN_DOCUMENT_H
+
+#include "buffer.h"
+#include "autolink.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*************
+ * CONSTANTS *
+ *************/
+
+typedef enum hoedown_extensions {
+ /* block-level extensions */
+ HOEDOWN_EXT_TABLES = (1 << 0),
+ HOEDOWN_EXT_FENCED_CODE = (1 << 1),
+ HOEDOWN_EXT_FOOTNOTES = (1 << 2),
+
+ /* span-level extensions */
+ HOEDOWN_EXT_AUTOLINK = (1 << 3),
+ HOEDOWN_EXT_STRIKETHROUGH = (1 << 4),
+ HOEDOWN_EXT_UNDERLINE = (1 << 5),
+ HOEDOWN_EXT_HIGHLIGHT = (1 << 6),
+ HOEDOWN_EXT_QUOTE = (1 << 7),
+ HOEDOWN_EXT_SUPERSCRIPT = (1 << 8),
+ HOEDOWN_EXT_MATH = (1 << 9),
+
+ /* other flags */
+ HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11),
+ HOEDOWN_EXT_SPACE_HEADERS = (1 << 12),
+ HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13),
+
+ /* negative flags */
+ HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14)
+} hoedown_extensions;
+
+#define HOEDOWN_EXT_BLOCK \
+ (HOEDOWN_EXT_TABLES | HOEDOWN_EXT_FENCED_CODE | HOEDOWN_EXT_FOOTNOTES)
+
+#define HOEDOWN_EXT_SPAN \
+ (HOEDOWN_EXT_AUTOLINK | HOEDOWN_EXT_STRIKETHROUGH | \
+ HOEDOWN_EXT_UNDERLINE | HOEDOWN_EXT_HIGHLIGHT | HOEDOWN_EXT_QUOTE | \
+ HOEDOWN_EXT_SUPERSCRIPT | HOEDOWN_EXT_MATH)
+
+#define HOEDOWN_EXT_FLAGS \
+ (HOEDOWN_EXT_NO_INTRA_EMPHASIS | HOEDOWN_EXT_SPACE_HEADERS | \
+ HOEDOWN_EXT_MATH_EXPLICIT)
+
+#define HOEDOWN_EXT_NEGATIVE (HOEDOWN_EXT_DISABLE_INDENTED_CODE)
+
+typedef enum hoedown_list_flags {
+ HOEDOWN_LIST_ORDERED = (1 << 0),
+ HOEDOWN_LI_BLOCK = (1 << 1) /* <li> containing block data */
+} hoedown_list_flags;
+
+typedef enum hoedown_table_flags {
+ HOEDOWN_TABLE_ALIGN_LEFT = 1,
+ HOEDOWN_TABLE_ALIGN_RIGHT = 2,
+ HOEDOWN_TABLE_ALIGN_CENTER = 3,
+ HOEDOWN_TABLE_ALIGNMASK = 3,
+ HOEDOWN_TABLE_HEADER = 4
+} hoedown_table_flags;
+
+typedef enum hoedown_autolink_type {
+ HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/
+ HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */
+ HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */
+} hoedown_autolink_type;
+
+/*********
+ * TYPES *
+ *********/
+
+struct hoedown_document;
+typedef struct hoedown_document hoedown_document;
+
+struct hoedown_renderer_data {
+ void* opaque;
+};
+typedef struct hoedown_renderer_data hoedown_renderer_data;
+
+/* hoedown_renderer - functions for rendering parsed data */
+struct hoedown_renderer {
+ /* state object */
+ void* opaque;
+
+ /* block level callbacks - NULL skips the block */
+ void (*blockcode)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_buffer* lang,
+ const hoedown_renderer_data* data);
+ void (*blockquote)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*header)(hoedown_buffer* ob, const hoedown_buffer* content, int level,
+ const hoedown_renderer_data* data);
+ void (*hrule)(hoedown_buffer* ob, const hoedown_renderer_data* data);
+ void (*list)(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_list_flags flags, const hoedown_renderer_data* data);
+ void (*listitem)(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_list_flags flags,
+ const hoedown_renderer_data* data);
+ void (*paragraph)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*table)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*table_header)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*table_body)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*table_row)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*table_cell)(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_table_flags flags,
+ const hoedown_renderer_data* data);
+ void (*footnotes)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ void (*footnote_def)(hoedown_buffer* ob, const hoedown_buffer* content,
+ unsigned int num, const hoedown_renderer_data* data);
+ void (*blockhtml)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data);
+
+ /* span level callbacks - NULL or return 0 prints the span verbatim */
+ int (*autolink)(hoedown_buffer* ob, const hoedown_buffer* link,
+ hoedown_autolink_type type,
+ const hoedown_renderer_data* data);
+ int (*codespan)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data);
+ int (*double_emphasis)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*emphasis)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*underline)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*highlight)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*quote)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*image)(hoedown_buffer* ob, const hoedown_buffer* link,
+ const hoedown_buffer* title, const hoedown_buffer* alt,
+ const hoedown_renderer_data* data);
+ int (*linebreak)(hoedown_buffer* ob, const hoedown_renderer_data* data);
+ int (*link)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_buffer* link, const hoedown_buffer* title,
+ const hoedown_renderer_data* data);
+ int (*triple_emphasis)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*strikethrough)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*superscript)(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data);
+ int (*footnote_ref)(hoedown_buffer* ob, unsigned int num,
+ const hoedown_renderer_data* data);
+ int (*math)(hoedown_buffer* ob, const hoedown_buffer* text, int displaymode,
+ const hoedown_renderer_data* data);
+ int (*raw_html)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data);
+
+ /* low level callbacks - NULL copies input directly into the output */
+ void (*entity)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data);
+ void (*normal_text)(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data);
+
+ /* miscellaneous callbacks */
+ void (*doc_header)(hoedown_buffer* ob, int inline_render,
+ const hoedown_renderer_data* data);
+ void (*doc_footer)(hoedown_buffer* ob, int inline_render,
+ const hoedown_renderer_data* data);
+};
+typedef struct hoedown_renderer hoedown_renderer;
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_document_new: allocate a new document processor instance */
+hoedown_document* hoedown_document_new(const hoedown_renderer* renderer,
+ hoedown_extensions extensions,
+ size_t max_nesting)
+ __attribute__((malloc));
+
+/* hoedown_document_render: render regular Markdown using the document processor
+ */
+void hoedown_document_render(hoedown_document* doc, hoedown_buffer* ob,
+ const uint8_t* data, size_t size);
+
+/* hoedown_document_render_inline: render inline Markdown using the document
+ * processor */
+void hoedown_document_render_inline(hoedown_document* doc, hoedown_buffer* ob,
+ const uint8_t* data, size_t size);
+
+/* hoedown_document_free: deallocate a document processor instance */
+void hoedown_document_free(hoedown_document* doc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_DOCUMENT_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/escape.h b/meshmc/libraries/hoedown/include/hoedown/escape.h
new file mode 100644
index 0000000000..a7fefa2f7c
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/escape.h
@@ -0,0 +1,46 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* escape.h - escape utilities */
+
+#ifndef HOEDOWN_ESCAPE_H
+#define HOEDOWN_ESCAPE_H
+
+#include "buffer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_escape_href: escape (part of) a URL inside HTML */
+void hoedown_escape_href(hoedown_buffer* ob, const uint8_t* data, size_t size);
+
+/* hoedown_escape_html: escape HTML */
+void hoedown_escape_html(hoedown_buffer* ob, const uint8_t* data, size_t size,
+ int secure);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_ESCAPE_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/html.h b/meshmc/libraries/hoedown/include/hoedown/html.h
new file mode 100644
index 0000000000..f774a9f513
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/html.h
@@ -0,0 +1,102 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* html.h - HTML renderer and utilities */
+
+#ifndef HOEDOWN_HTML_H
+#define HOEDOWN_HTML_H
+
+#include "document.h"
+#include "buffer.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*************
+ * CONSTANTS *
+ *************/
+
+typedef enum hoedown_html_flags {
+ HOEDOWN_HTML_SKIP_HTML = (1 << 0),
+ HOEDOWN_HTML_ESCAPE = (1 << 1),
+ HOEDOWN_HTML_HARD_WRAP = (1 << 2),
+ HOEDOWN_HTML_USE_XHTML = (1 << 3)
+} hoedown_html_flags;
+
+typedef enum hoedown_html_tag {
+ HOEDOWN_HTML_TAG_NONE = 0,
+ HOEDOWN_HTML_TAG_OPEN,
+ HOEDOWN_HTML_TAG_CLOSE
+} hoedown_html_tag;
+
+/*********
+ * TYPES *
+ *********/
+
+struct hoedown_html_renderer_state {
+ void* opaque;
+
+ struct {
+ int header_count;
+ int current_level;
+ int level_offset;
+ int nesting_level;
+ } toc_data;
+
+ hoedown_html_flags flags;
+
+ /* extra callbacks */
+ void (*link_attributes)(hoedown_buffer* ob, const hoedown_buffer* url,
+ const hoedown_renderer_data* data);
+};
+typedef struct hoedown_html_renderer_state hoedown_html_renderer_state;
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart
+ * punctuation */
+void hoedown_html_smartypants(hoedown_buffer* ob, const uint8_t* data,
+ size_t size);
+
+/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the
+ * tag type or NONE */
+hoedown_html_tag hoedown_html_is_tag(const uint8_t* data, size_t size,
+ const char* tagname);
+
+/* hoedown_html_renderer_new: allocates a regular HTML renderer */
+hoedown_renderer* hoedown_html_renderer_new(hoedown_html_flags render_flags,
+ int nesting_level)
+ __attribute__((malloc));
+
+/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the
+ * returned renderer produces the Table of Contents */
+hoedown_renderer* hoedown_html_toc_renderer_new(int nesting_level)
+ __attribute__((malloc));
+
+/* hoedown_html_renderer_free: deallocate an HTML renderer */
+void hoedown_html_renderer_free(hoedown_renderer* renderer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_HTML_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/stack.h b/meshmc/libraries/hoedown/include/hoedown/stack.h
new file mode 100644
index 0000000000..fa1deccfa8
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/stack.h
@@ -0,0 +1,68 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* stack.h - simple stacking */
+
+#ifndef HOEDOWN_STACK_H
+#define HOEDOWN_STACK_H
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*********
+ * TYPES *
+ *********/
+
+struct hoedown_stack {
+ void** item;
+ size_t size;
+ size_t asize;
+};
+typedef struct hoedown_stack hoedown_stack;
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_stack_init: initialize a stack */
+void hoedown_stack_init(hoedown_stack* st, size_t initial_size);
+
+/* hoedown_stack_uninit: free internal data of the stack */
+void hoedown_stack_uninit(hoedown_stack* st);
+
+/* hoedown_stack_grow: increase the allocated size to the given value */
+void hoedown_stack_grow(hoedown_stack* st, size_t neosz);
+
+/* hoedown_stack_push: push an item to the top of the stack */
+void hoedown_stack_push(hoedown_stack* st, void* item);
+
+/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */
+void* hoedown_stack_pop(hoedown_stack* st);
+
+/* hoedown_stack_top: retrieve the item at the top of the stack */
+void* hoedown_stack_top(const hoedown_stack* st);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_STACK_H **/
diff --git a/meshmc/libraries/hoedown/include/hoedown/version.h b/meshmc/libraries/hoedown/include/hoedown/version.h
new file mode 100644
index 0000000000..e913bb4468
--- /dev/null
+++ b/meshmc/libraries/hoedown/include/hoedown/version.h
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *//* version.h - holds Hoedown's version */
+
+#ifndef HOEDOWN_VERSION_H
+#define HOEDOWN_VERSION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*************
+ * CONSTANTS *
+ *************/
+
+#define HOEDOWN_VERSION "3.0.2"
+#define HOEDOWN_VERSION_MAJOR 3
+#define HOEDOWN_VERSION_MINOR 0
+#define HOEDOWN_VERSION_REVISION 2
+
+/*************
+ * FUNCTIONS *
+ *************/
+
+/* hoedown_version: retrieve Hoedown's version numbers */
+void hoedown_version(int* major, int* minor, int* revision);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /** HOEDOWN_VERSION_H **/
diff --git a/meshmc/libraries/hoedown/src/autolink.c b/meshmc/libraries/hoedown/src/autolink.c
new file mode 100644
index 0000000000..ab8d64d601
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/autolink.c
@@ -0,0 +1,292 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/autolink.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#ifndef _MSC_VER
+#include <strings.h>
+#else
+#define strncasecmp _strnicmp
+#endif
+
+int hoedown_autolink_is_safe(const uint8_t* data, size_t size)
+{
+ static const size_t valid_uris_count = 6;
+ static const char* valid_uris[] = {"http://", "https://", "/",
+ "#", "ftp://", "mailto:"};
+ static const size_t valid_uris_size[] = {7, 8, 1, 1, 6, 7};
+ size_t i;
+
+ for (i = 0; i < valid_uris_count; ++i) {
+ size_t len = valid_uris_size[i];
+
+ if (size > len && strncasecmp((char*)data, valid_uris[i], len) == 0 &&
+ isalnum(data[len]))
+ return 1;
+ }
+
+ return 0;
+}
+
+static size_t autolink_delim(uint8_t* data, size_t link_end, size_t max_rewind,
+ size_t size)
+{
+ uint8_t cclose, copen = 0;
+ size_t i;
+
+ for (i = 0; i < link_end; ++i)
+ if (data[i] == '<') {
+ link_end = i;
+ break;
+ }
+
+ while (link_end > 0) {
+ if (strchr("?!.,:", data[link_end - 1]) != NULL)
+ link_end--;
+
+ else if (data[link_end - 1] == ';') {
+ size_t new_end = link_end - 2;
+
+ while (new_end > 0 && isalpha(data[new_end]))
+ new_end--;
+
+ if (new_end < link_end - 2 && data[new_end] == '&')
+ link_end = new_end;
+ else
+ link_end--;
+ } else
+ break;
+ }
+
+ if (link_end == 0)
+ return 0;
+
+ cclose = data[link_end - 1];
+
+ switch (cclose) {
+ case '"':
+ copen = '"';
+ break;
+ case '\'':
+ copen = '\'';
+ break;
+ case ')':
+ copen = '(';
+ break;
+ case ']':
+ copen = '[';
+ break;
+ case '}':
+ copen = '{';
+ break;
+ }
+
+ if (copen != 0) {
+ size_t closing = 0;
+ size_t opening = 0;
+ size_t i = 0;
+
+ /* Try to close the final punctuation sign in this same line;
+ * if we managed to close it outside of the URL, that means that it's
+ * not part of the URL. If it closes inside the URL, that means it
+ * is part of the URL.
+ *
+ * Examples:
+ *
+ * foo http://www.pokemon.com/Pikachu_(Electric) bar
+ * => http://www.pokemon.com/Pikachu_(Electric)
+ *
+ * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => http://www.pokemon.com/Pikachu_(Electric)
+ *
+ * foo http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => http://www.pokemon.com/Pikachu_(Electric))
+ *
+ * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
+ * => foo http://www.pokemon.com/Pikachu_(Electric)
+ */
+
+ while (i < link_end) {
+ if (data[i] == copen)
+ opening++;
+ else if (data[i] == cclose)
+ closing++;
+
+ i++;
+ }
+
+ if (closing != opening)
+ link_end--;
+ }
+
+ return link_end;
+}
+
+static size_t check_domain(uint8_t* data, size_t size, int allow_short)
+{
+ size_t i, np = 0;
+
+ if (!isalnum(data[0]))
+ return 0;
+
+ for (i = 1; i < size - 1; ++i) {
+ if (strchr(".:", data[i]) != NULL)
+ np++;
+ else if (!isalnum(data[i]) && data[i] != '-')
+ break;
+ }
+
+ if (allow_short) {
+ /* We don't need a valid domain in the strict sense (with
+ * least one dot; so just make sure it's composed of valid
+ * domain characters and return the length of the the valid
+ * sequence. */
+ return i;
+ } else {
+ /* a valid domain needs to have at least a dot.
+ * that's as far as we get */
+ return np ? i : 0;
+ }
+}
+
+size_t hoedown_autolink__www(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t max_rewind, size_t size,
+ unsigned int flags)
+{
+ size_t link_end;
+
+ if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
+ return 0;
+
+ if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
+ return 0;
+
+ link_end = check_domain(data, size, 0);
+
+ if (link_end == 0)
+ return 0;
+
+ while (link_end < size && !isspace(data[link_end]))
+ link_end++;
+
+ link_end = autolink_delim(data, link_end, max_rewind, size);
+
+ if (link_end == 0)
+ return 0;
+
+ hoedown_buffer_put(link, data, link_end);
+ *rewind_p = 0;
+
+ return (int)link_end;
+}
+
+size_t hoedown_autolink__email(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t max_rewind, size_t size,
+ unsigned int flags)
+{
+ size_t link_end, rewind;
+ int nb = 0, np = 0;
+
+ for (rewind = 0; rewind < max_rewind; ++rewind) {
+ uint8_t c = data[-1 - rewind];
+
+ if (isalnum(c))
+ continue;
+
+ if (strchr(".+-_", c) != NULL)
+ continue;
+
+ break;
+ }
+
+ if (rewind == 0)
+ return 0;
+
+ for (link_end = 0; link_end < size; ++link_end) {
+ uint8_t c = data[link_end];
+
+ if (isalnum(c))
+ continue;
+
+ if (c == '@')
+ nb++;
+ else if (c == '.' && link_end < size - 1)
+ np++;
+ else if (c != '-' && c != '_')
+ break;
+ }
+
+ if (link_end < 2 || nb != 1 || np == 0 || !isalpha(data[link_end - 1]))
+ return 0;
+
+ link_end = autolink_delim(data, link_end, max_rewind, size);
+
+ if (link_end == 0)
+ return 0;
+
+ hoedown_buffer_put(link, data - rewind, link_end + rewind);
+ *rewind_p = rewind;
+
+ return link_end;
+}
+
+size_t hoedown_autolink__url(size_t* rewind_p, hoedown_buffer* link,
+ uint8_t* data, size_t max_rewind, size_t size,
+ unsigned int flags)
+{
+ size_t link_end, rewind = 0, domain_len;
+
+ if (size < 4 || data[1] != '/' || data[2] != '/')
+ return 0;
+
+ while (rewind < max_rewind && isalpha(data[-1 - rewind]))
+ rewind++;
+
+ if (!hoedown_autolink_is_safe(data - rewind, size + rewind))
+ return 0;
+
+ link_end = strlen("://");
+
+ domain_len = check_domain(data + link_end, size - link_end,
+ flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS);
+
+ if (domain_len == 0)
+ return 0;
+
+ link_end += domain_len;
+ while (link_end < size && !isspace(data[link_end]))
+ link_end++;
+
+ link_end = autolink_delim(data, link_end, max_rewind, size);
+
+ if (link_end == 0)
+ return 0;
+
+ hoedown_buffer_put(link, data - rewind, link_end + rewind);
+ *rewind_p = rewind;
+
+ return link_end;
+}
diff --git a/meshmc/libraries/hoedown/src/buffer.c b/meshmc/libraries/hoedown/src/buffer.c
new file mode 100644
index 0000000000..b3559eb09f
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/buffer.c
@@ -0,0 +1,307 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/buffer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+void* hoedown_malloc(size_t size)
+{
+ void* ret = malloc(size);
+
+ if (!ret) {
+ fprintf(stderr, "Allocation failed.\n");
+ abort();
+ }
+
+ return ret;
+}
+
+void* hoedown_calloc(size_t nmemb, size_t size)
+{
+ void* ret = calloc(nmemb, size);
+
+ if (!ret) {
+ fprintf(stderr, "Allocation failed.\n");
+ abort();
+ }
+
+ return ret;
+}
+
+void* hoedown_realloc(void* ptr, size_t size)
+{
+ void* ret = realloc(ptr, size);
+
+ if (!ret) {
+ fprintf(stderr, "Allocation failed.\n");
+ abort();
+ }
+
+ return ret;
+}
+
+void hoedown_buffer_init(hoedown_buffer* buf, size_t unit,
+ hoedown_realloc_callback data_realloc,
+ hoedown_free_callback data_free,
+ hoedown_free_callback buffer_free)
+{
+ assert(buf);
+
+ buf->data = NULL;
+ buf->size = buf->asize = 0;
+ buf->unit = unit;
+ buf->data_realloc = data_realloc;
+ buf->data_free = data_free;
+ buf->buffer_free = buffer_free;
+}
+
+void hoedown_buffer_uninit(hoedown_buffer* buf)
+{
+ assert(buf && buf->unit);
+ buf->data_free(buf->data);
+}
+
+hoedown_buffer* hoedown_buffer_new(size_t unit)
+{
+ hoedown_buffer* ret = hoedown_malloc(sizeof(hoedown_buffer));
+ hoedown_buffer_init(ret, unit, hoedown_realloc, free, free);
+ return ret;
+}
+
+void hoedown_buffer_free(hoedown_buffer* buf)
+{
+ if (!buf)
+ return;
+ assert(buf && buf->unit);
+
+ buf->data_free(buf->data);
+
+ if (buf->buffer_free)
+ buf->buffer_free(buf);
+}
+
+void hoedown_buffer_reset(hoedown_buffer* buf)
+{
+ assert(buf && buf->unit);
+
+ buf->data_free(buf->data);
+ buf->data = NULL;
+ buf->size = buf->asize = 0;
+}
+
+void hoedown_buffer_grow(hoedown_buffer* buf, size_t neosz)
+{
+ size_t neoasz;
+ assert(buf && buf->unit);
+
+ if (buf->asize >= neosz)
+ return;
+
+ neoasz = buf->asize + buf->unit;
+ while (neoasz < neosz)
+ neoasz += buf->unit;
+
+ buf->data = (uint8_t*)buf->data_realloc(buf->data, neoasz);
+ buf->asize = neoasz;
+}
+
+void hoedown_buffer_put(hoedown_buffer* buf, const uint8_t* data, size_t size)
+{
+ assert(buf && buf->unit);
+
+ if (buf->size + size > buf->asize)
+ hoedown_buffer_grow(buf, buf->size + size);
+
+ memcpy(buf->data + buf->size, data, size);
+ buf->size += size;
+}
+
+void hoedown_buffer_puts(hoedown_buffer* buf, const char* str)
+{
+ hoedown_buffer_put(buf, (const uint8_t*)str, strlen(str));
+}
+
+void hoedown_buffer_putc(hoedown_buffer* buf, uint8_t c)
+{
+ assert(buf && buf->unit);
+
+ if (buf->size >= buf->asize)
+ hoedown_buffer_grow(buf, buf->size + 1);
+
+ buf->data[buf->size] = c;
+ buf->size += 1;
+}
+
+int hoedown_buffer_putf(hoedown_buffer* buf, FILE* file)
+{
+ assert(buf && buf->unit);
+
+ while (!(feof(file) || ferror(file))) {
+ hoedown_buffer_grow(buf, buf->size + buf->unit);
+ buf->size += fread(buf->data + buf->size, 1, buf->unit, file);
+ }
+
+ return ferror(file);
+}
+
+void hoedown_buffer_set(hoedown_buffer* buf, const uint8_t* data, size_t size)
+{
+ assert(buf && buf->unit);
+
+ if (size > buf->asize)
+ hoedown_buffer_grow(buf, size);
+
+ memcpy(buf->data, data, size);
+ buf->size = size;
+}
+
+void hoedown_buffer_sets(hoedown_buffer* buf, const char* str)
+{
+ hoedown_buffer_set(buf, (const uint8_t*)str, strlen(str));
+}
+
+int hoedown_buffer_eq(const hoedown_buffer* buf, const uint8_t* data,
+ size_t size)
+{
+ if (buf->size != size)
+ return 0;
+ return memcmp(buf->data, data, size) == 0;
+}
+
+int hoedown_buffer_eqs(const hoedown_buffer* buf, const char* str)
+{
+ return hoedown_buffer_eq(buf, (const uint8_t*)str, strlen(str));
+}
+
+int hoedown_buffer_prefix(const hoedown_buffer* buf, const char* prefix)
+{
+ size_t i;
+
+ for (i = 0; i < buf->size; ++i) {
+ if (prefix[i] == 0)
+ return 0;
+
+ if (buf->data[i] != prefix[i])
+ return buf->data[i] - prefix[i];
+ }
+
+ return 0;
+}
+
+void hoedown_buffer_slurp(hoedown_buffer* buf, size_t size)
+{
+ assert(buf && buf->unit);
+
+ if (size >= buf->size) {
+ buf->size = 0;
+ return;
+ }
+
+ buf->size -= size;
+ memmove(buf->data, buf->data + size, buf->size);
+}
+
+const char* hoedown_buffer_cstr(hoedown_buffer* buf)
+{
+ assert(buf && buf->unit);
+
+ if (buf->size < buf->asize && buf->data[buf->size] == 0)
+ return (char*)buf->data;
+
+ hoedown_buffer_grow(buf, buf->size + 1);
+ buf->data[buf->size] = 0;
+
+ return (char*)buf->data;
+}
+
+void hoedown_buffer_printf(hoedown_buffer* buf, const char* fmt, ...)
+{
+ va_list ap;
+ int n;
+
+ assert(buf && buf->unit);
+
+ if (buf->size >= buf->asize)
+ hoedown_buffer_grow(buf, buf->size + 1);
+
+ va_start(ap, fmt);
+ n = vsnprintf((char*)buf->data + buf->size, buf->asize - buf->size, fmt,
+ ap);
+ va_end(ap);
+
+ if (n < 0) {
+#ifndef _MSC_VER
+ return;
+#else
+ va_start(ap, fmt);
+ n = _vscprintf(fmt, ap);
+ va_end(ap);
+#endif
+ }
+
+ if ((size_t)n >= buf->asize - buf->size) {
+ hoedown_buffer_grow(buf, buf->size + n + 1);
+
+ va_start(ap, fmt);
+ n = vsnprintf((char*)buf->data + buf->size, buf->asize - buf->size, fmt,
+ ap);
+ va_end(ap);
+ }
+
+ if (n < 0)
+ return;
+
+ buf->size += n;
+}
+
+void hoedown_buffer_put_utf8(hoedown_buffer* buf, unsigned int c)
+{
+ unsigned char unichar[4];
+
+ assert(buf && buf->unit);
+
+ if (c < 0x80) {
+ hoedown_buffer_putc(buf, c);
+ } else if (c < 0x800) {
+ unichar[0] = 192 + (c / 64);
+ unichar[1] = 128 + (c % 64);
+ hoedown_buffer_put(buf, unichar, 2);
+ } else if (c - 0xd800u < 0x800) {
+ HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
+ } else if (c < 0x10000) {
+ unichar[0] = 224 + (c / 4096);
+ unichar[1] = 128 + (c / 64) % 64;
+ unichar[2] = 128 + (c % 64);
+ hoedown_buffer_put(buf, unichar, 3);
+ } else if (c < 0x110000) {
+ unichar[0] = 240 + (c / 262144);
+ unichar[1] = 128 + (c / 4096) % 64;
+ unichar[2] = 128 + (c / 64) % 64;
+ unichar[3] = 128 + (c % 64);
+ hoedown_buffer_put(buf, unichar, 4);
+ } else {
+ HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
+ }
+}
diff --git a/meshmc/libraries/hoedown/src/document.c b/meshmc/libraries/hoedown/src/document.c
new file mode 100644
index 0000000000..31819a7c98
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/document.c
@@ -0,0 +1,3102 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/document.h"
+
+#include <assert.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdio.h>
+
+#include "hoedown/stack.h"
+
+#ifndef _MSC_VER
+#include <strings.h>
+#else
+#define strncasecmp _strnicmp
+#endif
+
+#define REF_TABLE_SIZE 8
+
+#define BUFFER_BLOCK 0
+#define BUFFER_SPAN 1
+
+#define HOEDOWN_LI_END 8 /* internal list flag */
+
+const char* hoedown_find_block_tag(const char* str, unsigned int len);
+
+/***************
+ * LOCAL TYPES *
+ ***************/
+
+/* link_ref: reference to a link */
+struct link_ref {
+ unsigned int id;
+
+ hoedown_buffer* link;
+ hoedown_buffer* title;
+
+ struct link_ref* next;
+};
+
+/* footnote_ref: reference to a footnote */
+struct footnote_ref {
+ unsigned int id;
+
+ int is_used;
+ unsigned int num;
+
+ hoedown_buffer* contents;
+};
+
+/* footnote_item: an item in a footnote_list */
+struct footnote_item {
+ struct footnote_ref* ref;
+ struct footnote_item* next;
+};
+
+/* footnote_list: linked list of footnote_item */
+struct footnote_list {
+ unsigned int count;
+ struct footnote_item* head;
+ struct footnote_item* tail;
+};
+
+/* char_trigger: function pointer to render active chars */
+/* returns the number of chars taken care of */
+/* data is the pointer of the beginning of the span */
+/* offset is the number of valid chars before data */
+typedef size_t (*char_trigger)(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+
+static size_t char_emphasis(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_quote(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_linebreak(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_codespan(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_escape(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_entity(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_langle_tag(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_autolink_url(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_autolink_email(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_autolink_www(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_link(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_superscript(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+static size_t char_math(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size);
+
+enum markdown_char_t {
+ MD_CHAR_NONE = 0,
+ MD_CHAR_EMPHASIS,
+ MD_CHAR_CODESPAN,
+ MD_CHAR_LINEBREAK,
+ MD_CHAR_LINK,
+ MD_CHAR_LANGLE,
+ MD_CHAR_ESCAPE,
+ MD_CHAR_ENTITY,
+ MD_CHAR_AUTOLINK_URL,
+ MD_CHAR_AUTOLINK_EMAIL,
+ MD_CHAR_AUTOLINK_WWW,
+ MD_CHAR_SUPERSCRIPT,
+ MD_CHAR_QUOTE,
+ MD_CHAR_MATH
+};
+
+static char_trigger markdown_char_ptrs[] = {NULL,
+ &char_emphasis,
+ &char_codespan,
+ &char_linebreak,
+ &char_link,
+ &char_langle_tag,
+ &char_escape,
+ &char_entity,
+ &char_autolink_url,
+ &char_autolink_email,
+ &char_autolink_www,
+ &char_superscript,
+ &char_quote,
+ &char_math};
+
+struct hoedown_document {
+ hoedown_renderer md;
+ hoedown_renderer_data data;
+
+ struct link_ref* refs[REF_TABLE_SIZE];
+ struct footnote_list footnotes_found;
+ struct footnote_list footnotes_used;
+ uint8_t active_char[256];
+ hoedown_stack work_bufs[2];
+ hoedown_extensions ext_flags;
+ size_t max_nesting;
+ int in_link_body;
+};
+
+/***************************
+ * HELPER FUNCTIONS *
+ ***************************/
+
+static hoedown_buffer* newbuf(hoedown_document* doc, int type)
+{
+ static const size_t buf_size[2] = {256, 64};
+ hoedown_buffer* work = NULL;
+ hoedown_stack* pool = &doc->work_bufs[type];
+
+ if (pool->size < pool->asize && pool->item[pool->size] != NULL) {
+ work = pool->item[pool->size++];
+ work->size = 0;
+ } else {
+ work = hoedown_buffer_new(buf_size[type]);
+ hoedown_stack_push(pool, work);
+ }
+
+ return work;
+}
+
+static void popbuf(hoedown_document* doc, int type)
+{
+ doc->work_bufs[type].size--;
+}
+
+static void unscape_text(hoedown_buffer* ob, hoedown_buffer* src)
+{
+ size_t i = 0, org;
+ while (i < src->size) {
+ org = i;
+ while (i < src->size && src->data[i] != '\\')
+ i++;
+
+ if (i > org)
+ hoedown_buffer_put(ob, src->data + org, i - org);
+
+ if (i + 1 >= src->size)
+ break;
+
+ hoedown_buffer_putc(ob, src->data[i + 1]);
+ i += 2;
+ }
+}
+
+static unsigned int hash_link_ref(const uint8_t* link_ref, size_t length)
+{
+ size_t i;
+ unsigned int hash = 0;
+
+ for (i = 0; i < length; ++i)
+ hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash;
+
+ return hash;
+}
+
+static struct link_ref* add_link_ref(struct link_ref** references,
+ const uint8_t* name, size_t name_size)
+{
+ struct link_ref* ref = hoedown_calloc(1, sizeof(struct link_ref));
+
+ ref->id = hash_link_ref(name, name_size);
+ ref->next = references[ref->id % REF_TABLE_SIZE];
+
+ references[ref->id % REF_TABLE_SIZE] = ref;
+ return ref;
+}
+
+static struct link_ref* find_link_ref(struct link_ref** references,
+ uint8_t* name, size_t length)
+{
+ unsigned int hash = hash_link_ref(name, length);
+ struct link_ref* ref = NULL;
+
+ ref = references[hash % REF_TABLE_SIZE];
+
+ while (ref != NULL) {
+ if (ref->id == hash)
+ return ref;
+
+ ref = ref->next;
+ }
+
+ return NULL;
+}
+
+static void free_link_refs(struct link_ref** references)
+{
+ size_t i;
+
+ for (i = 0; i < REF_TABLE_SIZE; ++i) {
+ struct link_ref* r = references[i];
+ struct link_ref* next;
+
+ while (r) {
+ next = r->next;
+ hoedown_buffer_free(r->link);
+ hoedown_buffer_free(r->title);
+ free(r);
+ r = next;
+ }
+ }
+}
+
+static struct footnote_ref* create_footnote_ref(struct footnote_list* list,
+ const uint8_t* name,
+ size_t name_size)
+{
+ struct footnote_ref* ref = hoedown_calloc(1, sizeof(struct footnote_ref));
+
+ ref->id = hash_link_ref(name, name_size);
+
+ return ref;
+}
+
+static int add_footnote_ref(struct footnote_list* list,
+ struct footnote_ref* ref)
+{
+ struct footnote_item* item =
+ hoedown_calloc(1, sizeof(struct footnote_item));
+ if (!item)
+ return 0;
+ item->ref = ref;
+
+ if (list->head == NULL) {
+ list->head = list->tail = item;
+ } else {
+ list->tail->next = item;
+ list->tail = item;
+ }
+ list->count++;
+
+ return 1;
+}
+
+static struct footnote_ref* find_footnote_ref(struct footnote_list* list,
+ uint8_t* name, size_t length)
+{
+ unsigned int hash = hash_link_ref(name, length);
+ struct footnote_item* item = NULL;
+
+ item = list->head;
+
+ while (item != NULL) {
+ if (item->ref->id == hash)
+ return item->ref;
+ item = item->next;
+ }
+
+ return NULL;
+}
+
+static void free_footnote_ref(struct footnote_ref* ref)
+{
+ hoedown_buffer_free(ref->contents);
+ free(ref);
+}
+
+static void free_footnote_list(struct footnote_list* list, int free_refs)
+{
+ struct footnote_item* item = list->head;
+ struct footnote_item* next;
+
+ while (item) {
+ next = item->next;
+ if (free_refs)
+ free_footnote_ref(item->ref);
+ free(item);
+ item = next;
+ }
+}
+
+/*
+ * Check whether a char is a Markdown spacing char.
+
+ * Right now we only consider spaces the actual
+ * space and a newline: tabs and carriage returns
+ * are filtered out during the preprocessing phase.
+ *
+ * If we wanted to actually be UTF-8 compliant, we
+ * should instead extract an Unicode codepoint from
+ * this character and check for space properties.
+ */
+static int _isspace(int c)
+{
+ return c == ' ' || c == '\n';
+}
+
+/* is_empty_all: verify that all the data is spacing */
+static int is_empty_all(const uint8_t* data, size_t size)
+{
+ size_t i = 0;
+ while (i < size && _isspace(data[i]))
+ i++;
+ return i == size;
+}
+
+/*
+ * Replace all spacing characters in data with spaces. As a special
+ * case, this collapses a newline with the previous space, if possible.
+ */
+static void replace_spacing(hoedown_buffer* ob, const uint8_t* data,
+ size_t size)
+{
+ size_t i = 0, mark;
+ hoedown_buffer_grow(ob, size);
+ while (1) {
+ mark = i;
+ while (i < size && data[i] != '\n')
+ i++;
+ hoedown_buffer_put(ob, data + mark, i - mark);
+
+ if (i >= size)
+ break;
+
+ if (!(i > 0 && data[i - 1] == ' '))
+ hoedown_buffer_putc(ob, ' ');
+ i++;
+ }
+}
+
+/****************************
+ * INLINE PARSING FUNCTIONS *
+ ****************************/
+
+/* is_mail_autolink • looks for the address part of a mail autolink and '>' */
+/* this is less strict than the original markdown e-mail address matching */
+static size_t is_mail_autolink(uint8_t* data, size_t size)
+{
+ size_t i = 0, nb = 0;
+
+ /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
+ for (i = 0; i < size; ++i) {
+ if (isalnum(data[i]))
+ continue;
+
+ switch (data[i]) {
+ case '@':
+ nb++;
+
+ case '-':
+ case '.':
+ case '_':
+ break;
+
+ case '>':
+ return (nb == 1) ? i + 1 : 0;
+
+ default:
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+/* tag_length • returns the length of the given tag, or 0 is it's not valid */
+static size_t tag_length(uint8_t* data, size_t size,
+ hoedown_autolink_type* autolink)
+{
+ size_t i, j;
+
+ /* a valid tag can't be shorter than 3 chars */
+ if (size < 3)
+ return 0;
+
+ /* begins with a '<' optionally followed by '/', followed by letter or
+ * number */
+ if (data[0] != '<')
+ return 0;
+ i = (data[1] == '/') ? 2 : 1;
+
+ if (!isalnum(data[i]))
+ return 0;
+
+ /* scheme test */
+ *autolink = HOEDOWN_AUTOLINK_NONE;
+
+ /* try to find the beginning of an URI */
+ while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' ||
+ data[i] == '-'))
+ i++;
+
+ if (i > 1 && data[i] == '@') {
+ if ((j = is_mail_autolink(data + i, size - i)) != 0) {
+ *autolink = HOEDOWN_AUTOLINK_EMAIL;
+ return i + j;
+ }
+ }
+
+ if (i > 2 && data[i] == ':') {
+ *autolink = HOEDOWN_AUTOLINK_NORMAL;
+ i++;
+ }
+
+ /* completing autolink test: no spacing or ' or " */
+ if (i >= size)
+ *autolink = HOEDOWN_AUTOLINK_NONE;
+
+ else if (*autolink) {
+ j = i;
+
+ while (i < size) {
+ if (data[i] == '\\')
+ i += 2;
+ else if (data[i] == '>' || data[i] == '\'' || data[i] == '"' ||
+ data[i] == ' ' || data[i] == '\n')
+ break;
+ else
+ i++;
+ }
+
+ if (i >= size)
+ return 0;
+ if (i > j && data[i] == '>')
+ return i + 1;
+ /* one of the forbidden chars has been found */
+ *autolink = HOEDOWN_AUTOLINK_NONE;
+ }
+
+ /* looking for something looking like a tag end */
+ while (i < size && data[i] != '>')
+ i++;
+ if (i >= size)
+ return 0;
+ return i + 1;
+}
+
+/* parse_inline • parses inline markdown elements */
+static void parse_inline(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t i = 0, end = 0, consumed = 0;
+ hoedown_buffer work = {0, 0, 0, 0, NULL, NULL, NULL};
+ uint8_t* active_char = doc->active_char;
+
+ if (doc->work_bufs[BUFFER_SPAN].size + doc->work_bufs[BUFFER_BLOCK].size >
+ doc->max_nesting)
+ return;
+
+ while (i < size) {
+ /* copying inactive chars into the output */
+ while (end < size && active_char[data[end]] == 0)
+ end++;
+
+ if (doc->md.normal_text) {
+ work.data = data + i;
+ work.size = end - i;
+ doc->md.normal_text(ob, &work, &doc->data);
+ } else
+ hoedown_buffer_put(ob, data + i, end - i);
+
+ if (end >= size)
+ break;
+ i = end;
+
+ end = markdown_char_ptrs[(int)active_char[data[end]]](
+ ob, doc, data + i, i - consumed, size - i);
+ if (!end) /* no action from the callback */
+ end = i + 1;
+ else {
+ i += end;
+ end = i;
+ consumed = i;
+ }
+ }
+}
+
+/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */
+static int is_escaped(uint8_t* data, size_t loc)
+{
+ size_t i = loc;
+ while (i >= 1 && data[i - 1] == '\\')
+ i--;
+
+ /* odd numbers of backslashes escapes data[loc] */
+ return (loc - i) % 2;
+}
+
+/* find_emph_char • looks for the next emph uint8_t, skipping other constructs
+ */
+static size_t find_emph_char(uint8_t* data, size_t size, uint8_t c)
+{
+ size_t i = 0;
+
+ while (i < size) {
+ while (i < size && data[i] != c && data[i] != '[' && data[i] != '`')
+ i++;
+
+ if (i == size)
+ return 0;
+
+ /* not counting escaped chars */
+ if (is_escaped(data, i)) {
+ i++;
+ continue;
+ }
+
+ if (data[i] == c)
+ return i;
+
+ /* skipping a codespan */
+ if (data[i] == '`') {
+ size_t span_nb = 0, bt;
+ size_t tmp_i = 0;
+
+ /* counting the number of opening backticks */
+ while (i < size && data[i] == '`') {
+ i++;
+ span_nb++;
+ }
+
+ if (i >= size)
+ return 0;
+
+ /* finding the matching closing sequence */
+ bt = 0;
+ while (i < size && bt < span_nb) {
+ if (!tmp_i && data[i] == c)
+ tmp_i = i;
+ if (data[i] == '`')
+ bt++;
+ else
+ bt = 0;
+ i++;
+ }
+
+ /* not a well-formed codespan; use found matching emph char */
+ if (i >= size)
+ return tmp_i;
+ }
+ /* skipping a link */
+ else if (data[i] == '[') {
+ size_t tmp_i = 0;
+ uint8_t cc;
+
+ i++;
+ while (i < size && data[i] != ']') {
+ if (!tmp_i && data[i] == c)
+ tmp_i = i;
+ i++;
+ }
+
+ i++;
+ while (i < size && _isspace(data[i]))
+ i++;
+
+ if (i >= size)
+ return tmp_i;
+
+ switch (data[i]) {
+ case '[':
+ cc = ']';
+ break;
+
+ case '(':
+ cc = ')';
+ break;
+
+ default:
+ if (tmp_i)
+ return tmp_i;
+ else
+ continue;
+ }
+
+ i++;
+ while (i < size && data[i] != cc) {
+ if (!tmp_i && data[i] == c)
+ tmp_i = i;
+ i++;
+ }
+
+ if (i >= size)
+ return tmp_i;
+
+ i++;
+ }
+ }
+
+ return 0;
+}
+
+/* parse_emph1 • parsing single emphase */
+/* closed by a symbol not preceded by spacing and not followed by symbol */
+static size_t parse_emph1(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, uint8_t c)
+{
+ size_t i = 0, len;
+ hoedown_buffer* work = 0;
+ int r;
+
+ /* skipping one symbol if coming from emph3 */
+ if (size > 1 && data[0] == c && data[1] == c)
+ i = 1;
+
+ while (i < size) {
+ len = find_emph_char(data + i, size - i, c);
+ if (!len)
+ return 0;
+ i += len;
+ if (i >= size)
+ return 0;
+
+ if (data[i] == c && !_isspace(data[i - 1])) {
+
+ if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
+ if (i + 1 < size && isalnum(data[i + 1]))
+ continue;
+ }
+
+ work = newbuf(doc, BUFFER_SPAN);
+ parse_inline(work, doc, data, i);
+
+ if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_')
+ r = doc->md.underline(ob, work, &doc->data);
+ else
+ r = doc->md.emphasis(ob, work, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ return r ? i + 1 : 0;
+ }
+ }
+
+ return 0;
+}
+
+/* parse_emph2 • parsing single emphase */
+static size_t parse_emph2(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, uint8_t c)
+{
+ size_t i = 0, len;
+ hoedown_buffer* work = 0;
+ int r;
+
+ while (i < size) {
+ len = find_emph_char(data + i, size - i, c);
+ if (!len)
+ return 0;
+ i += len;
+
+ if (i + 1 < size && data[i] == c && data[i + 1] == c && i &&
+ !_isspace(data[i - 1])) {
+ work = newbuf(doc, BUFFER_SPAN);
+ parse_inline(work, doc, data, i);
+
+ if (c == '~')
+ r = doc->md.strikethrough(ob, work, &doc->data);
+ else if (c == '=')
+ r = doc->md.highlight(ob, work, &doc->data);
+ else
+ r = doc->md.double_emphasis(ob, work, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ return r ? i + 2 : 0;
+ }
+ i++;
+ }
+ return 0;
+}
+
+/* parse_emph3 • parsing single emphase */
+/* finds the first closing tag, and delegates to the other emph */
+static size_t parse_emph3(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, uint8_t c)
+{
+ size_t i = 0, len;
+ int r;
+
+ while (i < size) {
+ len = find_emph_char(data + i, size - i, c);
+ if (!len)
+ return 0;
+ i += len;
+
+ /* skip spacing preceded symbols */
+ if (data[i] != c || _isspace(data[i - 1]))
+ continue;
+
+ if (i + 2 < size && data[i + 1] == c && data[i + 2] == c &&
+ doc->md.triple_emphasis) {
+ /* triple symbol found */
+ hoedown_buffer* work = newbuf(doc, BUFFER_SPAN);
+
+ parse_inline(work, doc, data, i);
+ r = doc->md.triple_emphasis(ob, work, &doc->data);
+ popbuf(doc, BUFFER_SPAN);
+ return r ? i + 3 : 0;
+
+ } else if (i + 1 < size && data[i + 1] == c) {
+ /* double symbol found, handing over to emph1 */
+ len = parse_emph1(ob, doc, data - 2, size + 2, c);
+ if (!len)
+ return 0;
+ else
+ return len - 2;
+
+ } else {
+ /* single symbol found, handing over to emph2 */
+ len = parse_emph2(ob, doc, data - 1, size + 1, c);
+ if (!len)
+ return 0;
+ else
+ return len - 1;
+ }
+ }
+ return 0;
+}
+
+/* parse_math • parses a math span until the given ending delimiter */
+static size_t parse_math(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size,
+ const char* end, size_t delimsz, int displaymode)
+{
+ hoedown_buffer text = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ size_t i = delimsz;
+
+ if (!doc->md.math)
+ return 0;
+
+ /* find ending delimiter */
+ while (1) {
+ while (i < size && data[i] != (uint8_t)end[0])
+ i++;
+
+ if (i >= size)
+ return 0;
+
+ if (!is_escaped(data, i) && !(i + delimsz > size) &&
+ memcmp(data + i, end, delimsz) == 0)
+ break;
+
+ i++;
+ }
+
+ /* prepare buffers */
+ text.data = data + delimsz;
+ text.size = i - delimsz;
+
+ /* if this is a $$ and MATH_EXPLICIT is not active,
+ * guess whether displaymode should be enabled from the context */
+ i += delimsz;
+ if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT))
+ displaymode = is_empty_all(data - offset, offset) &&
+ is_empty_all(data + i, size - i);
+
+ /* call callback */
+ if (doc->md.math(ob, &text, displaymode, &doc->data))
+ return i;
+
+ return 0;
+}
+
+/* char_emphasis • single and double emphasis parsing */
+static size_t char_emphasis(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ uint8_t c = data[0];
+ size_t ret;
+
+ if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
+ if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' &&
+ data[-1] != '(')
+ return 0;
+ }
+
+ if (size > 2 && data[1] != c) {
+ /* spacing cannot follow an opening emphasis;
+ * strikethrough and highlight only takes two characters '~~' */
+ if (c == '~' || c == '=' || _isspace(data[1]) ||
+ (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0)
+ return 0;
+
+ return ret + 1;
+ }
+
+ if (size > 3 && data[1] == c && data[2] != c) {
+ if (_isspace(data[2]) ||
+ (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0)
+ return 0;
+
+ return ret + 2;
+ }
+
+ if (size > 4 && data[1] == c && data[2] == c && data[3] != c) {
+ if (c == '~' || c == '=' || _isspace(data[3]) ||
+ (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0)
+ return 0;
+
+ return ret + 3;
+ }
+
+ return 0;
+}
+
+/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */
+static size_t char_linebreak(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ if (offset < 2 || data[-1] != ' ' || data[-2] != ' ')
+ return 0;
+
+ /* removing the last space from ob and rendering */
+ while (ob->size && ob->data[ob->size - 1] == ' ')
+ ob->size--;
+
+ return doc->md.linebreak(ob, &doc->data) ? 1 : 0;
+}
+
+/* char_codespan • '`' parsing a code span (assuming codespan != 0) */
+static size_t char_codespan(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ hoedown_buffer work = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ size_t end, nb = 0, i, f_begin, f_end;
+
+ /* counting the number of backticks in the delimiter */
+ while (nb < size && data[nb] == '`')
+ nb++;
+
+ /* finding the next delimiter */
+ i = 0;
+ for (end = nb; end < size && i < nb; end++) {
+ if (data[end] == '`')
+ i++;
+ else
+ i = 0;
+ }
+
+ if (i < nb && end >= size)
+ return 0; /* no matching delimiter */
+
+ /* trimming outside spaces */
+ f_begin = nb;
+ while (f_begin < end && data[f_begin] == ' ')
+ f_begin++;
+
+ f_end = end - nb;
+ while (f_end > nb && data[f_end - 1] == ' ')
+ f_end--;
+
+ /* real code span */
+ if (f_begin < f_end) {
+ work.data = data + f_begin;
+ work.size = f_end - f_begin;
+
+ if (!doc->md.codespan(ob, &work, &doc->data))
+ end = 0;
+ } else {
+ if (!doc->md.codespan(ob, 0, &doc->data))
+ end = 0;
+ }
+
+ return end;
+}
+
+/* char_quote • '"' parsing a quote */
+static size_t char_quote(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ size_t end, nq = 0, i, f_begin, f_end;
+
+ /* counting the number of quotes in the delimiter */
+ while (nq < size && data[nq] == '"')
+ nq++;
+
+ /* finding the next delimiter */
+ end = nq;
+ while (1) {
+ i = end;
+ end += find_emph_char(data + end, size - end, '"');
+ if (end == i)
+ return 0; /* no matching delimiter */
+ i = end;
+ while (end < size && data[end] == '"' && end - i < nq)
+ end++;
+ if (end - i >= nq)
+ break;
+ }
+
+ /* trimming outside spaces */
+ f_begin = nq;
+ while (f_begin < end && data[f_begin] == ' ')
+ f_begin++;
+
+ f_end = end - nq;
+ while (f_end > nq && data[f_end - 1] == ' ')
+ f_end--;
+
+ /* real quote */
+ if (f_begin < f_end) {
+ hoedown_buffer* work = newbuf(doc, BUFFER_SPAN);
+ parse_inline(work, doc, data + f_begin, f_end - f_begin);
+
+ if (!doc->md.quote(ob, work, &doc->data))
+ end = 0;
+ popbuf(doc, BUFFER_SPAN);
+ } else {
+ if (!doc->md.quote(ob, 0, &doc->data))
+ end = 0;
+ }
+
+ return end;
+}
+
+/* char_escape • '\\' backslash escape */
+static size_t char_escape(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ static const char* escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$";
+ hoedown_buffer work = {0, 0, 0, 0, NULL, NULL, NULL};
+ size_t w;
+
+ if (size > 1) {
+ if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) &&
+ size > 2 && (data[2] == '(' || data[2] == '[')) {
+ const char* end = (data[2] == '[') ? "\\\\]" : "\\\\)";
+ w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '[');
+ if (w)
+ return w;
+ }
+
+ if (strchr(escape_chars, data[1]) == NULL)
+ return 0;
+
+ if (doc->md.normal_text) {
+ work.data = data + 1;
+ work.size = 1;
+ doc->md.normal_text(ob, &work, &doc->data);
+ } else
+ hoedown_buffer_putc(ob, data[1]);
+ } else if (size == 1) {
+ hoedown_buffer_putc(ob, data[0]);
+ }
+
+ return 2;
+}
+
+/* char_entity • '&' escaped when it doesn't belong to an entity */
+/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */
+static size_t char_entity(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ size_t end = 1;
+ hoedown_buffer work = {0, 0, 0, 0, NULL, NULL, NULL};
+
+ if (end < size && data[end] == '#')
+ end++;
+
+ while (end < size && isalnum(data[end]))
+ end++;
+
+ if (end < size && data[end] == ';')
+ end++; /* real entity */
+ else
+ return 0; /* lone '&' */
+
+ if (doc->md.entity) {
+ work.data = data;
+ work.size = end;
+ doc->md.entity(ob, &work, &doc->data);
+ } else
+ hoedown_buffer_put(ob, data, end);
+
+ return end;
+}
+
+/* char_langle_tag • '<' when tags or autolinks are allowed */
+static size_t char_langle_tag(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ hoedown_buffer work = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE;
+ size_t end = tag_length(data, size, &altype);
+ int ret = 0;
+
+ work.data = data;
+ work.size = end;
+
+ if (end > 2) {
+ if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) {
+ hoedown_buffer* u_link = newbuf(doc, BUFFER_SPAN);
+ work.data = data + 1;
+ work.size = end - 2;
+ unscape_text(u_link, &work);
+ ret = doc->md.autolink(ob, u_link, altype, &doc->data);
+ popbuf(doc, BUFFER_SPAN);
+ } else if (doc->md.raw_html)
+ ret = doc->md.raw_html(ob, &work, &doc->data);
+ }
+
+ if (!ret)
+ return 0;
+ else
+ return end;
+}
+
+static size_t char_autolink_www(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ hoedown_buffer *link, *link_url, *link_text;
+ size_t link_len, rewind;
+
+ if (!doc->md.link || doc->in_link_body)
+ return 0;
+
+ link = newbuf(doc, BUFFER_SPAN);
+
+ if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size,
+ HOEDOWN_AUTOLINK_SHORT_DOMAINS)) >
+ 0) {
+ link_url = newbuf(doc, BUFFER_SPAN);
+ HOEDOWN_BUFPUTSL(link_url, "http://");
+ hoedown_buffer_put(link_url, link->data, link->size);
+
+ ob->size -= rewind;
+ if (doc->md.normal_text) {
+ link_text = newbuf(doc, BUFFER_SPAN);
+ doc->md.normal_text(link_text, link, &doc->data);
+ doc->md.link(ob, link_text, link_url, NULL, &doc->data);
+ popbuf(doc, BUFFER_SPAN);
+ } else {
+ doc->md.link(ob, link, link_url, NULL, &doc->data);
+ }
+ popbuf(doc, BUFFER_SPAN);
+ }
+
+ popbuf(doc, BUFFER_SPAN);
+ return link_len;
+}
+
+static size_t char_autolink_email(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ hoedown_buffer* link;
+ size_t link_len, rewind;
+
+ if (!doc->md.autolink || doc->in_link_body)
+ return 0;
+
+ link = newbuf(doc, BUFFER_SPAN);
+
+ if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size,
+ 0)) > 0) {
+ ob->size -= rewind;
+ doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data);
+ }
+
+ popbuf(doc, BUFFER_SPAN);
+ return link_len;
+}
+
+static size_t char_autolink_url(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ hoedown_buffer* link;
+ size_t link_len, rewind;
+
+ if (!doc->md.autolink || doc->in_link_body)
+ return 0;
+
+ link = newbuf(doc, BUFFER_SPAN);
+
+ if ((link_len =
+ hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) {
+ ob->size -= rewind;
+ doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data);
+ }
+
+ popbuf(doc, BUFFER_SPAN);
+ return link_len;
+}
+
+/* char_link • '[': parsing a link, a footnote or an image */
+static size_t char_link(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ int is_img =
+ (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1));
+ int is_footnote =
+ (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^');
+ size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
+ hoedown_buffer* content = NULL;
+ hoedown_buffer* link = NULL;
+ hoedown_buffer* title = NULL;
+ hoedown_buffer* u_link = NULL;
+ size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size;
+ int ret = 0, in_title = 0, qtype = 0;
+
+ /* checking whether the correct renderer exists */
+ if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image) ||
+ (!is_img && !is_footnote && !doc->md.link))
+ goto cleanup;
+
+ /* looking for the matching closing bracket */
+ i += find_emph_char(data + i, size - i, ']');
+ txt_e = i;
+
+ if (i < size && data[i] == ']')
+ i++;
+ else
+ goto cleanup;
+
+ /* footnote link */
+ if (is_footnote) {
+ hoedown_buffer id = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ struct footnote_ref* fr;
+
+ if (txt_e < 3)
+ goto cleanup;
+
+ id.data = data + 2;
+ id.size = txt_e - 2;
+
+ fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size);
+
+ /* mark footnote used */
+ if (fr && !fr->is_used) {
+ if (!add_footnote_ref(&doc->footnotes_used, fr))
+ goto cleanup;
+ fr->is_used = 1;
+ fr->num = doc->footnotes_used.count;
+
+ /* render */
+ if (doc->md.footnote_ref)
+ ret = doc->md.footnote_ref(ob, fr->num, &doc->data);
+ }
+
+ goto cleanup;
+ }
+
+ /* skip any amount of spacing */
+ /* (this is much more laxist than original markdown syntax) */
+ while (i < size && _isspace(data[i]))
+ i++;
+
+ /* inline style link */
+ if (i < size && data[i] == '(') {
+ size_t nb_p;
+
+ /* skipping initial spacing */
+ i++;
+
+ while (i < size && _isspace(data[i]))
+ i++;
+
+ link_b = i;
+
+ /* looking for link end: ' " ) */
+ /* Count the number of open parenthesis */
+ nb_p = 0;
+
+ while (i < size) {
+ if (data[i] == '\\')
+ i += 2;
+ else if (data[i] == '(' && i != 0) {
+ nb_p++;
+ i++;
+ } else if (data[i] == ')') {
+ if (nb_p == 0)
+ break;
+ else
+ nb_p--;
+ i++;
+ } else if (i >= 1 && _isspace(data[i - 1]) &&
+ (data[i] == '\'' || data[i] == '"'))
+ break;
+ else
+ i++;
+ }
+
+ if (i >= size)
+ goto cleanup;
+ link_e = i;
+
+ /* looking for title end if present */
+ if (data[i] == '\'' || data[i] == '"') {
+ qtype = data[i];
+ in_title = 1;
+ i++;
+ title_b = i;
+
+ while (i < size) {
+ if (data[i] == '\\')
+ i += 2;
+ else if (data[i] == qtype) {
+ in_title = 0;
+ i++;
+ } else if ((data[i] == ')') && !in_title)
+ break;
+ else
+ i++;
+ }
+
+ if (i >= size)
+ goto cleanup;
+
+ /* skipping spacing after title */
+ title_e = i - 1;
+ while (title_e > title_b && _isspace(data[title_e]))
+ title_e--;
+
+ /* checking for closing quote presence */
+ if (data[title_e] != '\'' && data[title_e] != '"') {
+ title_b = title_e = 0;
+ link_e = i;
+ }
+ }
+
+ /* remove spacing at the end of the link */
+ while (link_e > link_b && _isspace(data[link_e - 1]))
+ link_e--;
+
+ /* remove optional angle brackets around the link */
+ if (data[link_b] == '<')
+ link_b++;
+ if (data[link_e - 1] == '>')
+ link_e--;
+
+ /* building escaped link and title */
+ if (link_e > link_b) {
+ link = newbuf(doc, BUFFER_SPAN);
+ hoedown_buffer_put(link, data + link_b, link_e - link_b);
+ }
+
+ if (title_e > title_b) {
+ title = newbuf(doc, BUFFER_SPAN);
+ hoedown_buffer_put(title, data + title_b, title_e - title_b);
+ }
+
+ i++;
+ }
+
+ /* reference style link */
+ else if (i < size && data[i] == '[') {
+ hoedown_buffer* id = newbuf(doc, BUFFER_SPAN);
+ struct link_ref* lr;
+
+ /* looking for the id */
+ i++;
+ link_b = i;
+ while (i < size && data[i] != ']')
+ i++;
+ if (i >= size)
+ goto cleanup;
+ link_e = i;
+
+ /* finding the link_ref */
+ if (link_b == link_e)
+ replace_spacing(id, data + 1, txt_e - 1);
+ else
+ hoedown_buffer_put(id, data + link_b, link_e - link_b);
+
+ lr = find_link_ref(doc->refs, id->data, id->size);
+ if (!lr)
+ goto cleanup;
+
+ /* keeping link and title from link_ref */
+ link = lr->link;
+ title = lr->title;
+ i++;
+ }
+
+ /* shortcut reference style link */
+ else {
+ hoedown_buffer* id = newbuf(doc, BUFFER_SPAN);
+ struct link_ref* lr;
+
+ /* crafting the id */
+ replace_spacing(id, data + 1, txt_e - 1);
+
+ /* finding the link_ref */
+ lr = find_link_ref(doc->refs, id->data, id->size);
+ if (!lr)
+ goto cleanup;
+
+ /* keeping link and title from link_ref */
+ link = lr->link;
+ title = lr->title;
+
+ /* rewinding the spacing */
+ i = txt_e + 1;
+ }
+
+ /* building content: img alt is kept, only link content is parsed */
+ if (txt_e > 1) {
+ content = newbuf(doc, BUFFER_SPAN);
+ if (is_img) {
+ hoedown_buffer_put(content, data + 1, txt_e - 1);
+ } else {
+ /* disable autolinking when parsing inline the
+ * content of a link */
+ doc->in_link_body = 1;
+ parse_inline(content, doc, data + 1, txt_e - 1);
+ doc->in_link_body = 0;
+ }
+ }
+
+ if (link) {
+ u_link = newbuf(doc, BUFFER_SPAN);
+ unscape_text(u_link, link);
+ }
+
+ /* calling the relevant rendering function */
+ if (is_img) {
+ if (ob->size && ob->data[ob->size - 1] == '!')
+ ob->size -= 1;
+
+ ret = doc->md.image(ob, u_link, title, content, &doc->data);
+ } else {
+ ret = doc->md.link(ob, content, u_link, title, &doc->data);
+ }
+
+ /* cleanup */
+cleanup:
+ doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size;
+ return ret ? i : 0;
+}
+
+static size_t char_superscript(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ size_t sup_start, sup_len;
+ hoedown_buffer* sup;
+
+ if (!doc->md.superscript)
+ return 0;
+
+ if (size < 2)
+ return 0;
+
+ if (data[1] == '(') {
+ sup_start = 2;
+ sup_len = find_emph_char(data + 2, size - 2, ')') + 2;
+
+ if (sup_len == size)
+ return 0;
+ } else {
+ sup_start = sup_len = 1;
+
+ while (sup_len < size && !_isspace(data[sup_len]))
+ sup_len++;
+ }
+
+ if (sup_len - sup_start == 0)
+ return (sup_start == 2) ? 3 : 0;
+
+ sup = newbuf(doc, BUFFER_SPAN);
+ parse_inline(sup, doc, data + sup_start, sup_len - sup_start);
+ doc->md.superscript(ob, sup, &doc->data);
+ popbuf(doc, BUFFER_SPAN);
+
+ return (sup_start == 2) ? sup_len + 1 : sup_len;
+}
+
+static size_t char_math(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t offset, size_t size)
+{
+ /* double dollar */
+ if (size > 1 && data[1] == '$')
+ return parse_math(ob, doc, data, offset, size, "$$", 2, 1);
+
+ /* single dollar allowed only with MATH_EXPLICIT flag */
+ if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)
+ return parse_math(ob, doc, data, offset, size, "$", 1, 0);
+
+ return 0;
+}
+
+/*********************************
+ * BLOCK-LEVEL PARSING FUNCTIONS *
+ *********************************/
+
+/* is_empty • returns the line length when it is empty, 0 otherwise */
+static size_t is_empty(const uint8_t* data, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size && data[i] != '\n'; i++)
+ if (data[i] != ' ')
+ return 0;
+
+ return i + 1;
+}
+
+/* is_hrule • returns whether a line is a horizontal rule */
+static int is_hrule(uint8_t* data, size_t size)
+{
+ size_t i = 0, n = 0;
+ uint8_t c;
+
+ /* skipping initial spaces */
+ if (size < 3)
+ return 0;
+ if (data[0] == ' ') {
+ i++;
+ if (data[1] == ' ') {
+ i++;
+ if (data[2] == ' ') {
+ i++;
+ }
+ }
+ }
+
+ /* looking at the hrule uint8_t */
+ if (i + 2 >= size || (data[i] != '*' && data[i] != '-' && data[i] != '_'))
+ return 0;
+ c = data[i];
+
+ /* the whole line must be the char or space */
+ while (i < size && data[i] != '\n') {
+ if (data[i] == c)
+ n++;
+ else if (data[i] != ' ')
+ return 0;
+
+ i++;
+ }
+
+ return n >= 3;
+}
+
+/* check if a line is a code fence; return the
+ * end of the code fence. if passed, width of
+ * the fence rule and character will be returned */
+static size_t is_codefence(uint8_t* data, size_t size, size_t* width,
+ uint8_t* chr)
+{
+ size_t i = 0, n = 1;
+ uint8_t c;
+
+ /* skipping initial spaces */
+ if (size < 3)
+ return 0;
+
+ if (data[0] == ' ') {
+ i++;
+ if (data[1] == ' ') {
+ i++;
+ if (data[2] == ' ') {
+ i++;
+ }
+ }
+ }
+
+ /* looking at the hrule uint8_t */
+ c = data[i];
+ if (i + 2 >= size || !(c == '~' || c == '`'))
+ return 0;
+
+ /* the fence must be that same character */
+ while (++i < size && data[i] == c)
+ ++n;
+
+ if (n < 3)
+ return 0;
+
+ if (width)
+ *width = n;
+ if (chr)
+ *chr = c;
+ return i;
+}
+
+/* expects single line, checks if it's a codefence and extracts language */
+static size_t parse_codefence(uint8_t* data, size_t size, hoedown_buffer* lang,
+ size_t* width, uint8_t* chr)
+{
+ size_t i, w, lang_start;
+
+ i = w = is_codefence(data, size, width, chr);
+ if (i == 0)
+ return 0;
+
+ while (i < size && _isspace(data[i]))
+ i++;
+
+ lang_start = i;
+
+ while (i < size && !_isspace(data[i]))
+ i++;
+
+ lang->data = data + lang_start;
+ lang->size = i - lang_start;
+
+ /* Avoid parsing a codespan as a fence */
+ i = lang_start + 2;
+ while (i < size &&
+ !(data[i] == *chr && data[i - 1] == *chr && data[i - 2] == *chr))
+ i++;
+ if (i < size)
+ return 0;
+
+ return w;
+}
+
+/* is_atxheader • returns whether the line is a hash-prefixed header */
+static int is_atxheader(hoedown_document* doc, uint8_t* data, size_t size)
+{
+ if (data[0] != '#')
+ return 0;
+
+ if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) {
+ size_t level = 0;
+
+ while (level < size && level < 6 && data[level] == '#')
+ level++;
+
+ if (level < size && data[level] != ' ')
+ return 0;
+ }
+
+ return 1;
+}
+
+/* is_headerline • returns whether the line is a setext-style hdr underline */
+static int is_headerline(uint8_t* data, size_t size)
+{
+ size_t i = 0;
+
+ /* test of level 1 header */
+ if (data[i] == '=') {
+ for (i = 1; i < size && data[i] == '='; i++)
+ ;
+ while (i < size && data[i] == ' ')
+ i++;
+ return (i >= size || data[i] == '\n') ? 1 : 0;
+ }
+
+ /* test of level 2 header */
+ if (data[i] == '-') {
+ for (i = 1; i < size && data[i] == '-'; i++)
+ ;
+ while (i < size && data[i] == ' ')
+ i++;
+ return (i >= size || data[i] == '\n') ? 2 : 0;
+ }
+
+ return 0;
+}
+
+static int is_next_headerline(uint8_t* data, size_t size)
+{
+ size_t i = 0;
+
+ while (i < size && data[i] != '\n')
+ i++;
+
+ if (++i >= size)
+ return 0;
+
+ return is_headerline(data + i, size - i);
+}
+
+/* prefix_quote • returns blockquote prefix length */
+static size_t prefix_quote(uint8_t* data, size_t size)
+{
+ size_t i = 0;
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+
+ if (i < size && data[i] == '>') {
+ if (i + 1 < size && data[i + 1] == ' ')
+ return i + 2;
+
+ return i + 1;
+ }
+
+ return 0;
+}
+
+/* prefix_code • returns prefix length for block code*/
+static size_t prefix_code(uint8_t* data, size_t size)
+{
+ if (size > 3 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' &&
+ data[3] == ' ')
+ return 4;
+
+ return 0;
+}
+
+/* prefix_oli • returns ordered list item prefix */
+static size_t prefix_oli(uint8_t* data, size_t size)
+{
+ size_t i = 0;
+
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+
+ if (i >= size || data[i] < '0' || data[i] > '9')
+ return 0;
+
+ while (i < size && data[i] >= '0' && data[i] <= '9')
+ i++;
+
+ if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ')
+ return 0;
+
+ if (is_next_headerline(data + i, size - i))
+ return 0;
+
+ return i + 2;
+}
+
+/* prefix_uli • returns ordered list item prefix */
+static size_t prefix_uli(uint8_t* data, size_t size)
+{
+ size_t i = 0;
+
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+ if (i < size && data[i] == ' ')
+ i++;
+
+ if (i + 1 >= size || (data[i] != '*' && data[i] != '+' && data[i] != '-') ||
+ data[i + 1] != ' ')
+ return 0;
+
+ if (is_next_headerline(data + i, size - i))
+ return 0;
+
+ return i + 2;
+}
+
+/* parse_block • parsing of one block, returning next uint8_t to parse */
+static void parse_block(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size);
+
+/* parse_blockquote • handles parsing of a blockquote fragment */
+static size_t parse_blockquote(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t beg, end = 0, pre, work_size = 0;
+ uint8_t* work_data = 0;
+ hoedown_buffer* out = 0;
+
+ out = newbuf(doc, BUFFER_BLOCK);
+ beg = 0;
+ while (beg < size) {
+ for (end = beg + 1; end < size && data[end - 1] != '\n'; end++)
+ ;
+
+ pre = prefix_quote(data + beg, end - beg);
+
+ if (pre)
+ beg += pre; /* skipping prefix */
+
+ /* empty line followed by non-quote line */
+ else if (is_empty(data + beg, end - beg) &&
+ (end >= size || (prefix_quote(data + end, size - end) == 0 &&
+ !is_empty(data + end, size - end))))
+ break;
+
+ if (beg < end) { /* copy into the in-place working buffer */
+ /* hoedown_buffer_put(work, data + beg, end - beg); */
+ if (!work_data)
+ work_data = data + beg;
+ else if (data + beg != work_data + work_size)
+ memmove(work_data + work_size, data + beg, end - beg);
+ work_size += end - beg;
+ }
+ beg = end;
+ }
+
+ parse_block(out, doc, work_data, work_size);
+ if (doc->md.blockquote)
+ doc->md.blockquote(ob, out, &doc->data);
+ popbuf(doc, BUFFER_BLOCK);
+ return end;
+}
+
+static size_t parse_htmlblock(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, int do_render);
+
+/* parse_blockquote • handles parsing of a regular paragraph */
+static size_t parse_paragraph(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ hoedown_buffer work = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ size_t i = 0, end = 0;
+ int level = 0;
+
+ work.data = data;
+
+ while (i < size) {
+ for (end = i + 1; end < size && data[end - 1] != '\n';
+ end++) /* empty */
+ ;
+
+ if (is_empty(data + i, size - i))
+ break;
+
+ if ((level = is_headerline(data + i, size - i)) != 0)
+ break;
+
+ if (is_atxheader(doc, data + i, size - i) ||
+ is_hrule(data + i, size - i) || prefix_quote(data + i, size - i)) {
+ end = i;
+ break;
+ }
+
+ i = end;
+ }
+
+ work.size = i;
+ while (work.size && data[work.size - 1] == '\n')
+ work.size--;
+
+ if (!level) {
+ hoedown_buffer* tmp = newbuf(doc, BUFFER_BLOCK);
+ parse_inline(tmp, doc, work.data, work.size);
+ if (doc->md.paragraph)
+ doc->md.paragraph(ob, tmp, &doc->data);
+ popbuf(doc, BUFFER_BLOCK);
+ } else {
+ hoedown_buffer* header_work;
+
+ if (work.size) {
+ size_t beg;
+ i = work.size;
+ work.size -= 1;
+
+ while (work.size && data[work.size] != '\n')
+ work.size -= 1;
+
+ beg = work.size + 1;
+ while (work.size && data[work.size - 1] == '\n')
+ work.size -= 1;
+
+ if (work.size > 0) {
+ hoedown_buffer* tmp = newbuf(doc, BUFFER_BLOCK);
+ parse_inline(tmp, doc, work.data, work.size);
+
+ if (doc->md.paragraph)
+ doc->md.paragraph(ob, tmp, &doc->data);
+
+ popbuf(doc, BUFFER_BLOCK);
+ work.data += beg;
+ work.size = i - beg;
+ } else
+ work.size = i;
+ }
+
+ header_work = newbuf(doc, BUFFER_SPAN);
+ parse_inline(header_work, doc, work.data, work.size);
+
+ if (doc->md.header)
+ doc->md.header(ob, header_work, (int)level, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ }
+
+ return end;
+}
+
+/* parse_fencedcode • handles parsing of a block-level code fragment */
+static size_t parse_fencedcode(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ hoedown_buffer text = {0, 0, 0, 0, NULL, NULL, NULL};
+ hoedown_buffer lang = {0, 0, 0, 0, NULL, NULL, NULL};
+ size_t i = 0, text_start, line_start;
+ size_t w, w2;
+ size_t width, width2;
+ uint8_t chr, chr2;
+
+ /* parse codefence line */
+ while (i < size && data[i] != '\n')
+ i++;
+
+ w = parse_codefence(data, i, &lang, &width, &chr);
+ if (!w)
+ return 0;
+
+ /* search for end */
+ i++;
+ text_start = i;
+ while ((line_start = i) < size) {
+ while (i < size && data[i] != '\n')
+ i++;
+
+ w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2);
+ if (w == w2 && width == width2 && chr == chr2 &&
+ is_empty(data + (line_start + w), i - (line_start + w)))
+ break;
+
+ i++;
+ }
+
+ text.data = data + text_start;
+ text.size = line_start - text_start;
+
+ if (doc->md.blockcode)
+ doc->md.blockcode(ob, text.size ? &text : NULL,
+ lang.size ? &lang : NULL, &doc->data);
+
+ return i;
+}
+
+static size_t parse_blockcode(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t beg, end, pre;
+ hoedown_buffer* work = 0;
+
+ work = newbuf(doc, BUFFER_BLOCK);
+
+ beg = 0;
+ while (beg < size) {
+ for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {
+ };
+ pre = prefix_code(data + beg, end - beg);
+
+ if (pre)
+ beg += pre; /* skipping prefix */
+ else if (!is_empty(data + beg, end - beg))
+ /* non-empty non-prefixed line breaks the pre */
+ break;
+
+ if (beg < end) {
+ /* verbatim copy to the working buffer,
+ escaping entities */
+ if (is_empty(data + beg, end - beg))
+ hoedown_buffer_putc(work, '\n');
+ else
+ hoedown_buffer_put(work, data + beg, end - beg);
+ }
+ beg = end;
+ }
+
+ while (work->size && work->data[work->size - 1] == '\n')
+ work->size -= 1;
+
+ hoedown_buffer_putc(work, '\n');
+
+ if (doc->md.blockcode)
+ doc->md.blockcode(ob, work, NULL, &doc->data);
+
+ popbuf(doc, BUFFER_BLOCK);
+ return beg;
+}
+
+/* parse_listitem • parsing of a single list item */
+/* assuming initial prefix is already removed */
+static size_t parse_listitem(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size,
+ hoedown_list_flags* flags)
+{
+ hoedown_buffer *work = 0, *inter = 0;
+ size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
+ int in_empty = 0, has_inside_empty = 0, in_fence = 0;
+
+ /* keeping track of the first indentation prefix */
+ while (orgpre < 3 && orgpre < size && data[orgpre] == ' ')
+ orgpre++;
+
+ beg = prefix_uli(data, size);
+ if (!beg)
+ beg = prefix_oli(data, size);
+
+ if (!beg)
+ return 0;
+
+ /* skipping to the beginning of the following line */
+ end = beg;
+ while (end < size && data[end - 1] != '\n')
+ end++;
+
+ /* getting working buffers */
+ work = newbuf(doc, BUFFER_SPAN);
+ inter = newbuf(doc, BUFFER_SPAN);
+
+ /* putting the first line into the working buffer */
+ hoedown_buffer_put(work, data + beg, end - beg);
+ beg = end;
+
+ /* process the following lines */
+ while (beg < size) {
+ size_t has_next_uli = 0, has_next_oli = 0;
+
+ end++;
+
+ while (end < size && data[end - 1] != '\n')
+ end++;
+
+ /* process an empty line */
+ if (is_empty(data + beg, end - beg)) {
+ in_empty = 1;
+ beg = end;
+ continue;
+ }
+
+ /* calculating the indentation */
+ i = 0;
+ while (i < 4 && beg + i < end && data[beg + i] == ' ')
+ i++;
+
+ pre = i;
+
+ if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) {
+ if (is_codefence(data + beg + i, end - beg - i, NULL, NULL))
+ in_fence = !in_fence;
+ }
+
+ /* Only check for new list items if we are **not** inside
+ * a fenced code block */
+ if (!in_fence) {
+ has_next_uli = prefix_uli(data + beg + i, end - beg - i);
+ has_next_oli = prefix_oli(data + beg + i, end - beg - i);
+ }
+
+ /* checking for a new item */
+ if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) ||
+ has_next_oli) {
+ if (in_empty)
+ has_inside_empty = 1;
+
+ /* the following item must have the same (or less) indentation */
+ if (pre <= orgpre) {
+ /* if the following item has different list type, we end this
+ * list */
+ if (in_empty &&
+ (((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) ||
+ (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli)))
+ *flags |= HOEDOWN_LI_END;
+
+ break;
+ }
+
+ if (!sublist)
+ sublist = work->size;
+ }
+ /* joining only indented stuff after empty lines;
+ * note that now we only require 1 space of indentation
+ * to continue a list */
+ else if (in_empty && pre == 0) {
+ *flags |= HOEDOWN_LI_END;
+ break;
+ }
+
+ if (in_empty) {
+ hoedown_buffer_putc(work, '\n');
+ has_inside_empty = 1;
+ in_empty = 0;
+ }
+
+ /* adding the line without prefix into the working buffer */
+ hoedown_buffer_put(work, data + beg + i, end - beg - i);
+ beg = end;
+ }
+
+ /* render of li contents */
+ if (has_inside_empty)
+ *flags |= HOEDOWN_LI_BLOCK;
+
+ if (*flags & HOEDOWN_LI_BLOCK) {
+ /* intermediate render of block li */
+ if (sublist && sublist < work->size) {
+ parse_block(inter, doc, work->data, sublist);
+ parse_block(inter, doc, work->data + sublist, work->size - sublist);
+ } else
+ parse_block(inter, doc, work->data, work->size);
+ } else {
+ /* intermediate render of inline li */
+ if (sublist && sublist < work->size) {
+ parse_inline(inter, doc, work->data, sublist);
+ parse_block(inter, doc, work->data + sublist, work->size - sublist);
+ } else
+ parse_inline(inter, doc, work->data, work->size);
+ }
+
+ /* render of li itself */
+ if (doc->md.listitem)
+ doc->md.listitem(ob, inter, *flags, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ popbuf(doc, BUFFER_SPAN);
+ return beg;
+}
+
+/* parse_list • parsing ordered or unordered list block */
+static size_t parse_list(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, hoedown_list_flags flags)
+{
+ hoedown_buffer* work = 0;
+ size_t i = 0, j;
+
+ work = newbuf(doc, BUFFER_BLOCK);
+
+ while (i < size) {
+ j = parse_listitem(work, doc, data + i, size - i, &flags);
+ i += j;
+
+ if (!j || (flags & HOEDOWN_LI_END))
+ break;
+ }
+
+ if (doc->md.list)
+ doc->md.list(ob, work, flags, &doc->data);
+ popbuf(doc, BUFFER_BLOCK);
+ return i;
+}
+
+/* parse_atxheader • parsing of atx-style headers */
+static size_t parse_atxheader(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t level = 0;
+ size_t i, end, skip;
+
+ while (level < size && level < 6 && data[level] == '#')
+ level++;
+
+ for (i = level; i < size && data[i] == ' '; i++)
+ ;
+
+ for (end = i; end < size && data[end] != '\n'; end++)
+ ;
+ skip = end;
+
+ while (end && data[end - 1] == '#')
+ end--;
+
+ while (end && data[end - 1] == ' ')
+ end--;
+
+ if (end > i) {
+ hoedown_buffer* work = newbuf(doc, BUFFER_SPAN);
+
+ parse_inline(work, doc, data + i, end - i);
+
+ if (doc->md.header)
+ doc->md.header(ob, work, (int)level, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ }
+
+ return skip;
+}
+
+/* parse_footnote_def • parse a single footnote definition */
+static void parse_footnote_def(hoedown_buffer* ob, hoedown_document* doc,
+ unsigned int num, uint8_t* data, size_t size)
+{
+ hoedown_buffer* work = 0;
+ work = newbuf(doc, BUFFER_SPAN);
+
+ parse_block(work, doc, data, size);
+
+ if (doc->md.footnote_def)
+ doc->md.footnote_def(ob, work, num, &doc->data);
+ popbuf(doc, BUFFER_SPAN);
+}
+
+/* parse_footnote_list • render the contents of the footnotes */
+static void parse_footnote_list(hoedown_buffer* ob, hoedown_document* doc,
+ struct footnote_list* footnotes)
+{
+ hoedown_buffer* work = 0;
+ struct footnote_item* item;
+ struct footnote_ref* ref;
+
+ if (footnotes->count == 0)
+ return;
+
+ work = newbuf(doc, BUFFER_BLOCK);
+
+ item = footnotes->head;
+ while (item) {
+ ref = item->ref;
+ parse_footnote_def(work, doc, ref->num, ref->contents->data,
+ ref->contents->size);
+ item = item->next;
+ }
+
+ if (doc->md.footnotes)
+ doc->md.footnotes(ob, work, &doc->data);
+ popbuf(doc, BUFFER_BLOCK);
+}
+
+/* htmlblock_is_end • check for end of HTML block : </tag>( *)\n */
+/* returns tag length on match, 0 otherwise */
+/* assumes data starts with "<" */
+static size_t htmlblock_is_end(const char* tag, size_t tag_len,
+ hoedown_document* doc, uint8_t* data,
+ size_t size)
+{
+ size_t i = tag_len + 3, w;
+
+ /* try to match the end tag */
+ /* note: we're not considering tags like "</tag >" which are still valid */
+ if (i > size || data[1] != '/' ||
+ strncasecmp((char*)data + 2, tag, tag_len) != 0 ||
+ data[tag_len + 2] != '>')
+ return 0;
+
+ /* rest of the line must be empty */
+ if ((w = is_empty(data + i, size - i)) == 0 && i < size)
+ return 0;
+
+ return i + w;
+}
+
+/* htmlblock_find_end • try to find HTML block ending tag */
+/* returns the length on match, 0 otherwise */
+static size_t htmlblock_find_end(const char* tag, size_t tag_len,
+ hoedown_document* doc, uint8_t* data,
+ size_t size)
+{
+ size_t i = 0, w;
+
+ while (1) {
+ while (i < size && data[i] != '<')
+ i++;
+ if (i >= size)
+ return 0;
+
+ w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i);
+ if (w)
+ return i + w;
+ i++;
+ }
+}
+
+/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */
+/* (it must be an unindented line, and have a blank line afterwads) */
+/* returns the length on match, 0 otherwise */
+static size_t htmlblock_find_end_strict(const char* tag, size_t tag_len,
+ hoedown_document* doc, uint8_t* data,
+ size_t size)
+{
+ size_t i = 0, mark;
+
+ while (1) {
+ mark = i;
+ while (i < size && data[i] != '\n')
+ i++;
+ if (i < size)
+ i++;
+ if (i == mark)
+ return 0;
+
+ if (data[mark] == ' ' && mark > 0)
+ continue;
+ mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark);
+ if (mark == i && (is_empty(data + i, size - i) || i >= size))
+ break;
+ }
+
+ return i;
+}
+
+/* parse_htmlblock • parsing of inline HTML block */
+static size_t parse_htmlblock(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, int do_render)
+{
+ hoedown_buffer work = {NULL, 0, 0, 0, NULL, NULL, NULL};
+ size_t i, j = 0, tag_len, tag_end;
+ const char* curtag = NULL;
+
+ work.data = data;
+
+ /* identification of the opening tag */
+ if (size < 2 || data[0] != '<')
+ return 0;
+
+ i = 1;
+ while (i < size && data[i] != '>' && data[i] != ' ')
+ i++;
+
+ if (i < size)
+ curtag = hoedown_find_block_tag((char*)data + 1, (int)i - 1);
+
+ /* handling of special cases */
+ if (!curtag) {
+
+ /* HTML comment, laxist form */
+ if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
+ i = 5;
+
+ while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' &&
+ data[i] == '>'))
+ i++;
+
+ i++;
+
+ if (i < size)
+ j = is_empty(data + i, size - i);
+
+ if (j) {
+ work.size = i + j;
+ if (do_render && doc->md.blockhtml)
+ doc->md.blockhtml(ob, &work, &doc->data);
+ return work.size;
+ }
+ }
+
+ /* HR, which is the only self-closing block tag considered */
+ if (size > 4 && (data[1] == 'h' || data[1] == 'H') &&
+ (data[2] == 'r' || data[2] == 'R')) {
+ i = 3;
+ while (i < size && data[i] != '>')
+ i++;
+
+ if (i + 1 < size) {
+ i++;
+ j = is_empty(data + i, size - i);
+ if (j) {
+ work.size = i + j;
+ if (do_render && doc->md.blockhtml)
+ doc->md.blockhtml(ob, &work, &doc->data);
+ return work.size;
+ }
+ }
+ }
+
+ /* no special case recognised */
+ return 0;
+ }
+
+ /* looking for a matching closing tag in strict mode */
+ tag_len = strlen(curtag);
+ tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size);
+
+ /* if not found, trying a second pass looking for indented match */
+ /* but not if tag is "ins" or "del" (following original Markdown.pl) */
+ if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0)
+ tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size);
+
+ if (!tag_end)
+ return 0;
+
+ /* the end of the block has been found */
+ work.size = tag_end;
+ if (do_render && doc->md.blockhtml)
+ doc->md.blockhtml(ob, &work, &doc->data);
+
+ return tag_end;
+}
+
+static void parse_table_row(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, size_t columns,
+ hoedown_table_flags* col_data,
+ hoedown_table_flags header_flag)
+{
+ size_t i = 0, col, len;
+ hoedown_buffer* row_work = 0;
+
+ if (!doc->md.table_cell || !doc->md.table_row)
+ return;
+
+ row_work = newbuf(doc, BUFFER_SPAN);
+
+ if (i < size && data[i] == '|')
+ i++;
+
+ for (col = 0; col < columns && i < size; ++col) {
+ size_t cell_start, cell_end;
+ hoedown_buffer* cell_work;
+
+ cell_work = newbuf(doc, BUFFER_SPAN);
+
+ while (i < size && _isspace(data[i]))
+ i++;
+
+ cell_start = i;
+
+ len = find_emph_char(data + i, size - i, '|');
+ i += len ? len : size - i;
+
+ cell_end = i - 1;
+
+ while (cell_end > cell_start && _isspace(data[cell_end]))
+ cell_end--;
+
+ parse_inline(cell_work, doc, data + cell_start,
+ 1 + cell_end - cell_start);
+ doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag,
+ &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+ i++;
+ }
+
+ for (; col < columns; ++col) {
+ hoedown_buffer empty_cell = {0, 0, 0, 0, NULL, NULL, NULL};
+ doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag,
+ &doc->data);
+ }
+
+ doc->md.table_row(ob, row_work, &doc->data);
+
+ popbuf(doc, BUFFER_SPAN);
+}
+
+static size_t parse_table_header(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size, size_t* columns,
+ hoedown_table_flags** column_data)
+{
+ int pipes;
+ size_t i = 0, col, header_end, under_end;
+
+ pipes = 0;
+ while (i < size && data[i] != '\n')
+ if (data[i++] == '|')
+ pipes++;
+
+ if (i == size || pipes == 0)
+ return 0;
+
+ header_end = i;
+
+ while (header_end > 0 && _isspace(data[header_end - 1]))
+ header_end--;
+
+ if (data[0] == '|')
+ pipes--;
+
+ if (header_end && data[header_end - 1] == '|')
+ pipes--;
+
+ if (pipes < 0)
+ return 0;
+
+ *columns = pipes + 1;
+ *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags));
+
+ /* Parse the header underline */
+ i++;
+ if (i < size && data[i] == '|')
+ i++;
+
+ under_end = i;
+ while (under_end < size && data[under_end] != '\n')
+ under_end++;
+
+ for (col = 0; col < *columns && i < under_end; ++col) {
+ size_t dashes = 0;
+
+ while (i < under_end && data[i] == ' ')
+ i++;
+
+ if (data[i] == ':') {
+ i++;
+ (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT;
+ dashes++;
+ }
+
+ while (i < under_end && data[i] == '-') {
+ i++;
+ dashes++;
+ }
+
+ if (i < under_end && data[i] == ':') {
+ i++;
+ (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT;
+ dashes++;
+ }
+
+ while (i < under_end && data[i] == ' ')
+ i++;
+
+ if (i < under_end && data[i] != '|' && data[i] != '+')
+ break;
+
+ if (dashes < 3)
+ break;
+
+ i++;
+ }
+
+ if (col < *columns)
+ return 0;
+
+ parse_table_row(ob, doc, data, header_end, *columns, *column_data,
+ HOEDOWN_TABLE_HEADER);
+
+ return under_end + 1;
+}
+
+static size_t parse_table(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t i;
+
+ hoedown_buffer* work = 0;
+ hoedown_buffer* header_work = 0;
+ hoedown_buffer* body_work = 0;
+
+ size_t columns;
+ hoedown_table_flags* col_data = NULL;
+
+ work = newbuf(doc, BUFFER_BLOCK);
+ header_work = newbuf(doc, BUFFER_SPAN);
+ body_work = newbuf(doc, BUFFER_BLOCK);
+
+ i = parse_table_header(header_work, doc, data, size, &columns, &col_data);
+ if (i > 0) {
+
+ while (i < size) {
+ size_t row_start;
+ int pipes = 0;
+
+ row_start = i;
+
+ while (i < size && data[i] != '\n')
+ if (data[i++] == '|')
+ pipes++;
+
+ if (pipes == 0 || i == size) {
+ i = row_start;
+ break;
+ }
+
+ parse_table_row(body_work, doc, data + row_start, i - row_start,
+ columns, col_data, 0);
+
+ i++;
+ }
+
+ if (doc->md.table_header)
+ doc->md.table_header(work, header_work, &doc->data);
+
+ if (doc->md.table_body)
+ doc->md.table_body(work, body_work, &doc->data);
+
+ if (doc->md.table)
+ doc->md.table(ob, work, &doc->data);
+ }
+
+ free(col_data);
+ popbuf(doc, BUFFER_SPAN);
+ popbuf(doc, BUFFER_BLOCK);
+ popbuf(doc, BUFFER_BLOCK);
+ return i;
+}
+
+/* parse_block • parsing of one block, returning next uint8_t to parse */
+static void parse_block(hoedown_buffer* ob, hoedown_document* doc,
+ uint8_t* data, size_t size)
+{
+ size_t beg, end, i;
+ uint8_t* txt_data;
+ beg = 0;
+
+ if (doc->work_bufs[BUFFER_SPAN].size + doc->work_bufs[BUFFER_BLOCK].size >
+ doc->max_nesting)
+ return;
+
+ while (beg < size) {
+ txt_data = data + beg;
+ end = size - beg;
+
+ if (is_atxheader(doc, txt_data, end))
+ beg += parse_atxheader(ob, doc, txt_data, end);
+
+ else if (data[beg] == '<' && doc->md.blockhtml &&
+ (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0)
+ beg += i;
+
+ else if ((i = is_empty(txt_data, end)) != 0)
+ beg += i;
+
+ else if (is_hrule(txt_data, end)) {
+ if (doc->md.hrule)
+ doc->md.hrule(ob, &doc->data);
+
+ while (beg < size && data[beg] != '\n')
+ beg++;
+
+ beg++;
+ }
+
+ else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 &&
+ (i = parse_fencedcode(ob, doc, txt_data, end)) != 0)
+ beg += i;
+
+ else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 &&
+ (i = parse_table(ob, doc, txt_data, end)) != 0)
+ beg += i;
+
+ else if (prefix_quote(txt_data, end))
+ beg += parse_blockquote(ob, doc, txt_data, end);
+
+ else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) &&
+ prefix_code(txt_data, end))
+ beg += parse_blockcode(ob, doc, txt_data, end);
+
+ else if (prefix_uli(txt_data, end))
+ beg += parse_list(ob, doc, txt_data, end, 0);
+
+ else if (prefix_oli(txt_data, end))
+ beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED);
+
+ else
+ beg += parse_paragraph(ob, doc, txt_data, end);
+ }
+}
+
+/*********************
+ * REFERENCE PARSING *
+ *********************/
+
+/* is_footnote • returns whether a line is a footnote definition or not */
+static int is_footnote(const uint8_t* data, size_t beg, size_t end,
+ size_t* last, struct footnote_list* list)
+{
+ size_t i = 0;
+ hoedown_buffer* contents = 0;
+ size_t ind = 0;
+ int in_empty = 0;
+ size_t start = 0;
+
+ size_t id_offset, id_end;
+
+ /* up to 3 optional leading spaces */
+ if (beg + 3 >= end)
+ return 0;
+ if (data[beg] == ' ') {
+ i = 1;
+ if (data[beg + 1] == ' ') {
+ i = 2;
+ if (data[beg + 2] == ' ') {
+ i = 3;
+ if (data[beg + 3] == ' ')
+ return 0;
+ }
+ }
+ }
+ i += beg;
+
+ /* id part: caret followed by anything between brackets */
+ if (data[i] != '[')
+ return 0;
+ i++;
+ if (i >= end || data[i] != '^')
+ return 0;
+ i++;
+ id_offset = i;
+ while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
+ i++;
+ if (i >= end || data[i] != ']')
+ return 0;
+ id_end = i;
+
+ /* spacer: colon (space | tab)* newline? (space | tab)* */
+ i++;
+ if (i >= end || data[i] != ':')
+ return 0;
+ i++;
+
+ /* getting content buffer */
+ contents = hoedown_buffer_new(64);
+
+ start = i;
+
+ /* process lines similar to a list item */
+ while (i < end) {
+ while (i < end && data[i] != '\n' && data[i] != '\r')
+ i++;
+
+ /* process an empty line */
+ if (is_empty(data + start, i - start)) {
+ in_empty = 1;
+ if (i < end && (data[i] == '\n' || data[i] == '\r')) {
+ i++;
+ if (i < end && data[i] == '\n' && data[i - 1] == '\r')
+ i++;
+ }
+ start = i;
+ continue;
+ }
+
+ /* calculating the indentation */
+ ind = 0;
+ while (ind < 4 && start + ind < end && data[start + ind] == ' ')
+ ind++;
+
+ /* joining only indented stuff after empty lines;
+ * note that now we only require 1 space of indentation
+ * to continue, just like lists */
+ if (ind == 0) {
+ if (start == id_end + 2 && data[start] == '\t') {
+ } else
+ break;
+ } else if (in_empty) {
+ hoedown_buffer_putc(contents, '\n');
+ }
+
+ in_empty = 0;
+
+ /* adding the line into the content buffer */
+ hoedown_buffer_put(contents, data + start + ind, i - start - ind);
+ /* add carriage return */
+ if (i < end) {
+ hoedown_buffer_putc(contents, '\n');
+ if (i < end && (data[i] == '\n' || data[i] == '\r')) {
+ i++;
+ if (i < end && data[i] == '\n' && data[i - 1] == '\r')
+ i++;
+ }
+ }
+ start = i;
+ }
+
+ if (last)
+ *last = start;
+
+ if (list) {
+ struct footnote_ref* ref;
+ ref = create_footnote_ref(list, data + id_offset, id_end - id_offset);
+ if (!ref)
+ return 0;
+ if (!add_footnote_ref(list, ref)) {
+ free_footnote_ref(ref);
+ return 0;
+ }
+ ref->contents = contents;
+ }
+
+ return 1;
+}
+
+/* is_ref • returns whether a line is a reference or not */
+static int is_ref(const uint8_t* data, size_t beg, size_t end, size_t* last,
+ struct link_ref** refs)
+{
+ /* int n; */
+ size_t i = 0;
+ size_t id_offset, id_end;
+ size_t link_offset, link_end;
+ size_t title_offset, title_end;
+ size_t line_end;
+
+ /* up to 3 optional leading spaces */
+ if (beg + 3 >= end)
+ return 0;
+ if (data[beg] == ' ') {
+ i = 1;
+ if (data[beg + 1] == ' ') {
+ i = 2;
+ if (data[beg + 2] == ' ') {
+ i = 3;
+ if (data[beg + 3] == ' ')
+ return 0;
+ }
+ }
+ }
+ i += beg;
+
+ /* id part: anything but a newline between brackets */
+ if (data[i] != '[')
+ return 0;
+ i++;
+ id_offset = i;
+ while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
+ i++;
+ if (i >= end || data[i] != ']')
+ return 0;
+ id_end = i;
+
+ /* spacer: colon (space | tab)* newline? (space | tab)* */
+ i++;
+ if (i >= end || data[i] != ':')
+ return 0;
+ i++;
+ while (i < end && data[i] == ' ')
+ i++;
+ if (i < end && (data[i] == '\n' || data[i] == '\r')) {
+ i++;
+ if (i < end && data[i] == '\r' && data[i - 1] == '\n')
+ i++;
+ }
+ while (i < end && data[i] == ' ')
+ i++;
+ if (i >= end)
+ return 0;
+
+ /* link: spacing-free sequence, optionally between angle brackets */
+ if (data[i] == '<')
+ i++;
+
+ link_offset = i;
+
+ while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r')
+ i++;
+
+ if (data[i - 1] == '>')
+ link_end = i - 1;
+ else
+ link_end = i;
+
+ /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
+ while (i < end && data[i] == ' ')
+ i++;
+ if (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' &&
+ data[i] != '"' && data[i] != '(')
+ return 0;
+ line_end = 0;
+ /* computing end-of-line */
+ if (i >= end || data[i] == '\r' || data[i] == '\n')
+ line_end = i;
+ if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
+ line_end = i + 1;
+
+ /* optional (space|tab)* spacer after a newline */
+ if (line_end) {
+ i = line_end + 1;
+ while (i < end && data[i] == ' ')
+ i++;
+ }
+
+ /* optional title: any non-newline sequence enclosed in '"()
+ alone on its line */
+ title_offset = title_end = 0;
+ if (i + 1 < end && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) {
+ i++;
+ title_offset = i;
+ /* looking for EOL */
+ while (i < end && data[i] != '\n' && data[i] != '\r')
+ i++;
+ if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
+ title_end = i + 1;
+ else
+ title_end = i;
+ /* stepping back */
+ i -= 1;
+ while (i > title_offset && data[i] == ' ')
+ i -= 1;
+ if (i > title_offset &&
+ (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
+ line_end = title_end;
+ title_end = i;
+ }
+ }
+
+ if (!line_end || link_end == link_offset)
+ return 0; /* garbage after the link empty link */
+
+ /* a valid ref has been found, filling-in return structures */
+ if (last)
+ *last = line_end;
+
+ if (refs) {
+ struct link_ref* ref;
+
+ ref = add_link_ref(refs, data + id_offset, id_end - id_offset);
+ if (!ref)
+ return 0;
+
+ ref->link = hoedown_buffer_new(link_end - link_offset);
+ hoedown_buffer_put(ref->link, data + link_offset,
+ link_end - link_offset);
+
+ if (title_end > title_offset) {
+ ref->title = hoedown_buffer_new(title_end - title_offset);
+ hoedown_buffer_put(ref->title, data + title_offset,
+ title_end - title_offset);
+ }
+ }
+
+ return 1;
+}
+
+static void expand_tabs(hoedown_buffer* ob, const uint8_t* line, size_t size)
+{
+ /* This code makes two assumptions:
+ * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped,
+ * whether or not it is a valid UTF-8 continuation byte.)
+ * - Input contains no combining characters. (Combining characters
+ * should be skipped but are not.)
+ */
+ size_t i = 0, tab = 0;
+
+ while (i < size) {
+ size_t org = i;
+
+ while (i < size && line[i] != '\t') {
+ /* ignore UTF-8 continuation bytes */
+ if ((line[i] & 0xc0) != 0x80)
+ tab++;
+ i++;
+ }
+
+ if (i > org)
+ hoedown_buffer_put(ob, line + org, i - org);
+
+ if (i >= size)
+ break;
+
+ do {
+ hoedown_buffer_putc(ob, ' ');
+ tab++;
+ } while (tab % 4);
+
+ i++;
+ }
+}
+
+/**********************
+ * EXPORTED FUNCTIONS *
+ **********************/
+
+hoedown_document* hoedown_document_new(const hoedown_renderer* renderer,
+ hoedown_extensions extensions,
+ size_t max_nesting)
+{
+ hoedown_document* doc = NULL;
+
+ assert(max_nesting > 0 && renderer);
+
+ doc = hoedown_malloc(sizeof(hoedown_document));
+ memcpy(&doc->md, renderer, sizeof(hoedown_renderer));
+
+ doc->data.opaque = renderer->opaque;
+
+ hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4);
+ hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8);
+
+ memset(doc->active_char, 0x0, 256);
+
+ if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) {
+ doc->active_char['_'] = MD_CHAR_EMPHASIS;
+ }
+
+ if (doc->md.emphasis || doc->md.double_emphasis ||
+ doc->md.triple_emphasis) {
+ doc->active_char['*'] = MD_CHAR_EMPHASIS;
+ doc->active_char['_'] = MD_CHAR_EMPHASIS;
+ if (extensions & HOEDOWN_EXT_STRIKETHROUGH)
+ doc->active_char['~'] = MD_CHAR_EMPHASIS;
+ if (extensions & HOEDOWN_EXT_HIGHLIGHT)
+ doc->active_char['='] = MD_CHAR_EMPHASIS;
+ }
+
+ if (doc->md.codespan)
+ doc->active_char['`'] = MD_CHAR_CODESPAN;
+
+ if (doc->md.linebreak)
+ doc->active_char['\n'] = MD_CHAR_LINEBREAK;
+
+ if (doc->md.image || doc->md.link || doc->md.footnotes ||
+ doc->md.footnote_ref)
+ doc->active_char['['] = MD_CHAR_LINK;
+
+ doc->active_char['<'] = MD_CHAR_LANGLE;
+ doc->active_char['\\'] = MD_CHAR_ESCAPE;
+ doc->active_char['&'] = MD_CHAR_ENTITY;
+
+ if (extensions & HOEDOWN_EXT_AUTOLINK) {
+ doc->active_char[':'] = MD_CHAR_AUTOLINK_URL;
+ doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
+ doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW;
+ }
+
+ if (extensions & HOEDOWN_EXT_SUPERSCRIPT)
+ doc->active_char['^'] = MD_CHAR_SUPERSCRIPT;
+
+ if (extensions & HOEDOWN_EXT_QUOTE)
+ doc->active_char['"'] = MD_CHAR_QUOTE;
+
+ if (extensions & HOEDOWN_EXT_MATH)
+ doc->active_char['$'] = MD_CHAR_MATH;
+
+ /* Extension data */
+ doc->ext_flags = extensions;
+ doc->max_nesting = max_nesting;
+ doc->in_link_body = 0;
+
+ return doc;
+}
+
+void hoedown_document_render(hoedown_document* doc, hoedown_buffer* ob,
+ const uint8_t* data, size_t size)
+{
+ static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF};
+
+ hoedown_buffer* text;
+ size_t beg, end;
+
+ int footnotes_enabled;
+
+ text = hoedown_buffer_new(64);
+
+ /* Preallocate enough space for our buffer to avoid expanding while copying
+ */
+ hoedown_buffer_grow(text, size);
+
+ /* reset the references table */
+ memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void*));
+
+ footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES;
+
+ /* reset the footnotes lists */
+ if (footnotes_enabled) {
+ memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found));
+ memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used));
+ }
+
+ /* first pass: looking for references, copying everything else */
+ beg = 0;
+
+ /* Skip a possible UTF-8 BOM, even though the Unicode standard
+ * discourages having these in UTF-8 documents */
+ if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0)
+ beg += 3;
+
+ while (beg < size) /* iterating over lines */
+ if (footnotes_enabled &&
+ is_footnote(data, beg, size, &end, &doc->footnotes_found))
+ beg = end;
+ else if (is_ref(data, beg, size, &end, doc->refs))
+ beg = end;
+ else { /* skipping to the next line */
+ end = beg;
+ while (end < size && data[end] != '\n' && data[end] != '\r')
+ end++;
+
+ /* adding the line body if present */
+ if (end > beg)
+ expand_tabs(text, data + beg, end - beg);
+
+ while (end < size && (data[end] == '\n' || data[end] == '\r')) {
+ /* add one \n per newline */
+ if (data[end] == '\n' ||
+ (end + 1 < size && data[end + 1] != '\n'))
+ hoedown_buffer_putc(text, '\n');
+ end++;
+ }
+
+ beg = end;
+ }
+
+ /* pre-grow the output buffer to minimize allocations */
+ hoedown_buffer_grow(ob, text->size + (text->size >> 1));
+
+ /* second pass: actual rendering */
+ if (doc->md.doc_header)
+ doc->md.doc_header(ob, 0, &doc->data);
+
+ if (text->size) {
+ /* adding a final newline if not already present */
+ if (text->data[text->size - 1] != '\n' &&
+ text->data[text->size - 1] != '\r')
+ hoedown_buffer_putc(text, '\n');
+
+ parse_block(ob, doc, text->data, text->size);
+ }
+
+ /* footnotes */
+ if (footnotes_enabled)
+ parse_footnote_list(ob, doc, &doc->footnotes_used);
+
+ if (doc->md.doc_footer)
+ doc->md.doc_footer(ob, 0, &doc->data);
+
+ /* clean-up */
+ hoedown_buffer_free(text);
+ free_link_refs(doc->refs);
+ if (footnotes_enabled) {
+ free_footnote_list(&doc->footnotes_found, 1);
+ free_footnote_list(&doc->footnotes_used, 0);
+ }
+
+ assert(doc->work_bufs[BUFFER_SPAN].size == 0);
+ assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
+}
+
+void hoedown_document_render_inline(hoedown_document* doc, hoedown_buffer* ob,
+ const uint8_t* data, size_t size)
+{
+ size_t i = 0, mark;
+ hoedown_buffer* text = hoedown_buffer_new(64);
+
+ /* reset the references table */
+ memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void*));
+
+ /* first pass: expand tabs and process newlines */
+ hoedown_buffer_grow(text, size);
+ while (1) {
+ mark = i;
+ while (i < size && data[i] != '\n' && data[i] != '\r')
+ i++;
+
+ expand_tabs(text, data + mark, i - mark);
+
+ if (i >= size)
+ break;
+
+ while (i < size && (data[i] == '\n' || data[i] == '\r')) {
+ /* add one \n per newline */
+ if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n'))
+ hoedown_buffer_putc(text, '\n');
+ i++;
+ }
+ }
+
+ /* second pass: actual rendering */
+ hoedown_buffer_grow(ob, text->size + (text->size >> 1));
+
+ if (doc->md.doc_header)
+ doc->md.doc_header(ob, 1, &doc->data);
+
+ parse_inline(ob, doc, text->data, text->size);
+
+ if (doc->md.doc_footer)
+ doc->md.doc_footer(ob, 1, &doc->data);
+
+ /* clean-up */
+ hoedown_buffer_free(text);
+
+ assert(doc->work_bufs[BUFFER_SPAN].size == 0);
+ assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
+}
+
+void hoedown_document_free(hoedown_document* doc)
+{
+ size_t i;
+
+ for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i)
+ hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]);
+
+ for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i)
+ hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]);
+
+ hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]);
+ hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]);
+
+ free(doc);
+}
diff --git a/meshmc/libraries/hoedown/src/escape.c b/meshmc/libraries/hoedown/src/escape.c
new file mode 100644
index 0000000000..14a3dfa153
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/escape.c
@@ -0,0 +1,191 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/escape.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#define likely(x) __builtin_expect((x), 1)
+#define unlikely(x) __builtin_expect((x), 0)
+
+/*
+ * The following characters will not be escaped:
+ *
+ * -_.+!*'(),%#@?=;:/,+&$ alphanum
+ *
+ * Note that this character set is the addition of:
+ *
+ * - The characters which are safe to be in an URL
+ * - The characters which are *not* safe to be in
+ * an URL because they are RESERVED characters.
+ *
+ * We assume (lazily) that any RESERVED char that
+ * appears inside an URL is actually meant to
+ * have its native function (i.e. as an URL
+ * component/separator) and hence needs no escaping.
+ *
+ * There are two exceptions: the chacters & (amp)
+ * and ' (single quote) do not appear in the table.
+ * They are meant to appear in the URL as components,
+ * yet they require special HTML-entity escaping
+ * to generate valid HTML markup.
+ *
+ * All other characters will be escaped to %XX.
+ *
+ */
+static const uint8_t HREF_SAFE[UINT8_MAX + 1] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+void hoedown_escape_href(hoedown_buffer* ob, const uint8_t* data, size_t size)
+{
+ static const char hex_chars[] = "0123456789ABCDEF";
+ size_t i = 0, mark;
+ char hex_str[3];
+
+ hex_str[0] = '%';
+
+ while (i < size) {
+ mark = i;
+ while (i < size && HREF_SAFE[data[i]])
+ i++;
+
+ /* Optimization for cases where there's nothing to escape */
+ if (mark == 0 && i >= size) {
+ hoedown_buffer_put(ob, data, size);
+ return;
+ }
+
+ if (likely(i > mark)) {
+ hoedown_buffer_put(ob, data + mark, i - mark);
+ }
+
+ /* escaping */
+ if (i >= size)
+ break;
+
+ switch (data[i]) {
+ /* amp appears all the time in URLs, but needs
+ * HTML-entity escaping to be inside an href */
+ case '&':
+ HOEDOWN_BUFPUTSL(ob, "&amp;");
+ break;
+
+ /* the single quote is a valid URL character
+ * according to the standard; it needs HTML
+ * entity escaping too */
+ case '\'':
+ HOEDOWN_BUFPUTSL(ob, "&#x27;");
+ break;
+
+ /* the space can be escaped to %20 or a plus
+ * sign. we're going with the generic escape
+ * for now. the plus thing is more commonly seen
+ * when building GET strings */
+#if 0
+ case ' ':
+ hoedown_buffer_putc(ob, '+');
+ break;
+#endif
+
+ /* every other character goes with a %XX escaping */
+ default:
+ hex_str[1] = hex_chars[(data[i] >> 4) & 0xF];
+ hex_str[2] = hex_chars[data[i] & 0xF];
+ hoedown_buffer_put(ob, (uint8_t*)hex_str, 3);
+ }
+
+ i++;
+ }
+}
+
+/**
+ * According to the OWASP rules:
+ *
+ * & --> &amp;
+ * < --> &lt;
+ * > --> &gt;
+ * " --> &quot;
+ * ' --> &#x27; &apos; is not recommended
+ * / --> &#x2F; forward slash is included as it helps end an HTML entity
+ *
+ */
+static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX + 1] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static const char* HTML_ESCAPES[] = {"", "&quot;", "&amp;", "&#39;",
+ "&#47;", "&lt;", "&gt;"};
+
+void hoedown_escape_html(hoedown_buffer* ob, const uint8_t* data, size_t size,
+ int secure)
+{
+ size_t i = 0, mark;
+
+ while (1) {
+ mark = i;
+ while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0)
+ i++;
+
+ /* Optimization for cases where there's nothing to escape */
+ if (mark == 0 && i >= size) {
+ hoedown_buffer_put(ob, data, size);
+ return;
+ }
+
+ if (likely(i > mark))
+ hoedown_buffer_put(ob, data + mark, i - mark);
+
+ if (i >= size)
+ break;
+
+ /* The forward slash is only escaped in secure mode */
+ if (!secure && data[i] == '/') {
+ hoedown_buffer_putc(ob, '/');
+ } else {
+ hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]);
+ }
+
+ i++;
+ }
+}
diff --git a/meshmc/libraries/hoedown/src/html.c b/meshmc/libraries/hoedown/src/html.c
new file mode 100644
index 0000000000..2778e7633c
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/html.c
@@ -0,0 +1,823 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/html.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#include "hoedown/escape.h"
+
+#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML)
+
+hoedown_html_tag hoedown_html_is_tag(const uint8_t* data, size_t size,
+ const char* tagname)
+{
+ size_t i;
+ int closed = 0;
+
+ if (size < 3 || data[0] != '<')
+ return HOEDOWN_HTML_TAG_NONE;
+
+ i = 1;
+
+ if (data[i] == '/') {
+ closed = 1;
+ i++;
+ }
+
+ for (; i < size; ++i, ++tagname) {
+ if (*tagname == 0)
+ break;
+
+ if (data[i] != *tagname)
+ return HOEDOWN_HTML_TAG_NONE;
+ }
+
+ if (i == size)
+ return HOEDOWN_HTML_TAG_NONE;
+
+ if (isspace(data[i]) || data[i] == '>')
+ return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN;
+
+ return HOEDOWN_HTML_TAG_NONE;
+}
+
+static void escape_html(hoedown_buffer* ob, const uint8_t* source,
+ size_t length)
+{
+ hoedown_escape_html(ob, source, length, 0);
+}
+
+static void escape_href(hoedown_buffer* ob, const uint8_t* source,
+ size_t length)
+{
+ hoedown_escape_href(ob, source, length);
+}
+
+/********************
+ * GENERIC RENDERER *
+ ********************/
+static int rndr_autolink(hoedown_buffer* ob, const hoedown_buffer* link,
+ hoedown_autolink_type type,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ if (!link || !link->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<a href=\"");
+ if (type == HOEDOWN_AUTOLINK_EMAIL)
+ HOEDOWN_BUFPUTSL(ob, "mailto:");
+ escape_href(ob, link->data, link->size);
+
+ if (state->link_attributes) {
+ hoedown_buffer_putc(ob, '\"');
+ state->link_attributes(ob, link, data);
+ hoedown_buffer_putc(ob, '>');
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "\">");
+ }
+
+ /*
+ * Pretty printing: if we get an email address as
+ * an actual URI, e.g. `mailto:foo@bar.com`, we don't
+ * want to print the `mailto:` prefix
+ */
+ if (hoedown_buffer_prefix(link, "mailto:") == 0) {
+ escape_html(ob, link->data + 7, link->size - 7);
+ } else {
+ escape_html(ob, link->data, link->size);
+ }
+
+ HOEDOWN_BUFPUTSL(ob, "</a>");
+
+ return 1;
+}
+
+static void rndr_blockcode(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_buffer* lang,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+
+ if (lang) {
+ HOEDOWN_BUFPUTSL(ob, "<pre><code class=\"language-");
+ escape_html(ob, lang->data, lang->size);
+ HOEDOWN_BUFPUTSL(ob, "\">");
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "<pre><code>");
+ }
+
+ if (text)
+ escape_html(ob, text->data, text->size);
+
+ HOEDOWN_BUFPUTSL(ob, "</code></pre>\n");
+}
+
+static void rndr_blockquote(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ HOEDOWN_BUFPUTSL(ob, "<blockquote>\n");
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</blockquote>\n");
+}
+
+static int rndr_codespan(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data)
+{
+ HOEDOWN_BUFPUTSL(ob, "<code>");
+ if (text)
+ escape_html(ob, text->data, text->size);
+ HOEDOWN_BUFPUTSL(ob, "</code>");
+ return 1;
+}
+
+static int rndr_strikethrough(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<del>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</del>");
+ return 1;
+}
+
+static int rndr_double_emphasis(hoedown_buffer* ob,
+ const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<strong>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</strong>");
+
+ return 1;
+}
+
+static int rndr_emphasis(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+ HOEDOWN_BUFPUTSL(ob, "<em>");
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</em>");
+ return 1;
+}
+
+static int rndr_underline(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<u>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</u>");
+
+ return 1;
+}
+
+static int rndr_highlight(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<mark>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</mark>");
+
+ return 1;
+}
+
+static int rndr_quote(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<q>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</q>");
+
+ return 1;
+}
+
+static int rndr_linebreak(hoedown_buffer* ob, const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+ hoedown_buffer_puts(ob, USE_XHTML(state) ? "<br/>\n" : "<br>\n");
+ return 1;
+}
+
+static void rndr_header(hoedown_buffer* ob, const hoedown_buffer* content,
+ int level, const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+
+ if (level <= state->toc_data.nesting_level)
+ hoedown_buffer_printf(ob, "<h%d id=\"toc_%d\">", level,
+ state->toc_data.header_count++);
+ else
+ hoedown_buffer_printf(ob, "<h%d>", level);
+
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ hoedown_buffer_printf(ob, "</h%d>\n", level);
+}
+
+static int rndr_link(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_buffer* link, const hoedown_buffer* title,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ HOEDOWN_BUFPUTSL(ob, "<a href=\"");
+
+ if (link && link->size)
+ escape_href(ob, link->data, link->size);
+
+ if (title && title->size) {
+ HOEDOWN_BUFPUTSL(ob, "\" title=\"");
+ escape_html(ob, title->data, title->size);
+ }
+
+ if (state->link_attributes) {
+ hoedown_buffer_putc(ob, '\"');
+ state->link_attributes(ob, link, data);
+ hoedown_buffer_putc(ob, '>');
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "\">");
+ }
+
+ if (content && content->size)
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</a>");
+ return 1;
+}
+
+static void rndr_list(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_list_flags flags,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ hoedown_buffer_put(
+ ob,
+ (const uint8_t*)(flags & HOEDOWN_LIST_ORDERED ? "<ol>\n" : "<ul>\n"),
+ 5);
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ hoedown_buffer_put(
+ ob,
+ (const uint8_t*)(flags & HOEDOWN_LIST_ORDERED ? "</ol>\n" : "</ul>\n"),
+ 6);
+}
+
+static void rndr_listitem(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_list_flags flags,
+ const hoedown_renderer_data* data)
+{
+ HOEDOWN_BUFPUTSL(ob, "<li>");
+ if (content) {
+ size_t size = content->size;
+ while (size && content->data[size - 1] == '\n')
+ size--;
+
+ hoedown_buffer_put(ob, content->data, size);
+ }
+ HOEDOWN_BUFPUTSL(ob, "</li>\n");
+}
+
+static void rndr_paragraph(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+ size_t i = 0;
+
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+
+ if (!content || !content->size)
+ return;
+
+ while (i < content->size && isspace(content->data[i]))
+ i++;
+
+ if (i == content->size)
+ return;
+
+ HOEDOWN_BUFPUTSL(ob, "<p>");
+ if (state->flags & HOEDOWN_HTML_HARD_WRAP) {
+ size_t org;
+ while (i < content->size) {
+ org = i;
+ while (i < content->size && content->data[i] != '\n')
+ i++;
+
+ if (i > org)
+ hoedown_buffer_put(ob, content->data + org, i - org);
+
+ /*
+ * do not insert a line break if this newline
+ * is the last character on the paragraph
+ */
+ if (i >= content->size - 1)
+ break;
+
+ rndr_linebreak(ob, data);
+ i++;
+ }
+ } else {
+ hoedown_buffer_put(ob, content->data + i, content->size - i);
+ }
+ HOEDOWN_BUFPUTSL(ob, "</p>\n");
+}
+
+static void rndr_raw_block(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data)
+{
+ size_t org, sz;
+
+ if (!text)
+ return;
+
+ /* FIXME: Do we *really* need to trim the HTML? How does that make a
+ * difference? */
+ sz = text->size;
+ while (sz > 0 && text->data[sz - 1] == '\n')
+ sz--;
+
+ org = 0;
+ while (org < sz && text->data[org] == '\n')
+ org++;
+
+ if (org >= sz)
+ return;
+
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+
+ hoedown_buffer_put(ob, text->data + org, sz - org);
+ hoedown_buffer_putc(ob, '\n');
+}
+
+static int rndr_triple_emphasis(hoedown_buffer* ob,
+ const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+ HOEDOWN_BUFPUTSL(ob, "<strong><em>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</em></strong>");
+ return 1;
+}
+
+static void rndr_hrule(hoedown_buffer* ob, const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
+}
+
+static int rndr_image(hoedown_buffer* ob, const hoedown_buffer* link,
+ const hoedown_buffer* title, const hoedown_buffer* alt,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+ if (!link || !link->size)
+ return 0;
+
+ HOEDOWN_BUFPUTSL(ob, "<img src=\"");
+ escape_href(ob, link->data, link->size);
+ HOEDOWN_BUFPUTSL(ob, "\" alt=\"");
+
+ if (alt && alt->size)
+ escape_html(ob, alt->data, alt->size);
+
+ if (title && title->size) {
+ HOEDOWN_BUFPUTSL(ob, "\" title=\"");
+ escape_html(ob, title->data, title->size);
+ }
+
+ hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">");
+ return 1;
+}
+
+static int rndr_raw_html(hoedown_buffer* ob, const hoedown_buffer* text,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ /* ESCAPE overrides SKIP_HTML. It doesn't look to see if
+ * there are any valid tags, just escapes all of them. */
+ if ((state->flags & HOEDOWN_HTML_ESCAPE) != 0) {
+ escape_html(ob, text->data, text->size);
+ return 1;
+ }
+
+ if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0)
+ return 1;
+
+ hoedown_buffer_put(ob, text->data, text->size);
+ return 1;
+}
+
+static void rndr_table(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ HOEDOWN_BUFPUTSL(ob, "<table>\n");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</table>\n");
+}
+
+static void rndr_table_header(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ HOEDOWN_BUFPUTSL(ob, "<thead>\n");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</thead>\n");
+}
+
+static void rndr_table_body(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ HOEDOWN_BUFPUTSL(ob, "<tbody>\n");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</tbody>\n");
+}
+
+static void rndr_tablerow(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ HOEDOWN_BUFPUTSL(ob, "<tr>\n");
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</tr>\n");
+}
+
+static void rndr_tablecell(hoedown_buffer* ob, const hoedown_buffer* content,
+ hoedown_table_flags flags,
+ const hoedown_renderer_data* data)
+{
+ if (flags & HOEDOWN_TABLE_HEADER) {
+ HOEDOWN_BUFPUTSL(ob, "<th");
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "<td");
+ }
+
+ switch (flags & HOEDOWN_TABLE_ALIGNMASK) {
+ case HOEDOWN_TABLE_ALIGN_CENTER:
+ HOEDOWN_BUFPUTSL(ob, " style=\"text-align: center\">");
+ break;
+
+ case HOEDOWN_TABLE_ALIGN_LEFT:
+ HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">");
+ break;
+
+ case HOEDOWN_TABLE_ALIGN_RIGHT:
+ HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">");
+ break;
+
+ default:
+ HOEDOWN_BUFPUTSL(ob, ">");
+ }
+
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+
+ if (flags & HOEDOWN_TABLE_HEADER) {
+ HOEDOWN_BUFPUTSL(ob, "</th>\n");
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "</td>\n");
+ }
+}
+
+static int rndr_superscript(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (!content || !content->size)
+ return 0;
+ HOEDOWN_BUFPUTSL(ob, "<sup>");
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</sup>");
+ return 1;
+}
+
+static void rndr_normal_text(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ if (content)
+ escape_html(ob, content->data, content->size);
+}
+
+static void rndr_footnotes(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ if (ob->size)
+ hoedown_buffer_putc(ob, '\n');
+ HOEDOWN_BUFPUTSL(ob, "<div class=\"footnotes\">\n");
+ hoedown_buffer_puts(ob, USE_XHTML(state) ? "<hr/>\n" : "<hr>\n");
+ HOEDOWN_BUFPUTSL(ob, "<ol>\n");
+
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+
+ HOEDOWN_BUFPUTSL(ob, "\n</ol>\n</div>\n");
+}
+
+static void rndr_footnote_def(hoedown_buffer* ob, const hoedown_buffer* content,
+ unsigned int num,
+ const hoedown_renderer_data* data)
+{
+ size_t i = 0;
+ int pfound = 0;
+
+ /* insert anchor at the end of first paragraph block */
+ if (content) {
+ while ((i + 3) < content->size) {
+ if (content->data[i++] != '<')
+ continue;
+ if (content->data[i++] != '/')
+ continue;
+ if (content->data[i++] != 'p' && content->data[i] != 'P')
+ continue;
+ if (content->data[i] != '>')
+ continue;
+ i -= 3;
+ pfound = 1;
+ break;
+ }
+ }
+
+ hoedown_buffer_printf(ob, "\n<li id=\"fn%d\">\n", num);
+ if (pfound) {
+ hoedown_buffer_put(ob, content->data, i);
+ hoedown_buffer_printf(
+ ob, "&nbsp;<a href=\"#fnref%d\" rev=\"footnote\">&#8617;</a>", num);
+ hoedown_buffer_put(ob, content->data + i, content->size - i);
+ } else if (content) {
+ hoedown_buffer_put(ob, content->data, content->size);
+ }
+ HOEDOWN_BUFPUTSL(ob, "</li>\n");
+}
+
+static int rndr_footnote_ref(hoedown_buffer* ob, unsigned int num,
+ const hoedown_renderer_data* data)
+{
+ hoedown_buffer_printf(
+ ob,
+ "<sup id=\"fnref%d\"><a href=\"#fn%d\" rel=\"footnote\">%d</a></sup>",
+ num, num, num);
+ return 1;
+}
+
+static int rndr_math(hoedown_buffer* ob, const hoedown_buffer* text,
+ int displaymode, const hoedown_renderer_data* data)
+{
+ hoedown_buffer_put(ob, (const uint8_t*)(displaymode ? "\\[" : "\\("), 2);
+ escape_html(ob, text->data, text->size);
+ hoedown_buffer_put(ob, (const uint8_t*)(displaymode ? "\\]" : "\\)"), 2);
+ return 1;
+}
+
+static void toc_header(hoedown_buffer* ob, const hoedown_buffer* content,
+ int level, const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state = data->opaque;
+
+ if (level <= state->toc_data.nesting_level) {
+ /* set the level offset if this is the first header
+ * we're parsing for the document */
+ if (state->toc_data.current_level == 0)
+ state->toc_data.level_offset = level - 1;
+
+ level -= state->toc_data.level_offset;
+
+ if (level > state->toc_data.current_level) {
+ while (level > state->toc_data.current_level) {
+ HOEDOWN_BUFPUTSL(ob, "<ul>\n<li>\n");
+ state->toc_data.current_level++;
+ }
+ } else if (level < state->toc_data.current_level) {
+ HOEDOWN_BUFPUTSL(ob, "</li>\n");
+ while (level < state->toc_data.current_level) {
+ HOEDOWN_BUFPUTSL(ob, "</ul>\n</li>\n");
+ state->toc_data.current_level--;
+ }
+ HOEDOWN_BUFPUTSL(ob, "<li>\n");
+ } else {
+ HOEDOWN_BUFPUTSL(ob, "</li>\n<li>\n");
+ }
+
+ hoedown_buffer_printf(ob, "<a href=\"#toc_%d\">",
+ state->toc_data.header_count++);
+ if (content)
+ hoedown_buffer_put(ob, content->data, content->size);
+ HOEDOWN_BUFPUTSL(ob, "</a>\n");
+ }
+}
+
+static int toc_link(hoedown_buffer* ob, const hoedown_buffer* content,
+ const hoedown_buffer* link, const hoedown_buffer* title,
+ const hoedown_renderer_data* data)
+{
+ if (content && content->size)
+ hoedown_buffer_put(ob, content->data, content->size);
+ return 1;
+}
+
+static void toc_finalize(hoedown_buffer* ob, int inline_render,
+ const hoedown_renderer_data* data)
+{
+ hoedown_html_renderer_state* state;
+
+ if (inline_render)
+ return;
+
+ state = data->opaque;
+
+ while (state->toc_data.current_level > 0) {
+ HOEDOWN_BUFPUTSL(ob, "</li>\n</ul>\n");
+ state->toc_data.current_level--;
+ }
+
+ state->toc_data.header_count = 0;
+}
+
+hoedown_renderer* hoedown_html_toc_renderer_new(int nesting_level)
+{
+ static const hoedown_renderer cb_default = {NULL,
+
+ NULL,
+ NULL,
+ toc_header,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+
+ NULL,
+ rndr_codespan,
+ rndr_double_emphasis,
+ rndr_emphasis,
+ rndr_underline,
+ rndr_highlight,
+ rndr_quote,
+ NULL,
+ NULL,
+ toc_link,
+ rndr_triple_emphasis,
+ rndr_strikethrough,
+ rndr_superscript,
+ NULL,
+ NULL,
+ NULL,
+
+ NULL,
+ rndr_normal_text,
+
+ NULL,
+ toc_finalize};
+
+ hoedown_html_renderer_state* state;
+ hoedown_renderer* renderer;
+
+ /* Prepare the state pointer */
+ state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
+ memset(state, 0x0, sizeof(hoedown_html_renderer_state));
+
+ state->toc_data.nesting_level = nesting_level;
+
+ /* Prepare the renderer */
+ renderer = hoedown_malloc(sizeof(hoedown_renderer));
+ memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
+
+ renderer->opaque = state;
+ return renderer;
+}
+
+hoedown_renderer* hoedown_html_renderer_new(hoedown_html_flags render_flags,
+ int nesting_level)
+{
+ static const hoedown_renderer cb_default = {NULL,
+
+ rndr_blockcode,
+ rndr_blockquote,
+ rndr_header,
+ rndr_hrule,
+ rndr_list,
+ rndr_listitem,
+ rndr_paragraph,
+ rndr_table,
+ rndr_table_header,
+ rndr_table_body,
+ rndr_tablerow,
+ rndr_tablecell,
+ rndr_footnotes,
+ rndr_footnote_def,
+ rndr_raw_block,
+
+ rndr_autolink,
+ rndr_codespan,
+ rndr_double_emphasis,
+ rndr_emphasis,
+ rndr_underline,
+ rndr_highlight,
+ rndr_quote,
+ rndr_image,
+ rndr_linebreak,
+ rndr_link,
+ rndr_triple_emphasis,
+ rndr_strikethrough,
+ rndr_superscript,
+ rndr_footnote_ref,
+ rndr_math,
+ rndr_raw_html,
+
+ NULL,
+ rndr_normal_text,
+
+ NULL,
+ NULL};
+
+ hoedown_html_renderer_state* state;
+ hoedown_renderer* renderer;
+
+ /* Prepare the state pointer */
+ state = hoedown_malloc(sizeof(hoedown_html_renderer_state));
+ memset(state, 0x0, sizeof(hoedown_html_renderer_state));
+
+ state->flags = render_flags;
+ state->toc_data.nesting_level = nesting_level;
+
+ /* Prepare the renderer */
+ renderer = hoedown_malloc(sizeof(hoedown_renderer));
+ memcpy(renderer, &cb_default, sizeof(hoedown_renderer));
+
+ if (render_flags & HOEDOWN_HTML_SKIP_HTML ||
+ render_flags & HOEDOWN_HTML_ESCAPE)
+ renderer->blockhtml = NULL;
+
+ renderer->opaque = state;
+ return renderer;
+}
+
+void hoedown_html_renderer_free(hoedown_renderer* renderer)
+{
+ free(renderer->opaque);
+ free(renderer);
+}
diff --git a/meshmc/libraries/hoedown/src/html_blocks.c b/meshmc/libraries/hoedown/src/html_blocks.c
new file mode 100644
index 0000000000..bfccee4855
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/html_blocks.c
@@ -0,0 +1,241 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* ANSI-C code produced by gperf version 3.0.3 */
+/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1
+ * --ignore-case -m100 html_block_names.gperf */
+/* Computed positions: -k'1-2' */
+
+#if !( \
+ (' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) && ('%' == 37) && \
+ ('&' == 38) && ('\'' == 39) && ('(' == 40) && (')' == 41) && \
+ ('*' == 42) && ('+' == 43) && (',' == 44) && ('-' == 45) && ('.' == 46) && \
+ ('/' == 47) && ('0' == 48) && ('1' == 49) && ('2' == 50) && ('3' == 51) && \
+ ('4' == 52) && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) && \
+ ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) && ('=' == 61) && \
+ ('>' == 62) && ('?' == 63) && ('A' == 65) && ('B' == 66) && ('C' == 67) && \
+ ('D' == 68) && ('E' == 69) && ('F' == 70) && ('G' == 71) && ('H' == 72) && \
+ ('I' == 73) && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) && \
+ ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) && ('R' == 82) && \
+ ('S' == 83) && ('T' == 84) && ('U' == 85) && ('V' == 86) && ('W' == 87) && \
+ ('X' == 88) && ('Y' == 89) && ('Z' == 90) && ('[' == 91) && \
+ ('\\' == 92) && (']' == 93) && ('^' == 94) && ('_' == 95) && \
+ ('a' == 97) && ('b' == 98) && ('c' == 99) && ('d' == 100) && \
+ ('e' == 101) && ('f' == 102) && ('g' == 103) && ('h' == 104) && \
+ ('i' == 105) && ('j' == 106) && ('k' == 107) && ('l' == 108) && \
+ ('m' == 109) && ('n' == 110) && ('o' == 111) && ('p' == 112) && \
+ ('q' == 113) && ('r' == 114) && ('s' == 115) && ('t' == 116) && \
+ ('u' == 117) && ('v' == 118) && ('w' == 119) && ('x' == 120) && \
+ ('y' == 121) && ('z' == 122) && ('{' == 123) && ('|' == 124) && \
+ ('}' == 125) && ('~' == 126))
+/* The character set is not based on ISO-646. */
+#error \
+ "gperf generated tables don't work with this execution character set. Please report a bug to <bug-gnu-gperf@gnu.org>."
+#endif
+
+/* maximum key range = 24, duplicates = 0 */
+
+#ifndef GPERF_DOWNCASE
+#define GPERF_DOWNCASE 1
+static unsigned char gperf_downcase[256] = {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
+ 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
+ 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
+ 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,
+ 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
+ 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+ 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
+ 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
+ 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
+ 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194,
+ 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
+ 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224,
+ 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+ 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254,
+ 255};
+#endif
+
+#ifndef GPERF_CASE_STRNCMP
+#define GPERF_CASE_STRNCMP 1
+static int gperf_case_strncmp(register const char* s1, register const char* s2,
+ register unsigned int n)
+{
+ for (; n > 0;) {
+ unsigned char c1 = gperf_downcase[(unsigned char)*s1++];
+ unsigned char c2 = gperf_downcase[(unsigned char)*s2++];
+ if (c1 != 0 && c1 == c2) {
+ n--;
+ continue;
+ }
+ return (int)c1 - (int)c2;
+ }
+ return 0;
+}
+#endif
+
+#ifdef __GNUC__
+__inline
+#else
+#ifdef __cplusplus
+inline
+#endif
+#endif
+ static unsigned int hash(register const char* str,
+ register unsigned int len)
+{
+ static const unsigned char asso_values[] = {
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 22, 21, 19, 18,
+ 16, 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 1, 25, 0, 25, 1, 0,
+ 0, 13, 0, 25, 25, 11, 2, 1, 0, 25, 25, 5, 0, 2, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 1, 25, 0, 25, 1, 0, 0, 13, 0, 25,
+ 25, 11, 2, 1, 0, 25, 25, 5, 0, 2, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+ 25, 25, 25, 25, 25};
+ register int hval = (int)len;
+
+ switch (hval) {
+ default:
+ hval += asso_values[(unsigned char)str[1] + 1];
+ /*FALLTHROUGH*/
+ case 1:
+ hval += asso_values[(unsigned char)str[0]];
+ break;
+ }
+ return hval;
+}
+
+#ifdef __GNUC__
+__inline
+#ifdef __GNUC_STDC_INLINE__
+ __attribute__((__gnu_inline__))
+#endif
+#endif
+ const char* hoedown_find_block_tag(register const char* str,
+ register unsigned int len)
+{
+ enum {
+ TOTAL_KEYWORDS = 24,
+ MIN_WORD_LENGTH = 1,
+ MAX_WORD_LENGTH = 10,
+ MIN_HASH_VALUE = 1,
+ MAX_HASH_VALUE = 24
+ };
+
+ if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) {
+ register int key = hash(str, len);
+
+ if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE) {
+ register const char* resword;
+
+ switch (key - 1) {
+ case 0:
+ resword = "p";
+ goto compare;
+ case 1:
+ resword = "h6";
+ goto compare;
+ case 2:
+ resword = "div";
+ goto compare;
+ case 3:
+ resword = "del";
+ goto compare;
+ case 4:
+ resword = "form";
+ goto compare;
+ case 5:
+ resword = "table";
+ goto compare;
+ case 6:
+ resword = "figure";
+ goto compare;
+ case 7:
+ resword = "pre";
+ goto compare;
+ case 8:
+ resword = "fieldset";
+ goto compare;
+ case 9:
+ resword = "noscript";
+ goto compare;
+ case 10:
+ resword = "script";
+ goto compare;
+ case 11:
+ resword = "style";
+ goto compare;
+ case 12:
+ resword = "dl";
+ goto compare;
+ case 13:
+ resword = "ol";
+ goto compare;
+ case 14:
+ resword = "ul";
+ goto compare;
+ case 15:
+ resword = "math";
+ goto compare;
+ case 16:
+ resword = "ins";
+ goto compare;
+ case 17:
+ resword = "h5";
+ goto compare;
+ case 18:
+ resword = "iframe";
+ goto compare;
+ case 19:
+ resword = "h4";
+ goto compare;
+ case 20:
+ resword = "h3";
+ goto compare;
+ case 21:
+ resword = "blockquote";
+ goto compare;
+ case 22:
+ resword = "h2";
+ goto compare;
+ case 23:
+ resword = "h1";
+ goto compare;
+ }
+ return 0;
+ compare:
+ if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 &&
+ !gperf_case_strncmp(str, resword, len) && resword[len] == '\0')
+ return resword;
+ }
+ }
+ return 0;
+}
diff --git a/meshmc/libraries/hoedown/src/html_smartypants.c b/meshmc/libraries/hoedown/src/html_smartypants.c
new file mode 100644
index 0000000000..518bb1e220
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/html_smartypants.c
@@ -0,0 +1,512 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/html.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+
+#ifdef _MSC_VER
+#define snprintf _snprintf
+#endif
+
+struct smartypants_data {
+ int in_squote;
+ int in_dquote;
+};
+
+static size_t smartypants_cb__ltag(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__dquote(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__amp(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__period(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__number(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__dash(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__parens(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__squote(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+static size_t smartypants_cb__backtick(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char,
+ const uint8_t* text, size_t size);
+static size_t smartypants_cb__escape(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size);
+
+static size_t (*smartypants_cb_ptrs[])(hoedown_buffer*,
+ struct smartypants_data*, uint8_t,
+ const uint8_t*, size_t) = {
+ NULL, /* 0 */
+ smartypants_cb__dash, /* 1 */
+ smartypants_cb__parens, /* 2 */
+ smartypants_cb__squote, /* 3 */
+ smartypants_cb__dquote, /* 4 */
+ smartypants_cb__amp, /* 5 */
+ smartypants_cb__period, /* 6 */
+ smartypants_cb__number, /* 7 */
+ smartypants_cb__ltag, /* 8 */
+ smartypants_cb__backtick, /* 9 */
+ smartypants_cb__escape, /* 10 */
+};
+
+static const uint8_t smartypants_cb_chars[UINT8_MAX + 1] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0,
+ 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0,
+ 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static int word_boundary(uint8_t c)
+{
+ return c == 0 || isspace(c) || ispunct(c);
+}
+
+/*
+ If 'text' begins with any kind of single quote (e.g. "'" or "&apos;" etc.),
+ returns the length of the sequence of characters that makes up the single-
+ quote. Otherwise, returns zero.
+*/
+static size_t squote_len(const uint8_t* text, size_t size)
+{
+ static char* single_quote_list[] = {"'", "&#39;", "&#x27;", "&apos;", NULL};
+ char** p;
+
+ for (p = single_quote_list; *p; ++p) {
+ size_t len = strlen(*p);
+ if (size >= len && memcmp(text, *p, len) == 0) {
+ return len;
+ }
+ }
+
+ return 0;
+}
+
+/* Converts " or ' at very beginning or end of a word to left or right quote */
+static int smartypants_quotes(hoedown_buffer* ob, uint8_t previous_char,
+ uint8_t next_char, uint8_t quote, int* is_open)
+{
+ char ent[8];
+
+ if (*is_open && !word_boundary(next_char))
+ return 0;
+
+ if (!(*is_open) && !word_boundary(previous_char))
+ return 0;
+
+ snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote);
+ *is_open = !(*is_open);
+ hoedown_buffer_puts(ob, ent);
+ return 1;
+}
+
+/*
+ Converts ' to left or right single quote; but the initial ' might be in
+ different forms, e.g. &apos; or &#39; or &#x27;.
+ 'squote_text' points to the original single quote, and 'squote_size' is its
+ length. 'text' points at the last character of the single-quote, e.g. ' or ;
+*/
+static size_t smartypants_squote(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size, const uint8_t* squote_text,
+ size_t squote_size)
+{
+ if (size >= 2) {
+ uint8_t t1 = tolower(text[1]);
+ size_t next_squote_len = squote_len(text + 1, size - 1);
+
+ /* convert '' to &ldquo; or &rdquo; */
+ if (next_squote_len > 0) {
+ uint8_t next_char =
+ (size > 1 + next_squote_len) ? text[1 + next_squote_len] : 0;
+ if (smartypants_quotes(ob, previous_char, next_char, 'd',
+ &smrt->in_dquote))
+ return next_squote_len;
+ }
+
+ /* Tom's, isn't, I'm, I'd */
+ if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') &&
+ (size == 3 || word_boundary(text[2]))) {
+ HOEDOWN_BUFPUTSL(ob, "&rsquo;");
+ return 0;
+ }
+
+ /* you're, you'll, you've */
+ if (size >= 3) {
+ uint8_t t2 = tolower(text[2]);
+
+ if (((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') ||
+ (t1 == 'v' && t2 == 'e')) &&
+ (size == 4 || word_boundary(text[3]))) {
+ HOEDOWN_BUFPUTSL(ob, "&rsquo;");
+ return 0;
+ }
+ }
+ }
+
+ if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's',
+ &smrt->in_squote))
+ return 0;
+
+ hoedown_buffer_put(ob, squote_text, squote_size);
+ return 0;
+}
+
+/* Converts ' to left or right single quote. */
+static size_t smartypants_cb__squote(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ return smartypants_squote(ob, smrt, previous_char, text, size, text, 1);
+}
+
+/* Converts (c), (r), (tm) */
+static size_t smartypants_cb__parens(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (size >= 3) {
+ uint8_t t1 = tolower(text[1]);
+ uint8_t t2 = tolower(text[2]);
+
+ if (t1 == 'c' && t2 == ')') {
+ HOEDOWN_BUFPUTSL(ob, "&copy;");
+ return 2;
+ }
+
+ if (t1 == 'r' && t2 == ')') {
+ HOEDOWN_BUFPUTSL(ob, "&reg;");
+ return 2;
+ }
+
+ if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') {
+ HOEDOWN_BUFPUTSL(ob, "&trade;");
+ return 3;
+ }
+ }
+
+ hoedown_buffer_putc(ob, text[0]);
+ return 0;
+}
+
+/* Converts "--" to em-dash, etc. */
+static size_t smartypants_cb__dash(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (size >= 3 && text[1] == '-' && text[2] == '-') {
+ HOEDOWN_BUFPUTSL(ob, "&mdash;");
+ return 2;
+ }
+
+ if (size >= 2 && text[1] == '-') {
+ HOEDOWN_BUFPUTSL(ob, "&ndash;");
+ return 1;
+ }
+
+ hoedown_buffer_putc(ob, text[0]);
+ return 0;
+}
+
+/* Converts &quot; etc. */
+static size_t smartypants_cb__amp(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ size_t len;
+ if (size >= 6 && memcmp(text, "&quot;", 6) == 0) {
+ if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd',
+ &smrt->in_dquote))
+ return 5;
+ }
+
+ len = squote_len(text, size);
+ if (len > 0) {
+ return (len - 1) + smartypants_squote(ob, smrt, previous_char,
+ text + (len - 1),
+ size - (len - 1), text, len);
+ }
+
+ if (size >= 4 && memcmp(text, "&#0;", 4) == 0)
+ return 3;
+
+ hoedown_buffer_putc(ob, '&');
+ return 0;
+}
+
+/* Converts "..." to ellipsis */
+static size_t smartypants_cb__period(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (size >= 3 && text[1] == '.' && text[2] == '.') {
+ HOEDOWN_BUFPUTSL(ob, "&hellip;");
+ return 2;
+ }
+
+ if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' &&
+ text[4] == '.') {
+ HOEDOWN_BUFPUTSL(ob, "&hellip;");
+ return 4;
+ }
+
+ hoedown_buffer_putc(ob, text[0]);
+ return 0;
+}
+
+/* Converts `` to opening double quote */
+static size_t smartypants_cb__backtick(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char,
+ const uint8_t* text, size_t size)
+{
+ if (size >= 2 && text[1] == '`') {
+ if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd',
+ &smrt->in_dquote))
+ return 1;
+ }
+
+ hoedown_buffer_putc(ob, text[0]);
+ return 0;
+}
+
+/* Converts 1/2, 1/4, 3/4 */
+static size_t smartypants_cb__number(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (word_boundary(previous_char) && size >= 3) {
+ if (text[0] == '1' && text[1] == '/' && text[2] == '2') {
+ if (size == 3 || word_boundary(text[3])) {
+ HOEDOWN_BUFPUTSL(ob, "&frac12;");
+ return 2;
+ }
+ }
+
+ if (text[0] == '1' && text[1] == '/' && text[2] == '4') {
+ if (size == 3 || word_boundary(text[3]) ||
+ (size >= 5 && tolower(text[3]) == 't' &&
+ tolower(text[4]) == 'h')) {
+ HOEDOWN_BUFPUTSL(ob, "&frac14;");
+ return 2;
+ }
+ }
+
+ if (text[0] == '3' && text[1] == '/' && text[2] == '4') {
+ if (size == 3 || word_boundary(text[3]) ||
+ (size >= 6 && tolower(text[3]) == 't' &&
+ tolower(text[4]) == 'h' && tolower(text[5]) == 's')) {
+ HOEDOWN_BUFPUTSL(ob, "&frac34;");
+ return 2;
+ }
+ }
+ }
+
+ hoedown_buffer_putc(ob, text[0]);
+ return 0;
+}
+
+/* Converts " to left or right double quote */
+static size_t smartypants_cb__dquote(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd',
+ &smrt->in_dquote))
+ HOEDOWN_BUFPUTSL(ob, "&quot;");
+
+ return 0;
+}
+
+static size_t smartypants_cb__ltag(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ static const char* skip_tags[] = {"pre", "code", "var", "samp",
+ "kbd", "math", "script", "style"};
+ static const size_t skip_tags_count = 8;
+
+ size_t tag, i = 0;
+
+ /* This is a comment. Copy everything verbatim until --> or EOF is seen. */
+ if (i + 4 < size && memcmp(text, "<!--", 4) == 0) {
+ i += 4;
+ while (i + 3 < size && memcmp(text + i, "-->", 3) != 0)
+ i++;
+ i += 3;
+ hoedown_buffer_put(ob, text, i + 1);
+ return i;
+ }
+
+ while (i < size && text[i] != '>')
+ i++;
+
+ for (tag = 0; tag < skip_tags_count; ++tag) {
+ if (hoedown_html_is_tag(text, size, skip_tags[tag]) ==
+ HOEDOWN_HTML_TAG_OPEN)
+ break;
+ }
+
+ if (tag < skip_tags_count) {
+ for (;;) {
+ while (i < size && text[i] != '<')
+ i++;
+
+ if (i == size)
+ break;
+
+ if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) ==
+ HOEDOWN_HTML_TAG_CLOSE)
+ break;
+
+ i++;
+ }
+
+ while (i < size && text[i] != '>')
+ i++;
+ }
+
+ hoedown_buffer_put(ob, text, i + 1);
+ return i;
+}
+
+static size_t smartypants_cb__escape(hoedown_buffer* ob,
+ struct smartypants_data* smrt,
+ uint8_t previous_char, const uint8_t* text,
+ size_t size)
+{
+ if (size < 2)
+ return 0;
+
+ switch (text[1]) {
+ case '\\':
+ case '"':
+ case '\'':
+ case '.':
+ case '-':
+ case '`':
+ hoedown_buffer_putc(ob, text[1]);
+ return 1;
+
+ default:
+ hoedown_buffer_putc(ob, '\\');
+ return 0;
+ }
+}
+
+#if 0
+static struct {
+ uint8_t c0;
+ const uint8_t *pattern;
+ const uint8_t *entity;
+ int skip;
+} smartypants_subs[] = {
+ { '\'', "'s>", "&rsquo;", 0 },
+ { '\'', "'t>", "&rsquo;", 0 },
+ { '\'', "'re>", "&rsquo;", 0 },
+ { '\'', "'ll>", "&rsquo;", 0 },
+ { '\'', "'ve>", "&rsquo;", 0 },
+ { '\'', "'m>", "&rsquo;", 0 },
+ { '\'', "'d>", "&rsquo;", 0 },
+ { '-', "--", "&mdash;", 1 },
+ { '-', "<->", "&ndash;", 0 },
+ { '.', "...", "&hellip;", 2 },
+ { '.', ". . .", "&hellip;", 4 },
+ { '(', "(c)", "&copy;", 2 },
+ { '(', "(r)", "&reg;", 2 },
+ { '(', "(tm)", "&trade;", 3 },
+ { '3', "<3/4>", "&frac34;", 2 },
+ { '3', "<3/4ths>", "&frac34;", 2 },
+ { '1', "<1/2>", "&frac12;", 2 },
+ { '1', "<1/4>", "&frac14;", 2 },
+ { '1', "<1/4th>", "&frac14;", 2 },
+ { '&', "&#0;", 0, 3 },
+};
+#endif
+
+void hoedown_html_smartypants(hoedown_buffer* ob, const uint8_t* text,
+ size_t size)
+{
+ size_t i;
+ struct smartypants_data smrt = {0, 0};
+
+ if (!text)
+ return;
+
+ hoedown_buffer_grow(ob, size);
+
+ for (i = 0; i < size; ++i) {
+ size_t org;
+ uint8_t action = 0;
+
+ org = i;
+ while (i < size && (action = smartypants_cb_chars[text[i]]) == 0)
+ i++;
+
+ if (i > org)
+ hoedown_buffer_put(ob, text + org, i - org);
+
+ if (i < size) {
+ i += smartypants_cb_ptrs[(int)action](
+ ob, &smrt, i ? text[i - 1] : 0, text + i, size - i);
+ }
+ }
+}
diff --git a/meshmc/libraries/hoedown/src/stack.c b/meshmc/libraries/hoedown/src/stack.c
new file mode 100644
index 0000000000..b3b72a19d4
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/stack.c
@@ -0,0 +1,94 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/stack.h"
+
+#include "hoedown/buffer.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+void hoedown_stack_init(hoedown_stack* st, size_t initial_size)
+{
+ assert(st);
+
+ st->item = NULL;
+ st->size = st->asize = 0;
+
+ if (!initial_size)
+ initial_size = 8;
+
+ hoedown_stack_grow(st, initial_size);
+}
+
+void hoedown_stack_uninit(hoedown_stack* st)
+{
+ assert(st);
+
+ free(st->item);
+}
+
+void hoedown_stack_grow(hoedown_stack* st, size_t neosz)
+{
+ assert(st);
+
+ if (st->asize >= neosz)
+ return;
+
+ st->item = hoedown_realloc(st->item, neosz * sizeof(void*));
+ memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void*));
+
+ st->asize = neosz;
+
+ if (st->size > neosz)
+ st->size = neosz;
+}
+
+void hoedown_stack_push(hoedown_stack* st, void* item)
+{
+ assert(st);
+
+ if (st->size >= st->asize)
+ hoedown_stack_grow(st, st->size * 2);
+
+ st->item[st->size++] = item;
+}
+
+void* hoedown_stack_pop(hoedown_stack* st)
+{
+ assert(st);
+
+ if (!st->size)
+ return NULL;
+
+ return st->item[--st->size];
+}
+
+void* hoedown_stack_top(const hoedown_stack* st)
+{
+ assert(st);
+
+ if (!st->size)
+ return NULL;
+
+ return st->item[st->size - 1];
+}
diff --git a/meshmc/libraries/hoedown/src/version.c b/meshmc/libraries/hoedown/src/version.c
new file mode 100644
index 0000000000..3386d8a519
--- /dev/null
+++ b/meshmc/libraries/hoedown/src/version.c
@@ -0,0 +1,29 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "hoedown/version.h"
+
+void hoedown_version(int* major, int* minor, int* revision)
+{
+ *major = HOEDOWN_VERSION_MAJOR;
+ *minor = HOEDOWN_VERSION_MINOR;
+ *revision = HOEDOWN_VERSION_REVISION;
+}
diff --git a/meshmc/libraries/iconfix/CMakeLists.txt b/meshmc/libraries/iconfix/CMakeLists.txt
new file mode 100644
index 0000000000..a6144101da
--- /dev/null
+++ b/meshmc/libraries/iconfix/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required(VERSION 3.25)
+project(iconfix)
+
+find_package(Qt6Core REQUIRED QUIET)
+find_package(Qt6Widgets REQUIRED QUIET)
+
+set(ICONFIX_SOURCES
+xdgicon.h
+xdgicon.cpp
+internal/qhexstring_p.h
+internal/qiconloader.cpp
+internal/qiconloader_p.h
+)
+
+add_library(MeshMC_iconfix SHARED ${ICONFIX_SOURCES})
+target_include_directories(MeshMC_iconfix PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}" )
+
+target_link_libraries(MeshMC_iconfix Qt6::Core Qt6::Widgets)
+
+set_target_properties(MeshMC_iconfix PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1)
+generate_export_header(MeshMC_iconfix)
+
+# Install it
+install(
+ TARGETS MeshMC_iconfix
+ RUNTIME DESTINATION ${LIBRARY_DEST_DIR}
+ LIBRARY DESTINATION ${LIBRARY_DEST_DIR}
+) \ No newline at end of file
diff --git a/meshmc/libraries/iconfix/internal/qhexstring_p.h b/meshmc/libraries/iconfix/internal/qhexstring_p.h
new file mode 100644
index 0000000000..3da8468ceb
--- /dev/null
+++ b/meshmc/libraries/iconfix/internal/qhexstring_p.h
@@ -0,0 +1,92 @@
+/****************************************************************************
+** SPDX-License-Identifier: LGPL-2.1-or-later
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtCore/qglobal.h>
+#include <QtCore/qpoint.h>
+#include <QtCore/qstring.h>
+#include <QtGui/qpolygon.h>
+#include <QtCore/qstringbuilder.h>
+
+#pragma once
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+// internal helper. Converts an integer value to an unique string token
+template <typename T> struct HexString {
+ inline HexString(const T t) : val(t) {}
+
+ inline void write(QChar*& dest) const
+ {
+ const ushort hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+ const char* c = reinterpret_cast<const char*>(&val);
+ for (uint i = 0; i < sizeof(T); ++i) {
+ *dest++ = hexChars[*c & 0xf];
+ *dest++ = hexChars[(*c & 0xf0) >> 4];
+ ++c;
+ }
+ }
+ const T val;
+};
+
+// specialization to enable fast concatenating of our string tokens to a string
+template <typename T> struct QConcatenable<HexString<T>> {
+ typedef HexString<T> type;
+ enum { ExactSize = true };
+ static int size(const HexString<T>&)
+ {
+ return sizeof(T) * 2;
+ }
+ static inline void appendTo(const HexString<T>& str, QChar*& out)
+ {
+ str.write(out);
+ }
+ typedef QString ConvertTo;
+};
diff --git a/meshmc/libraries/iconfix/internal/qiconloader.cpp b/meshmc/libraries/iconfix/internal/qiconloader.cpp
new file mode 100644
index 0000000000..38ca549ade
--- /dev/null
+++ b/meshmc/libraries/iconfix/internal/qiconloader.cpp
@@ -0,0 +1,646 @@
+/****************************************************************************
+** SPDX-License-Identifier: LGPL-2.1-or-later
+** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qiconloader_p.h"
+
+#include <QtGui/QIconEnginePlugin>
+#include <QtGui/QPixmapCache>
+#include <QtGui/QIconEngine>
+#include <QtGui/QPalette>
+#include <QtCore/QList>
+#include <QtCore/QHash>
+#include <QtCore/QDir>
+#include <QtCore/QSettings>
+#include <QtGui/QPainter>
+#include <QApplication>
+#include <QString>
+
+#include "qhexstring_p.h"
+
+namespace QtXdg
+{
+
+ Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance)
+
+ /* Theme to use in last resort, if the theme does not have the icon, neither
+ * the parents */
+
+ static QString fallbackTheme()
+ {
+ return QString("hicolor");
+ }
+
+ QIconLoader::QIconLoader()
+ : m_themeKey(1), m_supportsSvg(false), m_initialized(false)
+ {
+ }
+
+ // We lazily initialize the loader to make static icons
+ // work. Though we do not officially support this.
+
+ static inline QString systemThemeName()
+ {
+ return QIcon::themeName();
+ }
+
+ static inline QStringList systemIconSearchPaths()
+ {
+ auto paths = QIcon::themeSearchPaths();
+ paths.push_front(":/icons");
+ return paths;
+ }
+
+ void QIconLoader::ensureInitialized()
+ {
+ if (!m_initialized) {
+ m_initialized = true;
+
+ Q_ASSERT(qApp);
+
+ m_systemTheme = QIcon::themeName();
+
+ if (m_systemTheme.isEmpty())
+ m_systemTheme = fallbackTheme();
+ m_supportsSvg = true;
+ }
+ }
+
+ QIconLoader* QIconLoader::instance()
+ {
+ iconLoaderInstance()->ensureInitialized();
+ return iconLoaderInstance();
+ }
+
+ // Queries the system theme and invalidates existing
+ // icons if the theme has changed.
+ void QIconLoader::updateSystemTheme()
+ {
+ // Only change if this is not explicitly set by the user
+ if (m_userTheme.isEmpty()) {
+ QString theme = systemThemeName();
+ if (theme.isEmpty())
+ theme = fallbackTheme();
+ if (theme != m_systemTheme) {
+ m_systemTheme = theme;
+ invalidateKey();
+ }
+ }
+ }
+
+ void QIconLoader::setThemeName(const QString& themeName)
+ {
+ m_userTheme = themeName;
+ invalidateKey();
+ }
+
+ void QIconLoader::setThemeSearchPath(const QStringList& searchPaths)
+ {
+ m_iconDirs = searchPaths;
+ themeList.clear();
+ invalidateKey();
+ }
+
+ QStringList QIconLoader::themeSearchPaths() const
+ {
+ if (m_iconDirs.isEmpty()) {
+ m_iconDirs = systemIconSearchPaths();
+ }
+ return m_iconDirs;
+ }
+
+ QIconTheme::QIconTheme(const QString& themeName) : m_valid(false)
+ {
+ QFile themeIndex;
+
+ QStringList iconDirs = systemIconSearchPaths();
+ for (int i = 0; i < iconDirs.size(); ++i) {
+ QDir iconDir(iconDirs[i]);
+ QString themeDir = iconDir.path() + QLatin1Char('/') + themeName;
+ themeIndex.setFileName(themeDir + QLatin1String("/index.theme"));
+ if (themeIndex.exists()) {
+ m_contentDir = themeDir;
+ m_valid = true;
+
+ foreach (QString path, iconDirs) {
+ if (QFileInfo(path).isDir())
+ m_contentDirs.append(path + QLatin1Char('/') +
+ themeName);
+ }
+
+ break;
+ }
+ }
+
+ // if there is no index file, abscond.
+ if (!themeIndex.exists())
+ return;
+
+ // otherwise continue reading index file
+ const QSettings indexReader(themeIndex.fileName(),
+ QSettings::IniFormat);
+ QStringListIterator keyIterator(indexReader.allKeys());
+ while (keyIterator.hasNext()) {
+ const QString key = keyIterator.next();
+ if (!key.endsWith(QLatin1String("/Size")))
+ continue;
+
+ // Note the QSettings ini-format does not accept
+ // slashes in key names, hence we have to cheat
+ int size = indexReader.value(key).toInt();
+ if (!size)
+ continue;
+
+ QString directoryKey = key.left(key.size() - 5);
+ QIconDirInfo dirInfo(directoryKey);
+ dirInfo.size = size;
+ QString type =
+ indexReader.value(directoryKey + QLatin1String("/Type"))
+ .toString();
+
+ if (type == QLatin1String("Fixed"))
+ dirInfo.type = QIconDirInfo::Fixed;
+ else if (type == QLatin1String("Scalable"))
+ dirInfo.type = QIconDirInfo::Scalable;
+ else
+ dirInfo.type = QIconDirInfo::Threshold;
+
+ dirInfo.threshold =
+ indexReader.value(directoryKey + QLatin1String("/Threshold"), 2)
+ .toInt();
+
+ dirInfo.minSize =
+ indexReader
+ .value(directoryKey + QLatin1String("/MinSize"), size)
+ .toInt();
+
+ dirInfo.maxSize =
+ indexReader
+ .value(directoryKey + QLatin1String("/MaxSize"), size)
+ .toInt();
+ m_keyList.append(dirInfo);
+ }
+
+ // Parent themes provide fallbacks for missing icons
+ m_parents = indexReader.value(QLatin1String("Icon Theme/Inherits"))
+ .toStringList();
+ m_parents.removeAll(QString());
+
+ // Ensure a default platform fallback for all themes
+ if (m_parents.isEmpty()) {
+ const QString fallback = fallbackTheme();
+ if (!fallback.isEmpty())
+ m_parents.append(fallback);
+ }
+
+ // Ensure that all themes fall back to hicolor
+ if (!m_parents.contains(QLatin1String("hicolor")))
+ m_parents.append(QLatin1String("hicolor"));
+ }
+
+ QThemeIconEntries QIconLoader::findIconHelper(const QString& themeName,
+ const QString& iconName,
+ QStringList& visited) const
+ {
+ QThemeIconEntries entries;
+ Q_ASSERT(!themeName.isEmpty());
+
+ QPixmap pixmap;
+
+ // Used to protect against potential recursions
+ visited << themeName;
+
+ QIconTheme theme = themeList.value(themeName);
+ if (!theme.isValid()) {
+ theme = QIconTheme(themeName);
+ if (!theme.isValid())
+ theme = QIconTheme(fallbackTheme());
+
+ themeList.insert(themeName, theme);
+ }
+
+ QStringList contentDirs = theme.contentDirs();
+ const QVector<QIconDirInfo> subDirs = theme.keyList();
+
+ const QString svgext(QLatin1String(".svg"));
+ const QString pngext(QLatin1String(".png"));
+ const QString xpmext(QLatin1String(".xpm"));
+
+ // Add all relevant files
+ for (int i = 0; i < subDirs.size(); ++i) {
+ const QIconDirInfo& dirInfo = subDirs.at(i);
+ QString subdir = dirInfo.path;
+
+ foreach (QString contentDir, contentDirs) {
+ QDir currentDir(contentDir + '/' + subdir);
+
+ if (currentDir.exists(iconName + pngext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename =
+ currentDir.filePath(iconName + pngext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.prepend(iconEntry);
+ } else if (m_supportsSvg &&
+ currentDir.exists(iconName + svgext)) {
+ ScalableEntry* iconEntry = new ScalableEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename =
+ currentDir.filePath(iconName + svgext);
+ entries.append(iconEntry);
+ break;
+ } else if (currentDir.exists(iconName + xpmext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename =
+ currentDir.filePath(iconName + xpmext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.append(iconEntry);
+ break;
+ }
+ }
+ }
+
+ if (entries.isEmpty()) {
+ const QStringList parents = theme.parents();
+ // Search recursively through inherited themes
+ for (int i = 0; i < parents.size(); ++i) {
+
+ const QString parentTheme = parents.at(i).trimmed();
+
+ if (!visited.contains(parentTheme)) // guard against recursion
+ entries = findIconHelper(parentTheme, iconName, visited);
+
+ if (!entries.isEmpty()) // success
+ break;
+ }
+ }
+
+/*********************************************************************
+Author: Kaitlin Rupert <kaitlin.rupert@intel.com>
+Date: Aug 12, 2010
+Description: Make it so that the QIcon loader honors /usr/share/pixmaps
+ directory. This is a valid directory per the Freedesktop.org
+ icon theme specification.
+Bug: https://bugreports.qt.nokia.com/browse/QTBUG-12874
+ *********************************************************************/
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+ /* Freedesktop standard says to look in /usr/share/pixmaps last */
+ if (entries.isEmpty()) {
+ const QString pixmaps(QLatin1String("/usr/share/pixmaps"));
+
+ QDir currentDir(pixmaps);
+ QIconDirInfo dirInfo(pixmaps);
+ if (currentDir.exists(iconName + pngext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename = currentDir.filePath(iconName + pngext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.prepend(iconEntry);
+ } else if (m_supportsSvg && currentDir.exists(iconName + svgext)) {
+ ScalableEntry* iconEntry = new ScalableEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename = currentDir.filePath(iconName + svgext);
+ entries.append(iconEntry);
+ } else if (currentDir.exists(iconName + xpmext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->dir = dirInfo;
+ iconEntry->filename = currentDir.filePath(iconName + xpmext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.append(iconEntry);
+ }
+ }
+#endif
+
+ if (entries.isEmpty()) {
+ // Search for unthemed icons in main dir of search paths
+ QStringList themeSearchPaths = QIcon::themeSearchPaths();
+ foreach (QString contentDir, themeSearchPaths) {
+ QDir currentDir(contentDir);
+
+ if (currentDir.exists(iconName + pngext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->filename =
+ currentDir.filePath(iconName + pngext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.prepend(iconEntry);
+ } else if (m_supportsSvg &&
+ currentDir.exists(iconName + svgext)) {
+ ScalableEntry* iconEntry = new ScalableEntry;
+ iconEntry->filename =
+ currentDir.filePath(iconName + svgext);
+ entries.append(iconEntry);
+ break;
+ } else if (currentDir.exists(iconName + xpmext)) {
+ PixmapEntry* iconEntry = new PixmapEntry;
+ iconEntry->filename =
+ currentDir.filePath(iconName + xpmext);
+ // Notice we ensure that pixmap entries always come before
+ // scalable to preserve search order afterwards
+ entries.append(iconEntry);
+ break;
+ }
+ }
+ }
+ return entries;
+ }
+
+ QThemeIconEntries QIconLoader::loadIcon(const QString& name) const
+ {
+ if (!themeName().isEmpty()) {
+ QStringList visited;
+ return findIconHelper(themeName(), name, visited);
+ }
+
+ return QThemeIconEntries();
+ }
+
+ // -------- Icon Loader Engine -------- //
+
+ QIconLoaderEngineFixed::QIconLoaderEngineFixed(const QString& iconName)
+ : m_iconName(iconName), m_key(0)
+ {
+ }
+
+ QIconLoaderEngineFixed::~QIconLoaderEngineFixed()
+ {
+ qDeleteAll(m_entries);
+ }
+
+ QIconLoaderEngineFixed::QIconLoaderEngineFixed(
+ const QIconLoaderEngineFixed& other)
+ : QIconEngine(other), m_iconName(other.m_iconName), m_key(0)
+ {
+ }
+
+ QIconEngine* QIconLoaderEngineFixed::clone() const
+ {
+ return new QIconLoaderEngineFixed(*this);
+ }
+
+ bool QIconLoaderEngineFixed::read(QDataStream& in)
+ {
+ in >> m_iconName;
+ return true;
+ }
+
+ bool QIconLoaderEngineFixed::write(QDataStream& out) const
+ {
+ out << m_iconName;
+ return true;
+ }
+
+ bool QIconLoaderEngineFixed::hasIcon() const
+ {
+ return !(m_entries.isEmpty());
+ }
+
+ // Lazily load the icon
+ void QIconLoaderEngineFixed::ensureLoaded()
+ {
+ if (!(QIconLoader::instance()->themeKey() == m_key)) {
+
+ qDeleteAll(m_entries);
+
+ m_entries = QIconLoader::instance()->loadIcon(m_iconName);
+ m_key = QIconLoader::instance()->themeKey();
+ }
+ }
+
+ void QIconLoaderEngineFixed::paint(QPainter* painter, const QRect& rect,
+ QIcon::Mode mode, QIcon::State state)
+ {
+ QSize pixmapSize = rect.size();
+#if defined(Q_WS_MAC)
+ pixmapSize *= qt_mac_get_scalefactor();
+#endif
+ painter->drawPixmap(rect, pixmap(pixmapSize, mode, state));
+ }
+
+ /*
+ * This algorithm is defined by the freedesktop spec:
+ * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+ */
+ static bool directoryMatchesSize(const QIconDirInfo& dir, int iconsize)
+ {
+ if (dir.type == QIconDirInfo::Fixed) {
+ return dir.size == iconsize;
+ } else if (dir.type == QIconDirInfo::Scalable) {
+ return dir.size <= dir.maxSize && iconsize >= dir.minSize;
+ } else if (dir.type == QIconDirInfo::Threshold) {
+ return iconsize >= dir.size - dir.threshold &&
+ iconsize <= dir.size + dir.threshold;
+ }
+
+ Q_ASSERT(1); // Not a valid value
+ return false;
+ }
+
+ /*
+ * This algorithm is defined by the freedesktop spec:
+ * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+ */
+ static int directorySizeDistance(const QIconDirInfo& dir, int iconsize)
+ {
+ if (dir.type == QIconDirInfo::Fixed) {
+ return qAbs(dir.size - iconsize);
+ } else if (dir.type == QIconDirInfo::Scalable) {
+ if (iconsize < dir.minSize)
+ return dir.minSize - iconsize;
+ else if (iconsize > dir.maxSize)
+ return iconsize - dir.maxSize;
+ else
+ return 0;
+ } else if (dir.type == QIconDirInfo::Threshold) {
+ if (iconsize < dir.size - dir.threshold)
+ return dir.minSize - iconsize;
+ else if (iconsize > dir.size + dir.threshold)
+ return iconsize - dir.maxSize;
+ else
+ return 0;
+ }
+
+ Q_ASSERT(1); // Not a valid value
+ return INT_MAX;
+ }
+
+ QIconLoaderEngineEntry*
+ QIconLoaderEngineFixed::entryForSize(const QSize& size)
+ {
+ int iconsize = qMin(size.width(), size.height());
+
+ // Note that m_entries are sorted so that png-files
+ // come first
+
+ const int numEntries = m_entries.size();
+
+ // Search for exact matches first
+ for (int i = 0; i < numEntries; ++i) {
+ QIconLoaderEngineEntry* entry = m_entries.at(i);
+ if (directoryMatchesSize(entry->dir, iconsize)) {
+ return entry;
+ }
+ }
+
+ // Find the minimum distance icon
+ int minimalSize = INT_MAX;
+ QIconLoaderEngineEntry* closestMatch = 0;
+ for (int i = 0; i < numEntries; ++i) {
+ QIconLoaderEngineEntry* entry = m_entries.at(i);
+ int distance = directorySizeDistance(entry->dir, iconsize);
+ if (distance < minimalSize) {
+ minimalSize = distance;
+ closestMatch = entry;
+ }
+ }
+ return closestMatch;
+ }
+
+ /*
+ * Returns the actual icon size. For scalable svg's this is equivalent
+ * to the requested size. Otherwise the closest match is returned but
+ * we can never return a bigger size than the requested size.
+ *
+ */
+ QSize QIconLoaderEngineFixed::actualSize(const QSize& size,
+ QIcon::Mode mode,
+ QIcon::State state)
+ {
+ ensureLoaded();
+
+ QIconLoaderEngineEntry* entry = entryForSize(size);
+ if (entry) {
+ const QIconDirInfo& dir = entry->dir;
+ if (dir.type == QIconDirInfo::Scalable)
+ return size;
+ else {
+ int result =
+ qMin<int>(dir.size, qMin(size.width(), size.height()));
+ return QSize(result, result);
+ }
+ }
+ return QIconEngine::actualSize(size, mode, state);
+ }
+
+ QPixmap PixmapEntry::pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state)
+ {
+ Q_UNUSED(state);
+
+ // Ensure that basePixmap is lazily initialized before generating the
+ // key, otherwise the cache key is not unique
+ if (basePixmap.isNull())
+ basePixmap.load(filename);
+
+ QSize actualSize = basePixmap.size();
+ if (!actualSize.isNull() && (actualSize.width() > size.width() ||
+ actualSize.height() > size.height()))
+ actualSize.scale(size, Qt::KeepAspectRatio);
+
+ QString key = QLatin1String("$qt_theme_") %
+ HexString<qint64>(basePixmap.cacheKey()) %
+ HexString<int>(mode) %
+ HexString<qint64>(QGuiApplication::palette().cacheKey()) %
+ HexString<int>(actualSize.width()) %
+ HexString<int>(actualSize.height());
+
+ QPixmap cachedPixmap;
+ if (QPixmapCache::find(key, &cachedPixmap)) {
+ return cachedPixmap;
+ } else {
+ if (basePixmap.size() != actualSize) {
+ cachedPixmap =
+ basePixmap.scaled(actualSize, Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
+ } else {
+ cachedPixmap = basePixmap;
+ }
+ QPixmapCache::insert(key, cachedPixmap);
+ }
+ return cachedPixmap;
+ }
+
+ QPixmap ScalableEntry::pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state)
+ {
+ if (svgIcon.isNull()) {
+ svgIcon = QIcon(filename);
+ }
+
+ // Simply reuse svg icon engine
+ return svgIcon.pixmap(size, mode, state);
+ }
+
+ QPixmap QIconLoaderEngineFixed::pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state)
+ {
+ ensureLoaded();
+
+ QIconLoaderEngineEntry* entry = entryForSize(size);
+ if (entry) {
+ return entry->pixmap(size, mode, state);
+ }
+
+ return QPixmap();
+ }
+
+ QString QIconLoaderEngineFixed::key() const
+ {
+ return QLatin1String("QIconLoaderEngineFixed");
+ }
+
+ QList<QSize> QIconLoaderEngineFixed::availableSizes(QIcon::Mode mode,
+ QIcon::State state)
+ {
+ ensureLoaded();
+
+ const int N = m_entries.size();
+ QList<QSize> sizes;
+ sizes.reserve(N);
+
+ for (int i = 0; i < N; ++i) {
+ int size = m_entries.at(i)->dir.size;
+ sizes.append(QSize(size, size));
+ }
+ return sizes;
+ }
+
+ QString QIconLoaderEngineFixed::iconName()
+ {
+ return m_iconName;
+ }
+
+} // namespace QtXdg
diff --git a/meshmc/libraries/iconfix/internal/qiconloader_p.h b/meshmc/libraries/iconfix/internal/qiconloader_p.h
new file mode 100644
index 0000000000..89f2c1656b
--- /dev/null
+++ b/meshmc/libraries/iconfix/internal/qiconloader_p.h
@@ -0,0 +1,216 @@
+/****************************************************************************
+** SPDX-License-Identifier: LGPL-2.1-or-later
+** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the QtGui module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QtCore/qglobal.h>
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtGui/QIcon>
+#include <QtGui/QIconEngine>
+#include <QtGui/QPixmapCache>
+#include <QtCore/QHash>
+#include <QtCore/QVector>
+#include <QtCore/QTypeInfo>
+
+namespace QtXdg
+{
+
+ class QIconLoader;
+
+ struct QIconDirInfo {
+ enum Type { Fixed, Scalable, Threshold };
+ QIconDirInfo(const QString& _path = QString())
+ : path(_path), size(0), maxSize(0), minSize(0), threshold(0),
+ type(Threshold)
+ {
+ }
+ QString path;
+ short size;
+ short maxSize;
+ short minSize;
+ short threshold;
+ Type type : 4;
+ };
+
+ class QIconLoaderEngineEntry
+ {
+ public:
+ virtual ~QIconLoaderEngineEntry() {}
+ virtual QPixmap pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state) = 0;
+ QString filename;
+ QIconDirInfo dir;
+ static int count;
+ };
+
+ struct ScalableEntry : public QIconLoaderEngineEntry {
+ QPixmap pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state) Q_DECL_OVERRIDE;
+ QIcon svgIcon;
+ };
+
+ struct PixmapEntry : public QIconLoaderEngineEntry {
+ QPixmap pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state) Q_DECL_OVERRIDE;
+ QPixmap basePixmap;
+ };
+
+ typedef QList<QIconLoaderEngineEntry*> QThemeIconEntries;
+
+ // class QIconLoaderEngine : public QIconEngine
+ class QIconLoaderEngineFixed : public QIconEngine
+ {
+ public:
+ QIconLoaderEngineFixed(const QString& iconName = QString());
+ ~QIconLoaderEngineFixed();
+
+ void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode,
+ QIcon::State state) override;
+ QPixmap pixmap(const QSize& size, QIcon::Mode mode,
+ QIcon::State state) override;
+ QSize actualSize(const QSize& size, QIcon::Mode mode,
+ QIcon::State state) override;
+ QIconEngine* clone() const override;
+ bool read(QDataStream& in) override;
+ bool write(QDataStream& out) const override;
+
+ private:
+ QString key() const override;
+ bool hasIcon() const;
+ void ensureLoaded();
+ QList<QSize> availableSizes(QIcon::Mode mode = QIcon::Normal,
+ QIcon::State state = QIcon::Off) override;
+ QString iconName() override;
+ QIconLoaderEngineEntry* entryForSize(const QSize& size);
+ QIconLoaderEngineFixed(const QIconLoaderEngineFixed& other);
+ QThemeIconEntries m_entries;
+ QString m_iconName;
+ uint m_key;
+
+ friend class QIconLoader;
+ };
+
+ class QIconTheme
+ {
+ public:
+ QIconTheme(const QString& name);
+ QIconTheme() : m_valid(false) {}
+ QStringList parents()
+ {
+ return m_parents;
+ }
+ QVector<QIconDirInfo> keyList()
+ {
+ return m_keyList;
+ }
+ QString contentDir()
+ {
+ return m_contentDir;
+ }
+ QStringList contentDirs()
+ {
+ return m_contentDirs;
+ }
+ bool isValid()
+ {
+ return m_valid;
+ }
+
+ private:
+ QString m_contentDir;
+ QStringList m_contentDirs;
+ QVector<QIconDirInfo> m_keyList;
+ QStringList m_parents;
+ bool m_valid;
+ };
+
+ class QIconLoader
+ {
+ public:
+ QIconLoader();
+ QThemeIconEntries loadIcon(const QString& iconName) const;
+ uint themeKey() const
+ {
+ return m_themeKey;
+ }
+
+ QString themeName() const
+ {
+ return m_userTheme.isEmpty() ? m_systemTheme : m_userTheme;
+ }
+ void setThemeName(const QString& themeName);
+ QIconTheme theme()
+ {
+ return themeList.value(themeName());
+ }
+ void setThemeSearchPath(const QStringList& searchPaths);
+ QStringList themeSearchPaths() const;
+ QIconDirInfo dirInfo(int dirindex);
+ static QIconLoader* instance();
+ void updateSystemTheme();
+ void invalidateKey()
+ {
+ m_themeKey++;
+ }
+ void ensureInitialized();
+
+ private:
+ QThemeIconEntries findIconHelper(const QString& themeName,
+ const QString& iconName,
+ QStringList& visited) const;
+ uint m_themeKey;
+ bool m_supportsSvg;
+ bool m_initialized;
+
+ mutable QString m_userTheme;
+ mutable QString m_systemTheme;
+ mutable QStringList m_iconDirs;
+ mutable QHash<QString, QIconTheme> themeList;
+ };
+
+} // namespace QtXdg
+
+// Note: class template specialization of 'QTypeInfo' must occur at
+// global scope
+Q_DECLARE_TYPEINFO(QtXdg::QIconDirInfo, Q_MOVABLE_TYPE);
diff --git a/meshmc/libraries/iconfix/xdgicon.cpp b/meshmc/libraries/iconfix/xdgicon.cpp
new file mode 100644
index 0000000000..119495d369
--- /dev/null
+++ b/meshmc/libraries/iconfix/xdgicon.cpp
@@ -0,0 +1,142 @@
+/* BEGIN_COMMON_COPYRIGHT_HEADER
+ * (c)LGPL2+
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: N/A
+ * Razor - a lightweight, Qt based, desktop toolset
+ * http://razor-qt.org
+ *
+ * Copyright: 2010-2011 Razor team
+ * Authors:
+ * Alexander Sokoloff <sokoloff.a@gmail.com>
+ *
+ * This program or library is free software; you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ *
+ * END_COMMON_COPYRIGHT_HEADER */
+
+#include "xdgicon.h"
+
+#include <QString>
+#include <QDebug>
+#include <QDir>
+#include <QStringList>
+#include <QFileInfo>
+#include <QCache>
+#include "internal/qiconloader_p.h"
+#include <QCoreApplication>
+
+/************************************************
+
+ ************************************************/
+static void qt_cleanup_icon_cache();
+typedef QCache<QString, QIcon> IconCache;
+
+namespace
+{
+ struct QtIconCache : public IconCache {
+ QtIconCache()
+ {
+ qAddPostRoutine(qt_cleanup_icon_cache);
+ }
+ };
+} // namespace
+Q_GLOBAL_STATIC(IconCache, qtIconCache)
+
+static void qt_cleanup_icon_cache()
+{
+ qtIconCache()->clear();
+}
+
+/************************************************
+
+ ************************************************/
+XdgIcon::XdgIcon() {}
+
+/************************************************
+
+ ************************************************/
+XdgIcon::~XdgIcon() {}
+
+/************************************************
+ Returns the name of the current icon theme.
+ ************************************************/
+QString XdgIcon::themeName()
+{
+ return QIcon::themeName();
+}
+
+/************************************************
+ Sets the current icon theme to name.
+ ************************************************/
+void XdgIcon::setThemeName(const QString& themeName)
+{
+ QIcon::setThemeName(themeName);
+ QtXdg::QIconLoader::instance()->updateSystemTheme();
+}
+
+/************************************************
+ Returns the QIcon corresponding to name in the current icon theme. If no such
+ icon is found in the current theme fallback is return instead.
+ ************************************************/
+QIcon XdgIcon::fromTheme(const QString& iconName, const QIcon& fallback)
+{
+ if (iconName.isEmpty())
+ return fallback;
+
+ bool isAbsolute = (iconName[0] == '/');
+
+ QString name = QFileInfo(iconName).fileName();
+ if (name.endsWith(".png", Qt::CaseInsensitive) ||
+ name.endsWith(".svg", Qt::CaseInsensitive) ||
+ name.endsWith(".xpm", Qt::CaseInsensitive)) {
+ name.truncate(name.length() - 4);
+ }
+
+ QIcon icon;
+
+ if (qtIconCache()->contains(name)) {
+ icon = *qtIconCache()->object(name);
+ } else {
+ QIcon* cachedIcon;
+ if (!isAbsolute)
+ cachedIcon = new QIcon(new QtXdg::QIconLoaderEngineFixed(name));
+ else
+ cachedIcon = new QIcon(iconName);
+ qtIconCache()->insert(name, cachedIcon);
+ icon = *cachedIcon;
+ }
+
+ // Note the qapp check is to allow lazy loading of static icons
+ // Supporting fallbacks will not work for this case.
+ if (qApp && !isAbsolute && icon.availableSizes().isEmpty()) {
+ return fallback;
+ }
+ return icon;
+}
+
+/************************************************
+ Returns the QIcon corresponding to names in the current icon theme. If no such
+ icon is found in the current theme fallback is return instead.
+ ************************************************/
+QIcon XdgIcon::fromTheme(const QStringList& iconNames, const QIcon& fallback)
+{
+ foreach (QString iconName, iconNames) {
+ QIcon icon = fromTheme(iconName);
+ if (!icon.isNull())
+ return icon;
+ }
+
+ return fallback;
+}
diff --git a/meshmc/libraries/iconfix/xdgicon.h b/meshmc/libraries/iconfix/xdgicon.h
new file mode 100644
index 0000000000..799989fe1d
--- /dev/null
+++ b/meshmc/libraries/iconfix/xdgicon.h
@@ -0,0 +1,51 @@
+/* BEGIN_COMMON_COPYRIGHT_HEADER
+ * (c)LGPL2+
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * SPDX-FileCopyrightText: N/A
+ * Razor - a lightweight, Qt based, desktop toolset
+ * http://razor-qt.org
+ *
+ * Copyright: 2010-2011 Razor team
+ * Authors:
+ * Alexander Sokoloff <sokoloff.a@gmail.com>
+ *
+ * This program or library is free software; you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ *
+ * END_COMMON_COPYRIGHT_HEADER */
+
+#pragma once
+
+#include <QtGui/QIcon>
+#include <QString>
+#include <QStringList>
+
+#include "meshmc_iconfix_export.h"
+
+class MESHMC_ICONFIX_EXPORT XdgIcon
+{
+ public:
+ static QIcon fromTheme(const QString& iconName,
+ const QIcon& fallback = QIcon());
+ static QIcon fromTheme(const QStringList& iconNames,
+ const QIcon& fallback = QIcon());
+
+ static QString themeName();
+ static void setThemeName(const QString& themeName);
+
+ protected:
+ explicit XdgIcon();
+ virtual ~XdgIcon();
+};
diff --git a/meshmc/libraries/javacheck/.gitignore b/meshmc/libraries/javacheck/.gitignore
new file mode 100644
index 0000000000..cc1c52bf4d
--- /dev/null
+++ b/meshmc/libraries/javacheck/.gitignore
@@ -0,0 +1,6 @@
+.idea
+*.iml
+out
+.classpath
+.idea
+.project
diff --git a/meshmc/libraries/javacheck/CMakeLists.txt b/meshmc/libraries/javacheck/CMakeLists.txt
new file mode 100644
index 0000000000..3ddb0f0efd
--- /dev/null
+++ b/meshmc/libraries/javacheck/CMakeLists.txt
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 3.25)
+project(launcher Java)
+find_package(Java 1.7 REQUIRED COMPONENTS Development)
+
+include(UseJava)
+set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck)
+set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked)
+
+set(SRC
+ JavaCheck.java
+)
+
+add_jar(JavaCheck ${SRC})
+install_jar(JavaCheck "${JARS_DEST_DIR}")
diff --git a/meshmc/libraries/javacheck/JavaCheck.java b/meshmc/libraries/javacheck/JavaCheck.java
new file mode 100644
index 0000000000..611b95d695
--- /dev/null
+++ b/meshmc/libraries/javacheck/JavaCheck.java
@@ -0,0 +1,45 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import java.lang.Integer;
+
+public class JavaCheck
+{
+ private static final String[] keys = {"os.arch", "java.version", "java.vendor"};
+ public static void main (String [] args)
+ {
+ int ret = 0;
+ for(String key : keys)
+ {
+ String property = System.getProperty(key);
+ if(property != null)
+ {
+ System.out.println(key + "=" + property);
+ }
+ else
+ {
+ ret = 1;
+ }
+ }
+
+ System.exit(ret);
+ }
+}
diff --git a/meshmc/libraries/katabasis/.gitignore b/meshmc/libraries/katabasis/.gitignore
new file mode 100644
index 0000000000..35e189c5ef
--- /dev/null
+++ b/meshmc/libraries/katabasis/.gitignore
@@ -0,0 +1,2 @@
+build/
+*.kdev4
diff --git a/meshmc/libraries/katabasis/CMakeLists.txt b/meshmc/libraries/katabasis/CMakeLists.txt
new file mode 100644
index 0000000000..a42fb3057d
--- /dev/null
+++ b/meshmc/libraries/katabasis/CMakeLists.txt
@@ -0,0 +1,53 @@
+cmake_minimum_required(VERSION 3.25)
+
+string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
+if(IS_IN_SOURCE_BUILD)
+ message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.")
+endif()
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR
+ CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*"
+ )
+ message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.")
+ endif()
+endif()
+
+project(Katabasis)
+enable_testing()
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+set(CMAKE_CXX_STANDARD_REQUIRED true)
+set(CMAKE_C_STANDARD_REQUIRED true)
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_C_STANDARD 11)
+
+find_package(Qt6 COMPONENTS Core Network REQUIRED)
+
+set( katabasis_PRIVATE
+ src/DeviceFlow.cpp
+ src/JsonResponse.cpp
+ src/JsonResponse.h
+ src/PollServer.cpp
+ src/Reply.cpp
+)
+
+set( katabasis_PUBLIC
+ include/katabasis/DeviceFlow.h
+ include/katabasis/Globals.h
+ include/katabasis/PollServer.h
+ include/katabasis/Reply.h
+ include/katabasis/RequestParameter.h
+)
+
+add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
+target_link_libraries(Katabasis Qt6::Core Qt6::Network)
+
+# needed for statically linked Katabasis in shared libs on x86_64
+set_target_properties(Katabasis
+ PROPERTIES POSITION_INDEPENDENT_CODE TRUE
+)
+
+target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis)
diff --git a/meshmc/libraries/katabasis/LICENSE b/meshmc/libraries/katabasis/LICENSE
new file mode 100644
index 0000000000..9ac8d42fb0
--- /dev/null
+++ b/meshmc/libraries/katabasis/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2012, Akos Polster
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/meshmc/libraries/katabasis/README.md b/meshmc/libraries/katabasis/README.md
new file mode 100644
index 0000000000..646a128670
--- /dev/null
+++ b/meshmc/libraries/katabasis/README.md
@@ -0,0 +1,36 @@
+# Katabasis - MS-flavoerd OAuth for Qt, derived from the O2 library
+
+This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful.
+
+It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored.
+
+[You can find the original library's git repository here.](https://github.com/pipacs/o2)
+
+Notes to contributors:
+
+ * Please follow the coding style of the existing source, where reasonable
+ * Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
+ * If you are interested in working on this, come to the MeshMC Discord server and talk first
+
+## Installation
+
+Clone the Github repository, integrate the it into your CMake build system.
+
+The library is static only, dynamic linking and system-wide installation are out of scope and undesirable.
+
+## Usage
+
+At this stage, don't, unless you want to help with the library itself.
+
+This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features:
+
+* Multiple accounts
+* Multi-stage authentication/authorization schemes
+* Tighter control over token chains and their storage
+* Talking to complex APIs and individually authorized microservices
+* Token lifetime management, 'offline mode' and resilience in face of network failures
+* Token and claims/entitlements validation
+* Caching of some API results
+* XBox magic
+* Mojang magic
+* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available)
diff --git a/meshmc/libraries/katabasis/acknowledgements.md b/meshmc/libraries/katabasis/acknowledgements.md
new file mode 100644
index 0000000000..c1c8a3d49e
--- /dev/null
+++ b/meshmc/libraries/katabasis/acknowledgements.md
@@ -0,0 +1,110 @@
+# O2 library by Akos Polster and contributors
+
+[The origin of this fork.](https://github.com/pipacs/o2)
+
+> Copyright (c) 2012, Akos Polster
+> All rights reserved.
+>
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions are met:
+>
+> * Redistributions of source code must retain the above copyright notice, this
+> list of conditions and the following disclaimer.
+>
+> * Redistributions in binary form must reproduce the above copyright notice,
+> this list of conditions and the following disclaimer in the documentation
+> and/or other materials provided with the distribution.
+>
+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# SimpleCrypt by Andre Somers
+
+Cryptographic methods for Qt.
+
+> Copyright (c) 2011, Andre Somers
+> All rights reserved.
+>
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions are met:
+>
+> * Redistributions of source code must retain the above copyright
+> notice, this list of conditions and the following disclaimer.
+> * Redistributions in binary form must reproduce the above copyright
+> notice, this list of conditions and the following disclaimer in the
+> documentation and/or other materials provided with the distribution.
+> * Neither the name of the Rathenau Instituut, Andre Somers nor the
+> names of its contributors may be used to endorse or promote products
+> derived from this software without specific prior written permission.
+>
+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY
+> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
+
+Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
+
+> "Hi Akos,
+>
+> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file).
+>
+> Regards,
+> -mandeep"
+
+# Sergey Gavrushkin <https://github.com/ncux>
+
+FreshBooks specialization
+
+# Theofilos Intzoglou <https://github.com/parapente>
+
+Hubic specialization
+
+# Dimitar
+
+SurveyMonkey specialization
+
+# David Brooks <https://github.com/dbrnz>
+
+CMake related fixes and improvements.
+
+# Lukas Vogel <https://github.com/lukedirtwalker>
+
+Spotify support
+
+# Alan Garny <https://github.com/agarny>
+
+Windows DLL build support
+
+# MartinMikita <https://github.com/MartinMikita>
+
+Bug fixes
+
+# Larry Shaffer <https://github.com/dakcarto>
+
+Versioning, shared lib, install target and header support
+
+# Gilmanov Ildar <https://github.com/gilmanov-ildar>
+
+Bug fixes, support for ```qml``` module
+
+# Fabian Vogt <https://github.com/Vogtinator>
+
+Bug fixes, support for building without Qt keywords enabled
+
diff --git a/meshmc/libraries/katabasis/include/katabasis/Bits.h b/meshmc/libraries/katabasis/include/katabasis/Bits.h
new file mode 100644
index 0000000000..bd0aed8508
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/Bits.h
@@ -0,0 +1,57 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QDateTime>
+#include <QMap>
+#include <QVariantMap>
+
+namespace Katabasis
+{
+ enum class Activity {
+ Idle,
+ LoggingIn,
+ LoggingOut,
+ Refreshing,
+ FailedSoft, //!< soft failure. this generally means the user auth
+ //!< details haven't been invalidated
+ FailedHard, //!< hard failure. auth is invalid
+ FailedGone, //!< hard failure. auth is invalid, and the account no
+ //!< longer exists
+ Succeeded
+ };
+
+ enum class Validity { None, Assumed, Certain };
+
+ struct Token {
+ QDateTime issueInstant;
+ QDateTime notAfter;
+ QString token;
+ QString refresh_token;
+ QVariantMap extra;
+
+ Validity validity = Validity::None;
+ bool persistent = true;
+ };
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h b/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h
new file mode 100644
index 0000000000..37f39cc658
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/DeviceFlow.h
@@ -0,0 +1,178 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QPair>
+
+#include "Reply.h"
+#include "RequestParameter.h"
+#include "Bits.h"
+
+namespace Katabasis
+{
+
+ class ReplyServer;
+ class PollServer;
+
+ /// Simple OAuth2 Device Flow authenticator.
+ class DeviceFlow : public QObject
+ {
+ Q_OBJECT
+ public:
+ Q_ENUMS(GrantFlow)
+
+ public:
+ struct Options {
+ QString userAgent = QStringLiteral("Katabasis/1.0");
+ QString responseType = QStringLiteral("code");
+ QString scope;
+ QString clientIdentifier;
+ QString clientSecret;
+ QUrl authorizationUrl;
+ QUrl accessTokenUrl;
+ };
+
+ public:
+ /// Are we authenticated?
+ bool linked();
+
+ /// Authentication token.
+ QString token();
+
+ /// Provider-specific extra tokens, available after a successful
+ /// authentication
+ QVariantMap extraTokens();
+
+ public:
+ // TODO: put in `Options`
+ /// User-defined extra parameters to append to request URL
+ QVariantMap extraRequestParams();
+ void setExtraRequestParams(const QVariantMap& value);
+
+ // TODO: split up the class into multiple, each implementing one OAuth2
+ // flow
+ /// Grant type (if non-standard)
+ QString grantType();
+ void setGrantType(const QString& value);
+
+ public:
+ /// Constructor.
+ /// @param parent Parent object.
+ explicit DeviceFlow(Options& opts, Token& token, QObject* parent = 0,
+ QNetworkAccessManager* manager = 0);
+
+ /// Get refresh token.
+ QString refreshToken();
+
+ /// Get token expiration time
+ QDateTime expires();
+
+ public slots:
+ /// Authenticate.
+ void login();
+
+ /// De-authenticate.
+ void logout();
+
+ /// Refresh token.
+ bool refresh();
+
+ /// Handle situation where reply server has opted to close its
+ /// connection
+ void serverHasClosed(bool paramsfound = false);
+
+ signals:
+ /// Emitted when client needs to open a web browser window, with the
+ /// given URL.
+ void openBrowser(const QUrl& url);
+
+ /// Emitted when client can close the browser window.
+ void closeBrowser();
+
+ /// Emitted when client needs to show a verification uri and user code
+ void showVerificationUriAndCode(const QUrl& uri, const QString& code,
+ int expiresIn);
+
+ /// Emitted when the internal state changes
+ void activityChanged(Activity activity);
+
+ public slots:
+ /// Handle verification response.
+ void onVerificationReceived(QMap<QString, QString>);
+
+ protected slots:
+ /// Handle completion of a Device Authorization Request
+ void onDeviceAuthReplyFinished();
+
+ /// Handle completion of a refresh request.
+ void onRefreshFinished();
+
+ /// Handle failure of a refresh request.
+ void onRefreshError(QNetworkReply::NetworkError error,
+ QNetworkReply* reply);
+
+ protected:
+ /// Set refresh token.
+ void setRefreshToken(const QString& v);
+
+ /// Set token expiration time.
+ void setExpires(QDateTime v);
+
+ /// Start polling authorization server
+ void startPollServer(const QVariantMap& params, int expiresIn);
+
+ /// Set authentication token.
+ void setToken(const QString& v);
+
+ /// Set the linked state
+ void setLinked(bool v);
+
+ /// Set extra tokens found in OAuth response
+ void setExtraTokens(QVariantMap extraTokens);
+
+ /// Set local poll server
+ void setPollServer(PollServer* server);
+
+ PollServer* pollServer() const;
+
+ void updateActivity(Activity activity);
+
+ protected:
+ Options options_;
+
+ QVariantMap extraReqParams_;
+ QNetworkAccessManager* manager_ = nullptr;
+ ReplyList timedReplies_;
+ QString grantType_;
+
+ protected:
+ Token& token_;
+
+ private:
+ PollServer* pollServer_ = nullptr;
+ Activity activity_ = Activity::Idle;
+ };
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/include/katabasis/Globals.h b/meshmc/libraries/katabasis/include/katabasis/Globals.h
new file mode 100644
index 0000000000..cf6fa39b21
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/Globals.h
@@ -0,0 +1,82 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+namespace Katabasis
+{
+
+ // Common constants
+ const char ENCRYPTION_KEY[] = "12345678";
+ const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded";
+ const char MIME_TYPE_JSON[] = "application/json";
+
+ // OAuth 1/1.1 Request Parameters
+ const char OAUTH_CALLBACK[] = "oauth_callback";
+ const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key";
+ const char OAUTH_NONCE[] = "oauth_nonce";
+ const char OAUTH_SIGNATURE[] = "oauth_signature";
+ const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method";
+ const char OAUTH_TIMESTAMP[] = "oauth_timestamp";
+ const char OAUTH_VERSION[] = "oauth_version";
+ // OAuth 1/1.1 Response Parameters
+ const char OAUTH_TOKEN[] = "oauth_token";
+ const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret";
+ const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed";
+ const char OAUTH_VERFIER[] = "oauth_verifier";
+
+ // OAuth 2 Request Parameters
+ const char OAUTH2_RESPONSE_TYPE[] = "response_type";
+ const char OAUTH2_CLIENT_ID[] = "client_id";
+ const char OAUTH2_CLIENT_SECRET[] = "client_secret";
+ const char OAUTH2_USERNAME[] = "username";
+ const char OAUTH2_PASSWORD[] = "password";
+ const char OAUTH2_REDIRECT_URI[] = "redirect_uri";
+ const char OAUTH2_SCOPE[] = "scope";
+ const char OAUTH2_GRANT_TYPE_CODE[] = "code";
+ const char OAUTH2_GRANT_TYPE_TOKEN[] = "token";
+ const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password";
+ const char OAUTH2_GRANT_TYPE_DEVICE[] =
+ "urn:ietf:params:oauth:grant-type:device_code";
+ const char OAUTH2_GRANT_TYPE[] = "grant_type";
+ const char OAUTH2_API_KEY[] = "api_key";
+ const char OAUTH2_STATE[] = "state";
+ const char OAUTH2_CODE[] = "code";
+
+ // OAuth 2 Response Parameters
+ const char OAUTH2_ACCESS_TOKEN[] = "access_token";
+ const char OAUTH2_REFRESH_TOKEN[] = "refresh_token";
+ const char OAUTH2_EXPIRES_IN[] = "expires_in";
+ const char OAUTH2_DEVICE_CODE[] = "device_code";
+ const char OAUTH2_USER_CODE[] = "user_code";
+ const char OAUTH2_VERIFICATION_URI[] = "verification_uri";
+ const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in
+ const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete";
+ const char OAUTH2_INTERVAL[] = "interval";
+
+ // Parameter values
+ const char AUTHORIZATION_CODE[] = "authorization_code";
+
+ // Standard HTTP headers
+ const char HTTP_HTTP_HEADER[] = "HTTP";
+ const char HTTP_AUTHORIZATION_HEADER[] = "Authorization";
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/include/katabasis/PollServer.h b/meshmc/libraries/katabasis/include/katabasis/PollServer.h
new file mode 100644
index 0000000000..d070131f49
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/PollServer.h
@@ -0,0 +1,73 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QByteArray>
+#include <QMap>
+#include <QNetworkRequest>
+#include <QObject>
+#include <QString>
+#include <QTimer>
+
+class QNetworkAccessManager;
+
+namespace Katabasis
+{
+
+ /// Poll an authorization server for token
+ class PollServer : public QObject
+ {
+ Q_OBJECT
+
+ public:
+ explicit PollServer(QNetworkAccessManager* manager,
+ const QNetworkRequest& request,
+ const QByteArray& payload, int expiresIn,
+ QObject* parent = 0);
+
+ /// Seconds to wait between polling requests
+ Q_PROPERTY(int interval READ interval WRITE setInterval)
+ int interval() const;
+ void setInterval(int interval);
+
+ signals:
+ void verificationReceived(QMap<QString, QString>);
+ void serverClosed(bool); // whether it has found parameters
+
+ public slots:
+ void startPolling();
+
+ protected slots:
+ void onPollTimeout();
+ void onExpiration();
+ void onReplyFinished();
+
+ protected:
+ QNetworkAccessManager* manager_;
+ const QNetworkRequest request_;
+ const QByteArray payload_;
+ const int expiresIn_;
+ QTimer expirationTimer;
+ QTimer pollTimer;
+ };
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/include/katabasis/Reply.h b/meshmc/libraries/katabasis/include/katabasis/Reply.h
new file mode 100644
index 0000000000..dbfa939829
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/Reply.h
@@ -0,0 +1,92 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QList>
+#include <QTimer>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+#include <QByteArray>
+
+namespace Katabasis
+{
+
+ constexpr int defaultTimeout = 30 * 1000;
+
+ /// A network request/reply pair that can time out.
+ class Reply : public QTimer
+ {
+ Q_OBJECT
+
+ public:
+ Reply(QNetworkReply* reply, int timeOut = defaultTimeout,
+ QObject* parent = 0);
+
+ signals:
+ void error(QNetworkReply::NetworkError);
+
+ public slots:
+ /// When time out occurs, the QNetworkReply's error() signal is
+ /// triggered.
+ void onTimeOut();
+
+ public:
+ QNetworkReply* reply;
+ bool timedOut = false;
+ };
+
+ /// List of O2Replies.
+ class ReplyList
+ {
+ public:
+ ReplyList()
+ {
+ ignoreSslErrors_ = false;
+ }
+
+ /// Destructor.
+ /// Deletes all O2Reply instances in the list.
+ virtual ~ReplyList();
+
+ /// Create a new O2Reply from a QNetworkReply, and add it to this list.
+ void add(QNetworkReply* reply, int timeOut = defaultTimeout);
+
+ /// Add an O2Reply to the list, while taking ownership of it.
+ void add(Reply* reply);
+
+ /// Remove item from the list that corresponds to a QNetworkReply.
+ void remove(QNetworkReply* reply);
+
+ /// Find an O2Reply in the list, corresponding to a QNetworkReply.
+ /// @return Matching O2Reply or NULL.
+ Reply* find(QNetworkReply* reply);
+
+ bool ignoreSslErrors();
+ void setIgnoreSslErrors(bool ignoreSslErrors);
+
+ protected:
+ QList<Reply*> replies_;
+ bool ignoreSslErrors_;
+ };
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h b/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h
new file mode 100644
index 0000000000..e7ff1efda3
--- /dev/null
+++ b/meshmc/libraries/katabasis/include/katabasis/RequestParameter.h
@@ -0,0 +1,42 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+namespace Katabasis
+{
+
+ /// Request parameter (name-value pair) participating in authentication.
+ struct RequestParameter {
+ RequestParameter(const QByteArray& n, const QByteArray& v)
+ : name(n), value(v)
+ {
+ }
+ bool operator<(const RequestParameter& other) const
+ {
+ return (name == other.name) ? (value < other.value)
+ : (name < other.name);
+ }
+ QByteArray name;
+ QByteArray value;
+ };
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/src/DeviceFlow.cpp b/meshmc/libraries/katabasis/src/DeviceFlow.cpp
new file mode 100644
index 0000000000..d03b3efd8f
--- /dev/null
+++ b/meshmc/libraries/katabasis/src/DeviceFlow.cpp
@@ -0,0 +1,539 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QList>
+#include <QPair>
+#include <QDebug>
+#include <QTcpServer>
+#include <QMap>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+#include <QDateTime>
+#include <QCryptographicHash>
+#include <QTimer>
+#include <QVariantMap>
+#include <QUuid>
+#include <QDataStream>
+
+#include <QUrlQuery>
+
+#include "katabasis/DeviceFlow.h"
+#include "katabasis/PollServer.h"
+#include "katabasis/Globals.h"
+
+#include "JsonResponse.h"
+
+namespace
+{
+ // ref: https://tools.ietf.org/html/rfc8628#section-3.2
+ // Exception: Google sign-in uses "verification_url" instead of "*_uri" -
+ // we'll accept both.
+ bool hasMandatoryDeviceAuthParams(const QVariantMap& params)
+ {
+ if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE))
+ return false;
+
+ if (!params.contains(Katabasis::OAUTH2_USER_CODE))
+ return false;
+
+ if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) ||
+ params.contains(Katabasis::OAUTH2_VERIFICATION_URL)))
+ return false;
+
+ if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN))
+ return false;
+
+ return true;
+ }
+
+ QByteArray
+ createQueryParameters(const QList<Katabasis::RequestParameter>& parameters)
+ {
+ QByteArray ret;
+ bool first = true;
+ for (auto& h : parameters) {
+ if (first) {
+ first = false;
+ } else {
+ ret.append("&");
+ }
+ ret.append(QUrl::toPercentEncoding(h.name) + "=" +
+ QUrl::toPercentEncoding(h.value));
+ }
+ return ret;
+ }
+} // namespace
+
+namespace Katabasis
+{
+
+ DeviceFlow::DeviceFlow(Options& opts, Token& token, QObject* parent,
+ QNetworkAccessManager* manager)
+ : QObject(parent), token_(token)
+ {
+ manager_ = manager ? manager : new QNetworkAccessManager(this);
+ qRegisterMetaType<QNetworkReply::NetworkError>(
+ "QNetworkReply::NetworkError");
+ options_ = opts;
+ }
+
+ bool DeviceFlow::linked()
+ {
+ return token_.validity != Validity::None;
+ }
+ void DeviceFlow::setLinked(bool v)
+ {
+ qDebug() << "DeviceFlow::setLinked:" << (v ? "true" : "false");
+ token_.validity = v ? Validity::Certain : Validity::None;
+ }
+
+ void DeviceFlow::updateActivity(Activity activity)
+ {
+ if (activity_ == activity) {
+ return;
+ }
+
+ activity_ = activity;
+ switch (activity) {
+ case Katabasis::Activity::Idle:
+ case Katabasis::Activity::LoggingIn:
+ case Katabasis::Activity::LoggingOut:
+ case Katabasis::Activity::Refreshing:
+ // non-terminal states...
+ break;
+ case Katabasis::Activity::FailedSoft:
+ // terminal state, tokens did not change
+ break;
+ case Katabasis::Activity::FailedHard:
+ case Katabasis::Activity::FailedGone:
+ // terminal state, tokens are invalid
+ token_ = Token();
+ break;
+ case Katabasis::Activity::Succeeded:
+ setLinked(true);
+ break;
+ }
+ emit activityChanged(activity_);
+ }
+
+ QString DeviceFlow::token()
+ {
+ return token_.token;
+ }
+ void DeviceFlow::setToken(const QString& v)
+ {
+ token_.token = v;
+ }
+
+ QVariantMap DeviceFlow::extraTokens()
+ {
+ return token_.extra;
+ }
+
+ void DeviceFlow::setExtraTokens(QVariantMap extraTokens)
+ {
+ token_.extra = extraTokens;
+ }
+
+ void DeviceFlow::setPollServer(PollServer* server)
+ {
+ if (pollServer_)
+ pollServer_->deleteLater();
+
+ pollServer_ = server;
+ }
+
+ PollServer* DeviceFlow::pollServer() const
+ {
+ return pollServer_;
+ }
+
+ QVariantMap DeviceFlow::extraRequestParams()
+ {
+ return extraReqParams_;
+ }
+
+ void DeviceFlow::setExtraRequestParams(const QVariantMap& value)
+ {
+ extraReqParams_ = value;
+ }
+
+ QString DeviceFlow::grantType()
+ {
+ if (!grantType_.isEmpty())
+ return grantType_;
+
+ return OAUTH2_GRANT_TYPE_DEVICE;
+ }
+
+ void DeviceFlow::setGrantType(const QString& value)
+ {
+ grantType_ = value;
+ }
+
+ // First get the URL and token to display to the user
+ void DeviceFlow::login()
+ {
+ qDebug() << "DeviceFlow::link";
+
+ updateActivity(Activity::LoggingIn);
+ setLinked(false);
+ setToken("");
+ setExtraTokens(QVariantMap());
+ setRefreshToken(QString());
+ setExpires(QDateTime());
+
+ QList<RequestParameter> parameters;
+ parameters.append(RequestParameter(OAUTH2_CLIENT_ID,
+ options_.clientIdentifier.toUtf8()));
+ parameters.append(
+ RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8()));
+ QByteArray payload = createQueryParameters(parameters);
+
+ QUrl url(options_.authorizationUrl);
+ QNetworkRequest deviceRequest(url);
+ deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+ QNetworkReply* tokenReply = manager_->post(deviceRequest, payload);
+
+ connect(tokenReply, &QNetworkReply::finished, this,
+ &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection);
+ }
+
+ // Then, once we get them, present them to the user
+ void DeviceFlow::onDeviceAuthReplyFinished()
+ {
+ qDebug() << "DeviceFlow::onDeviceAuthReplyFinished";
+ QNetworkReply* tokenReply = qobject_cast<QNetworkReply*>(sender());
+ if (!tokenReply) {
+ qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null";
+ return;
+ }
+ if (tokenReply->error() == QNetworkReply::NoError) {
+ QByteArray replyData = tokenReply->readAll();
+
+ // Dump replyData
+ // SENSITIVE DATA in RelWithDebInfo or Debug builds
+ // qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n";
+ // qDebug() << QString( replyData );
+
+ QVariantMap params = parseJsonResponse(replyData);
+
+ // Dump tokens
+ qDebug()
+ << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n";
+ foreach (QString key, params.keys()) {
+ // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is
+ // truncated first
+ qDebug() << key << ": " << params.value(key).toString();
+ }
+
+ // Check for mandatory parameters
+ if (hasMandatoryDeviceAuthParams(params)) {
+ qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device "
+ "auth request response";
+
+ const QString userCode =
+ params.take(OAUTH2_USER_CODE).toString();
+ QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl();
+ if (uri.isEmpty())
+ uri = params.take(OAUTH2_VERIFICATION_URL).toUrl();
+
+ if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE))
+ emit openBrowser(
+ params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl());
+
+ bool ok = false;
+ int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
+ if (!ok) {
+ qWarning() << "DeviceFlow::startPollServer: No expired_in "
+ "parameter";
+ updateActivity(Activity::FailedHard);
+ return;
+ }
+
+ emit showVerificationUriAndCode(uri, userCode, expiresIn);
+
+ startPollServer(params, expiresIn);
+ } else {
+ qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: "
+ "Mandatory parameters missing from response";
+ updateActivity(Activity::FailedHard);
+ }
+ }
+ tokenReply->deleteLater();
+ }
+
+ // Spin up polling for the user completing the login flow out of band
+ void DeviceFlow::startPollServer(const QVariantMap& params, int expiresIn)
+ {
+ qDebug()
+ << "DeviceFlow::startPollServer: device_ and user_code expires in"
+ << expiresIn << "seconds";
+
+ QUrl url(options_.accessTokenUrl);
+ QNetworkRequest authRequest(url);
+ authRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+
+ const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString();
+ const QString grantType =
+ grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_;
+
+ QList<RequestParameter> parameters;
+ parameters.append(RequestParameter(OAUTH2_CLIENT_ID,
+ options_.clientIdentifier.toUtf8()));
+ if (!options_.clientSecret.isEmpty()) {
+ parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET,
+ options_.clientSecret.toUtf8()));
+ }
+ parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8()));
+ parameters.append(
+ RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8()));
+ QByteArray payload = createQueryParameters(parameters);
+
+ PollServer* pollServer =
+ new PollServer(manager_, authRequest, payload, expiresIn, this);
+ if (params.contains(OAUTH2_INTERVAL)) {
+ bool ok = false;
+ int interval = params[OAUTH2_INTERVAL].toInt(&ok);
+ if (ok) {
+ pollServer->setInterval(interval);
+ }
+ }
+ connect(pollServer, &PollServer::verificationReceived, this,
+ &DeviceFlow::onVerificationReceived);
+ connect(pollServer, &PollServer::serverClosed, this,
+ &DeviceFlow::serverHasClosed);
+ setPollServer(pollServer);
+ pollServer->startPolling();
+ }
+
+ // Once the user completes the flow, update the internal state and report it
+ // to observers
+ void
+ DeviceFlow::onVerificationReceived(const QMap<QString, QString> response)
+ {
+ qDebug()
+ << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()";
+ emit closeBrowser();
+
+ if (response.contains("error")) {
+ qWarning()
+ << "DeviceFlow::onVerificationReceived: Verification failed:"
+ << response;
+ updateActivity(Activity::FailedHard);
+ return;
+ }
+
+ // Check for mandatory tokens
+ if (response.contains(OAUTH2_ACCESS_TOKEN)) {
+ qDebug() << "DeviceFlow::onVerificationReceived: Access token "
+ "returned for implicit or device flow";
+ setToken(response.value(OAUTH2_ACCESS_TOKEN));
+ if (response.contains(OAUTH2_EXPIRES_IN)) {
+ bool ok = false;
+ int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok);
+ if (ok) {
+ qDebug() << "DeviceFlow::onVerificationReceived: Token "
+ "expires in"
+ << expiresIn << "seconds";
+ setExpires(
+ QDateTime::currentDateTimeUtc().addSecs(expiresIn));
+ }
+ }
+ if (response.contains(OAUTH2_REFRESH_TOKEN)) {
+ setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN));
+ }
+ updateActivity(Activity::Succeeded);
+ } else {
+ qWarning() << "DeviceFlow::onVerificationReceived: Access token "
+ "missing from response for implicit or device flow";
+ updateActivity(Activity::FailedHard);
+ }
+ }
+
+ // Or if the flow fails or the polling times out, update the internal state
+ // with error and report it to observers
+ void DeviceFlow::serverHasClosed(bool paramsfound)
+ {
+ if (!paramsfound) {
+ // server has probably timed out after receiving first response
+ updateActivity(Activity::FailedHard);
+ }
+ // poll server is not re-used for later auth requests
+ setPollServer(NULL);
+ }
+
+ void DeviceFlow::logout()
+ {
+ qDebug() << "DeviceFlow::unlink";
+ updateActivity(Activity::LoggingOut);
+ // FIXME: implement logout flows... if they exist
+ token_ = Token();
+ updateActivity(Activity::FailedHard);
+ }
+
+ QDateTime DeviceFlow::expires()
+ {
+ return token_.notAfter;
+ }
+ void DeviceFlow::setExpires(QDateTime v)
+ {
+ token_.notAfter = v;
+ }
+
+ QString DeviceFlow::refreshToken()
+ {
+ return token_.refresh_token;
+ }
+
+ void DeviceFlow::setRefreshToken(const QString& v)
+ {
+#ifndef NDEBUG
+ qDebug() << "DeviceFlow::setRefreshToken" << v << "...";
+#endif
+ token_.refresh_token = v;
+ }
+
+ namespace
+ {
+ QByteArray buildRequestBody(const QMap<QString, QString>& parameters)
+ {
+ QByteArray body;
+ bool first = true;
+ foreach (QString key, parameters.keys()) {
+ if (first) {
+ first = false;
+ } else {
+ body.append("&");
+ }
+ QString value = parameters.value(key);
+ body.append(QUrl::toPercentEncoding(key) +
+ QString("=").toUtf8() +
+ QUrl::toPercentEncoding(value));
+ }
+ return body;
+ }
+ } // namespace
+
+ bool DeviceFlow::refresh()
+ {
+ qDebug() << "DeviceFlow::refresh: Token: ..."
+ << refreshToken().right(7);
+
+ updateActivity(Activity::Refreshing);
+
+ if (refreshToken().isEmpty()) {
+ qWarning() << "DeviceFlow::refresh: No refresh token";
+ onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
+ return false;
+ }
+ if (options_.accessTokenUrl.isEmpty()) {
+ qWarning() << "DeviceFlow::refresh: Refresh token URL not set";
+ onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr);
+ return false;
+ }
+
+ QNetworkRequest refreshRequest(options_.accessTokenUrl);
+ refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+ MIME_TYPE_XFORM);
+ QMap<QString, QString> parameters;
+ parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier);
+ if (!options_.clientSecret.isEmpty()) {
+ parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret);
+ }
+ parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken());
+ parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN);
+
+ QByteArray data = buildRequestBody(parameters);
+ QNetworkReply* refreshReply = manager_->post(refreshRequest, data);
+ timedReplies_.add(refreshReply);
+ connect(refreshReply, &QNetworkReply::finished, this,
+ &DeviceFlow::onRefreshFinished, Qt::QueuedConnection);
+ return true;
+ }
+
+ void DeviceFlow::onRefreshFinished()
+ {
+ QNetworkReply* refreshReply = qobject_cast<QNetworkReply*>(sender());
+
+ auto networkError = refreshReply->error();
+ if (networkError == QNetworkReply::NoError) {
+ QByteArray reply = refreshReply->readAll();
+ QVariantMap tokens = parseJsonResponse(reply);
+ setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString());
+ setExpires(QDateTime::currentDateTimeUtc().addSecs(
+ tokens.value(OAUTH2_EXPIRES_IN).toInt()));
+ QString refreshToken =
+ tokens.value(OAUTH2_REFRESH_TOKEN).toString();
+ if (!refreshToken.isEmpty()) {
+ setRefreshToken(refreshToken);
+ } else {
+ qDebug() << "No new refresh token. Keep the old one.";
+ }
+ timedReplies_.remove(refreshReply);
+ refreshReply->deleteLater();
+ updateActivity(Activity::Succeeded);
+ qDebug() << "New token expires in" << expires() << "seconds";
+ } else {
+ // FIXME: differentiate the error more here
+ onRefreshError(networkError, refreshReply);
+ }
+ }
+
+ void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error,
+ QNetworkReply* refreshReply)
+ {
+ QString errorString = "No Reply";
+ if (refreshReply) {
+ timedReplies_.remove(refreshReply);
+ errorString = refreshReply->errorString();
+ }
+
+ switch (error) {
+ // used for invalid credentials and similar errors. Fall through.
+ case QNetworkReply::AuthenticationRequiredError:
+ case QNetworkReply::ContentAccessDenied:
+ case QNetworkReply::ContentOperationNotPermittedError:
+ case QNetworkReply::ProtocolInvalidOperationError:
+ updateActivity(Activity::FailedHard);
+ break;
+ case QNetworkReply::ContentGoneError: {
+ updateActivity(Activity::FailedGone);
+ break;
+ }
+ case QNetworkReply::TimeoutError:
+ case QNetworkReply::OperationCanceledError:
+ case QNetworkReply::SslHandshakeFailedError:
+ default:
+ updateActivity(Activity::FailedSoft);
+ return;
+ }
+ if (refreshReply) {
+ refreshReply->deleteLater();
+ }
+ qDebug() << "DeviceFlow::onRefreshFinished: Error" << (int)error
+ << " - " << errorString;
+ }
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/src/JsonResponse.cpp b/meshmc/libraries/katabasis/src/JsonResponse.cpp
new file mode 100644
index 0000000000..8daee82ebb
--- /dev/null
+++ b/meshmc/libraries/katabasis/src/JsonResponse.cpp
@@ -0,0 +1,51 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "JsonResponse.h"
+
+#include <QByteArray>
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace Katabasis
+{
+
+ QVariantMap parseJsonResponse(const QByteArray& data)
+ {
+ QJsonParseError err;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &err);
+ if (err.error != QJsonParseError::NoError) {
+ qWarning() << "parseTokenResponse: Failed to parse token response "
+ "due to err:"
+ << err.errorString();
+ return QVariantMap();
+ }
+
+ if (!doc.isObject()) {
+ qWarning() << "parseTokenResponse: Token response is not an object";
+ return QVariantMap();
+ }
+
+ return doc.object().toVariantMap();
+ }
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/src/JsonResponse.h b/meshmc/libraries/katabasis/src/JsonResponse.h
new file mode 100644
index 0000000000..0662c8ff61
--- /dev/null
+++ b/meshmc/libraries/katabasis/src/JsonResponse.h
@@ -0,0 +1,34 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QVariantMap>
+
+class QByteArray;
+
+namespace Katabasis
+{
+
+ /// Parse JSON data into a QVariantMap
+ QVariantMap parseJsonResponse(const QByteArray& data);
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/src/PollServer.cpp b/meshmc/libraries/katabasis/src/PollServer.cpp
new file mode 100644
index 0000000000..1c8556aa98
--- /dev/null
+++ b/meshmc/libraries/katabasis/src/PollServer.cpp
@@ -0,0 +1,147 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include "katabasis/PollServer.h"
+#include "JsonResponse.h"
+
+namespace
+{
+ QMap<QString, QString> toVerificationParams(const QVariantMap& map)
+ {
+ QMap<QString, QString> params;
+ for (QVariantMap::const_iterator i = map.constBegin();
+ i != map.constEnd(); ++i) {
+ params[i.key()] = i.value().toString();
+ }
+ return params;
+ }
+} // namespace
+
+namespace Katabasis
+{
+
+ PollServer::PollServer(QNetworkAccessManager* manager,
+ const QNetworkRequest& request,
+ const QByteArray& payload, int expiresIn,
+ QObject* parent)
+ : QObject(parent), manager_(manager), request_(request),
+ payload_(payload), expiresIn_(expiresIn)
+ {
+ expirationTimer.setTimerType(Qt::VeryCoarseTimer);
+ expirationTimer.setInterval(expiresIn * 1000);
+ expirationTimer.setSingleShot(true);
+ connect(&expirationTimer, SIGNAL(timeout()), this,
+ SLOT(onExpiration()));
+ expirationTimer.start();
+
+ pollTimer.setTimerType(Qt::VeryCoarseTimer);
+ pollTimer.setInterval(5 * 1000);
+ pollTimer.setSingleShot(true);
+ connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout()));
+ }
+
+ int PollServer::interval() const
+ {
+ return pollTimer.interval() / 1000;
+ }
+
+ void PollServer::setInterval(int interval)
+ {
+ pollTimer.setInterval(interval * 1000);
+ }
+
+ void PollServer::startPolling()
+ {
+ if (expirationTimer.isActive()) {
+ pollTimer.start();
+ }
+ }
+
+ void PollServer::onPollTimeout()
+ {
+ qDebug() << "PollServer::onPollTimeout: retrying";
+ QNetworkReply* reply = manager_->post(request_, payload_);
+ connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
+ }
+
+ void PollServer::onExpiration()
+ {
+ pollTimer.stop();
+ emit serverClosed(false);
+ }
+
+ void PollServer::onReplyFinished()
+ {
+ QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
+
+ if (!reply) {
+ qDebug() << "PollServer::onReplyFinished: reply is null";
+ return;
+ }
+
+ QByteArray replyData = reply->readAll();
+ QMap<QString, QString> params =
+ toVerificationParams(parseJsonResponse(replyData));
+
+ // Dump replyData
+ // SENSITIVE DATA in RelWithDebInfo or Debug builds
+ // qDebug() << "PollServer::onReplyFinished: replyData\n";
+ // qDebug() << QString( replyData );
+
+ if (reply->error() == QNetworkReply::TimeoutError) {
+ // rfc8628#section-3.2
+ // "On encountering a connection timeout, clients MUST unilaterally
+ // reduce their polling frequency before retrying. The use of an
+ // exponential backoff algorithm to achieve this, such as doubling
+ // the polling interval on each such connection timeout, is
+ // RECOMMENDED."
+ setInterval(interval() * 2);
+ pollTimer.start();
+ } else {
+ QString error = params.value("error");
+ if (error == "slow_down") {
+ // rfc8628#section-3.2
+ // "A variant of 'authorization_pending', the authorization
+ // request is still pending and polling should continue, but the
+ // interval MUST be increased by 5 seconds for this and all
+ // subsequent requests."
+ setInterval(interval() + 5);
+ pollTimer.start();
+ } else if (error == "authorization_pending") {
+ // keep trying - rfc8628#section-3.2
+ // "The authorization request is still pending as the end user
+ // hasn't yet completed the user-interaction steps
+ // (Section 3.3)."
+ pollTimer.start();
+ } else {
+ expirationTimer.stop();
+ emit serverClosed(true);
+ // let O2 handle the other cases
+ emit verificationReceived(params);
+ }
+ }
+ reply->deleteLater();
+ }
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/katabasis/src/Reply.cpp b/meshmc/libraries/katabasis/src/Reply.cpp
new file mode 100644
index 0000000000..c19113ac50
--- /dev/null
+++ b/meshmc/libraries/katabasis/src/Reply.cpp
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTimer>
+#include <QNetworkReply>
+
+#include "katabasis/Reply.h"
+
+namespace Katabasis
+{
+
+ Reply::Reply(QNetworkReply* r, int timeOut, QObject* parent)
+ : QTimer(parent), reply(r)
+ {
+ setSingleShot(true);
+ connect(this, &Reply::timeout, this, &Reply::onTimeOut,
+ Qt::QueuedConnection);
+ start(timeOut);
+ }
+
+ void Reply::onTimeOut()
+ {
+ timedOut = true;
+ reply->abort();
+ }
+
+ // ----------------------------
+
+ ReplyList::~ReplyList()
+ {
+ foreach (Reply* timedReply, replies_) {
+ delete timedReply;
+ }
+ }
+
+ void ReplyList::add(QNetworkReply* reply, int timeOut)
+ {
+ if (reply && ignoreSslErrors()) {
+ reply->ignoreSslErrors();
+ }
+ add(new Reply(reply, timeOut));
+ }
+
+ void ReplyList::add(Reply* reply)
+ {
+ replies_.append(reply);
+ }
+
+ void ReplyList::remove(QNetworkReply* reply)
+ {
+ Reply* o2Reply = find(reply);
+ if (o2Reply) {
+ o2Reply->stop();
+ (void)replies_.removeOne(o2Reply);
+ }
+ }
+
+ Reply* ReplyList::find(QNetworkReply* reply)
+ {
+ foreach (Reply* timedReply, replies_) {
+ if (timedReply->reply == reply) {
+ return timedReply;
+ }
+ }
+ return 0;
+ }
+
+ bool ReplyList::ignoreSslErrors()
+ {
+ return ignoreSslErrors_;
+ }
+
+ void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors)
+ {
+ ignoreSslErrors_ = ignoreSslErrors;
+ }
+
+} // namespace Katabasis
diff --git a/meshmc/libraries/launcher/.gitignore b/meshmc/libraries/launcher/.gitignore
new file mode 100644
index 0000000000..cc1c52bf4d
--- /dev/null
+++ b/meshmc/libraries/launcher/.gitignore
@@ -0,0 +1,6 @@
+.idea
+*.iml
+out
+.classpath
+.idea
+.project
diff --git a/meshmc/libraries/launcher/CMakeLists.txt b/meshmc/libraries/launcher/CMakeLists.txt
new file mode 100644
index 0000000000..860776385f
--- /dev/null
+++ b/meshmc/libraries/launcher/CMakeLists.txt
@@ -0,0 +1,22 @@
+cmake_minimum_required(VERSION 3.25)
+project(launcher Java)
+find_package(Java 1.7 REQUIRED COMPONENTS Development)
+
+include(UseJava)
+set(CMAKE_JAVA_JAR_ENTRY_POINT org.projecttick.EntryPoint)
+set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked)
+
+set(SRC
+ org/projecttick/EntryPoint.java
+ org/projecttick/MeshMC.java
+ org/projecttick/LegacyFrame.java
+ org/projecttick/NotFoundException.java
+ org/projecttick/ParamBucket.java
+ org/projecttick/ParseException.java
+ org/projecttick/Utils.java
+ org/projecttick/onesix/OneSixLauncher.java
+ org/projecttick/modern/ModernLauncher.java
+ net/minecraft/MeshMC.java
+)
+add_jar(NewLaunch ${SRC})
+install_jar(NewLaunch "${JARS_DEST_DIR}")
diff --git a/meshmc/libraries/launcher/net/minecraft/MeshMC.java b/meshmc/libraries/launcher/net/minecraft/MeshMC.java
new file mode 100644
index 0000000000..0d4da46d70
--- /dev/null
+++ b/meshmc/libraries/launcher/net/minecraft/MeshMC.java
@@ -0,0 +1,208 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.minecraft;
+
+import java.util.TreeMap;
+import java.util.Map;
+import java.net.URL;
+import java.awt.Dimension;
+import java.awt.BorderLayout;
+import java.awt.Graphics;
+import java.applet.Applet;
+import java.applet.AppletStub;
+import java.net.MalformedURLException;
+
+public class MeshMC extends Applet implements AppletStub
+{
+ private Applet wrappedApplet;
+ private URL documentBase;
+ private boolean active = false;
+ private final Map<String, String> params;
+
+ public MeshMC(Applet applet, URL documentBase)
+ {
+ params = new TreeMap<String, String>();
+
+ this.setLayout(new BorderLayout());
+ this.add(applet, "Center");
+ this.wrappedApplet = applet;
+ this.documentBase = documentBase;
+ }
+
+ public void setParameter(String name, String value)
+ {
+ params.put(name, value);
+ }
+
+ public void replace(Applet applet)
+ {
+ this.wrappedApplet = applet;
+
+ applet.setStub(this);
+ applet.setSize(getWidth(), getHeight());
+
+ this.setLayout(new BorderLayout());
+ this.add(applet, "Center");
+
+ applet.init();
+ active = true;
+ applet.start();
+ validate();
+ }
+
+ @Override
+ public String getParameter(String name)
+ {
+ String param = params.get(name);
+ if (param != null)
+ return param;
+ try
+ {
+ return super.getParameter(name);
+ } catch (Exception ignore){}
+ return null;
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return active;
+ }
+
+ @Override
+ public void appletResize(int width, int height)
+ {
+ wrappedApplet.resize(width, height);
+ }
+
+ @Override
+ public void resize(int width, int height)
+ {
+ wrappedApplet.resize(width, height);
+ }
+
+ @Override
+ public void resize(Dimension d)
+ {
+ wrappedApplet.resize(d);
+ }
+
+ @Override
+ public void init()
+ {
+ if (wrappedApplet != null)
+ {
+ wrappedApplet.init();
+ }
+ }
+
+ @Override
+ public void start()
+ {
+ wrappedApplet.start();
+ active = true;
+ }
+
+ @Override
+ public void stop()
+ {
+ wrappedApplet.stop();
+ active = false;
+ }
+
+ public void destroy()
+ {
+ wrappedApplet.destroy();
+ }
+
+ @Override
+ public URL getCodeBase() {
+ try {
+ return new URL("http://www.minecraft.net/game/");
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public URL getDocumentBase()
+ {
+ try {
+ // Special case only for Classic versions
+ if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) {
+ return new URL("http", "www.minecraft.net", 80, "/game/", null);
+ }
+ return new URL("http://www.minecraft.net/game/");
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ @Override
+ public void setVisible(boolean b)
+ {
+ super.setVisible(b);
+ wrappedApplet.setVisible(b);
+ }
+ public void update(Graphics paramGraphics)
+ {
+ }
+ public void paint(Graphics paramGraphics)
+ {
+ }
+} \ No newline at end of file
diff --git a/meshmc/libraries/launcher/org/projecttick/EntryPoint.java b/meshmc/libraries/launcher/org/projecttick/EntryPoint.java
new file mode 100644
index 0000000000..ca0c63f22d
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/EntryPoint.java
@@ -0,0 +1,200 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+import org.projecttick.modern.ModernLauncher;
+import org.projecttick.onesix.OneSixLauncher;
+
+import java.io.*;
+import java.nio.charset.Charset;
+
+public class EntryPoint
+{
+ private enum Action
+ {
+ Proceed,
+ Launch,
+ Abort
+ }
+
+ public static void main(String[] args)
+ {
+ EntryPoint listener = new EntryPoint();
+ int retCode = listener.listen();
+ if (retCode != 0)
+ {
+ System.out.println("Exiting with " + retCode);
+ System.exit(retCode);
+ }
+ }
+
+ private Action parseLine(String inData) throws ParseException
+ {
+ String[] pair = inData.split(" ", 2);
+
+ if(pair.length == 1)
+ {
+ String command = pair[0];
+ if (pair[0].equals("launch"))
+ return Action.Launch;
+
+ else if (pair[0].equals("abort"))
+ return Action.Abort;
+
+ else throw new ParseException("Error while parsing:" + pair[0]);
+ }
+
+ if(pair.length != 2)
+ throw new ParseException("Pair length is not 2.");
+
+ String command = pair[0];
+ String param = pair[1];
+
+ if(command.equals("launcher"))
+ {
+ if(param.equals("onesix"))
+ {
+ m_launcher = new OneSixLauncher();
+ Utils.log("Using onesix launcher.");
+ Utils.log();
+ return Action.Proceed;
+ }
+ else if(param.equals("modern"))
+ {
+ m_launcher = new ModernLauncher();
+ Utils.log("Using modern launcher (subprocess mode).");
+ Utils.log();
+ return Action.Proceed;
+ }
+ else
+ throw new ParseException("Invalid launcher type: " + param);
+ }
+
+ m_params.add(command, param);
+ //System.out.println(command + " : " + param);
+ return Action.Proceed;
+ }
+
+ public int listen()
+ {
+ BufferedReader buffer;
+ try
+ {
+ buffer = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
+ } catch (UnsupportedEncodingException e)
+ {
+ System.err.println("For some reason, your java does not support UTF-8. Consider living in the current century.");
+ e.printStackTrace();
+ return 1;
+ }
+ boolean isListening = true;
+ boolean isAborted = false;
+ // Main loop
+ while (isListening)
+ {
+ String inData;
+ try
+ {
+ // Read from the pipe one line at a time
+ inData = buffer.readLine();
+ if (inData != null)
+ {
+ Action a = parseLine(inData);
+ if(a == Action.Abort)
+ {
+ isListening = false;
+ isAborted = true;
+ }
+ if(a == Action.Launch)
+ {
+ isListening = false;
+ }
+ }
+ else
+ {
+ isListening = false;
+ isAborted = true;
+ }
+ }
+ catch (IOException e)
+ {
+ System.err.println("MeshMC ABORT due to IO exception:");
+ e.printStackTrace();
+ return 1;
+ }
+ catch (ParseException e)
+ {
+ System.err.println("MeshMC ABORT due to PARSE exception:");
+ e.printStackTrace();
+ return 1;
+ }
+ }
+ if(isAborted)
+ {
+ System.err.println("Launch aborted by MeshMC.");
+ return 1;
+ }
+ if(m_launcher != null)
+ {
+ return m_launcher.launch(m_params);
+ }
+ System.err.println("No valid launcher implementation specified.");
+ return 1;
+ }
+
+ private ParamBucket m_params = new ParamBucket();
+ private org.projecttick.MeshMC m_launcher;
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/LegacyFrame.java b/meshmc/libraries/launcher/org/projecttick/LegacyFrame.java
new file mode 100644
index 0000000000..0ea9936e33
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/LegacyFrame.java
@@ -0,0 +1,217 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+import net.minecraft.MeshMC;
+
+import javax.imageio.ImageIO;
+import java.applet.Applet;
+import java.awt.*;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Scanner;
+
+public class LegacyFrame extends Frame implements WindowListener
+{
+ private MeshMC appletWrap = null;
+ public LegacyFrame(String title)
+ {
+ super ( title );
+ BufferedImage image;
+ try {
+ image = ImageIO.read ( new File ( "icon.png" ) );
+ setIconImage ( image );
+ } catch ( IOException e ) {
+ e.printStackTrace();
+ }
+ this.addWindowListener ( this );
+ }
+
+ public void start (
+ Applet mcApplet,
+ String user,
+ String session,
+ int winSizeW,
+ int winSizeH,
+ boolean maximize,
+ String serverAddress,
+ String serverPort
+ )
+ {
+ try {
+ appletWrap = new MeshMC( mcApplet, new URL ( "http://www.minecraft.net/game" ) );
+ } catch ( MalformedURLException ignored ) {}
+
+ // Implements support for launching in to multiplayer on classic servers using a mpticket
+ // file generated by an external program and stored in the instance's root folder.
+ File mpticketFile = null;
+ Scanner fileReader = null;
+ try {
+ mpticketFile = new File(System.getProperty("user.dir") + "/../mpticket").getCanonicalFile();
+ fileReader = new Scanner(new FileInputStream(mpticketFile), "ascii");
+ String[] mpticketParams = new String[3];
+
+ for(int i=0;i<3;i++) {
+ if(fileReader.hasNextLine()) {
+ mpticketParams[i] = fileReader.nextLine();
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ // Assumes parameters are valid and in the correct order
+ appletWrap.setParameter("server", mpticketParams[0]);
+ appletWrap.setParameter("port", mpticketParams[1]);
+ appletWrap.setParameter("mppass", mpticketParams[2]);
+
+ fileReader.close();
+ mpticketFile.delete();
+ }
+ catch (FileNotFoundException e) {}
+ catch (IllegalArgumentException e) {
+
+ fileReader.close();
+ File mpticketFileCorrupt = new File(System.getProperty("user.dir") + "/../mpticket.corrupt");
+ if(mpticketFileCorrupt.exists()) {
+ mpticketFileCorrupt.delete();
+ }
+ mpticketFile.renameTo(mpticketFileCorrupt);
+
+ System.err.println("Malformed mpticket file, missing argument.");
+ e.printStackTrace(System.err);
+ System.exit(-1);
+ }
+ catch (Exception e) {
+ e.printStackTrace(System.err);
+ System.exit(-1);
+ }
+
+ if (serverAddress != null)
+ {
+ appletWrap.setParameter("server", serverAddress);
+ appletWrap.setParameter("port", serverPort);
+ }
+
+ appletWrap.setParameter ( "username", user );
+ appletWrap.setParameter ( "sessionid", session );
+ appletWrap.setParameter ( "stand-alone", "true" ); // Show the quit button.
+ appletWrap.setParameter ( "haspaid", "true" ); // Some old versions need this for world saves to work.
+ appletWrap.setParameter ( "demo", "false" );
+ appletWrap.setParameter ( "fullscreen", "false" );
+ mcApplet.setStub(appletWrap);
+ this.add ( appletWrap );
+ appletWrap.setPreferredSize ( new Dimension (winSizeW, winSizeH) );
+ this.pack();
+ this.setLocationRelativeTo ( null );
+ this.setResizable ( true );
+ if ( maximize ) {
+ this.setExtendedState ( MAXIMIZED_BOTH );
+ }
+ validate();
+ appletWrap.init();
+ appletWrap.start();
+ setVisible ( true );
+ }
+
+ @Override
+ public void windowActivated ( WindowEvent e ) {}
+
+ @Override
+ public void windowClosed ( WindowEvent e ) {}
+
+ @Override
+ public void windowClosing ( WindowEvent e )
+ {
+ new Thread() {
+ public void run() {
+ try {
+ Thread.sleep ( 30000L );
+ } catch ( InterruptedException localInterruptedException ) {
+ localInterruptedException.printStackTrace();
+ }
+ System.out.println ( "FORCING EXIT!" );
+ System.exit ( 0 );
+ }
+ }
+ .start();
+
+ if ( appletWrap != null ) {
+ appletWrap.stop();
+ appletWrap.destroy();
+ }
+ // old minecraft versions can hang without this >_<
+ System.exit ( 0 );
+ }
+
+ @Override
+ public void windowDeactivated ( WindowEvent e ) {}
+
+ @Override
+ public void windowDeiconified ( WindowEvent e ) {}
+
+ @Override
+ public void windowIconified ( WindowEvent e ) {}
+
+ @Override
+ public void windowOpened ( WindowEvent e ) {}
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/MeshMC.java b/meshmc/libraries/launcher/org/projecttick/MeshMC.java
new file mode 100644
index 0000000000..dae081c70f
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/MeshMC.java
@@ -0,0 +1,61 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+public interface MeshMC
+{
+ abstract int launch(ParamBucket params);
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/NotFoundException.java b/meshmc/libraries/launcher/org/projecttick/NotFoundException.java
new file mode 100644
index 0000000000..fdf94ace4c
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/NotFoundException.java
@@ -0,0 +1,60 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+public class NotFoundException extends Exception
+{
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/ParamBucket.java b/meshmc/libraries/launcher/org/projecttick/ParamBucket.java
new file mode 100644
index 0000000000..97773c9605
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/ParamBucket.java
@@ -0,0 +1,125 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class ParamBucket
+{
+ public void add(String key, String value)
+ {
+ List<String> coll = null;
+ if(!m_params.containsKey(key))
+ {
+ coll = new ArrayList<String>();
+ m_params.put(key, coll);
+ }
+ else
+ {
+ coll = m_params.get(key);
+ }
+ coll.add(value);
+ }
+
+ public List<String> all(String key) throws NotFoundException
+ {
+ if(!m_params.containsKey(key))
+ throw new NotFoundException();
+ return m_params.get(key);
+ }
+
+ public List<String> allSafe(String key, List<String> def)
+ {
+ if(!m_params.containsKey(key) || m_params.get(key).size() < 1)
+ {
+ return def;
+ }
+ return m_params.get(key);
+ }
+
+ public List<String> allSafe(String key)
+ {
+ return allSafe(key, new ArrayList<String>());
+ }
+
+ public String first(String key) throws NotFoundException
+ {
+ List<String> list = all(key);
+ if(list.size() < 1)
+ {
+ throw new NotFoundException();
+ }
+ return list.get(0);
+ }
+
+ public String firstSafe(String key, String def)
+ {
+ if(!m_params.containsKey(key) || m_params.get(key).size() < 1)
+ {
+ return def;
+ }
+ return m_params.get(key).get(0);
+ }
+
+ public String firstSafe(String key)
+ {
+ return firstSafe(key, "");
+ }
+
+ private HashMap<String, List<String>> m_params = new HashMap<String, List<String>>();
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/ParseException.java b/meshmc/libraries/launcher/org/projecttick/ParseException.java
new file mode 100644
index 0000000000..5e3ca0df1a
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/ParseException.java
@@ -0,0 +1,64 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+public class ParseException extends java.lang.Exception
+{
+ public ParseException() { super(); }
+ public ParseException(String message) {
+ super(message);
+ }
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/Utils.java b/meshmc/libraries/launcher/org/projecttick/Utils.java
new file mode 100644
index 0000000000..0dab388eff
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/Utils.java
@@ -0,0 +1,158 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick;
+
+import java.io.*;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class Utils
+{
+ /**
+ * Combine two parts of a path.
+ *
+ * @param path1
+ * @param path2
+ * @return the paths, combined
+ */
+ public static String combine(String path1, String path2)
+ {
+ File file1 = new File(path1);
+ File file2 = new File(file1, path2);
+ return file2.getPath();
+ }
+
+ /**
+ * Join a list of strings into a string using a separator!
+ *
+ * @param strings the string list to join
+ * @param separator the glue
+ * @return the result.
+ */
+ public static String join(List<String> strings, String separator)
+ {
+ StringBuilder sb = new StringBuilder();
+ String sep = "";
+ for (String s : strings)
+ {
+ sb.append(sep).append(s);
+ sep = separator;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Finds a field that looks like a Minecraft base folder in a supplied class
+ *
+ * @param mc the class to scan
+ */
+ public static Field getMCPathField(Class<?> mc)
+ {
+ Field[] fields = mc.getDeclaredFields();
+
+ for (Field f : fields)
+ {
+ if (f.getType() != File.class)
+ {
+ // Has to be File
+ continue;
+ }
+ if (f.getModifiers() != (Modifier.PRIVATE + Modifier.STATIC))
+ {
+ // And Private Static.
+ continue;
+ }
+ return f;
+ }
+ return null;
+ }
+
+ /**
+ * Log to MeshMC console
+ *
+ * @param message A String containing the message
+ * @param level A String containing the level name. See MinecraftLauncher::getLevel()
+ */
+ public static void log(String message, String level)
+ {
+ // Kinda dirty
+ String tag = "!![" + level + "]!";
+ System.out.println(tag + message.replace("\n", "\n" + tag));
+ }
+
+ public static void log(String message)
+ {
+ log(message, "MeshMC");
+ }
+
+ public static void log()
+ {
+ System.out.println();
+ }
+}
+
diff --git a/meshmc/libraries/launcher/org/projecttick/modern/ModernLauncher.java b/meshmc/libraries/launcher/org/projecttick/modern/ModernLauncher.java
new file mode 100644
index 0000000000..41616c346b
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/modern/ModernLauncher.java
@@ -0,0 +1,309 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick.modern;
+
+import org.projecttick.MeshMC;
+import org.projecttick.ParamBucket;
+import org.projecttick.Utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Modern launcher implementation for Minecraft 1.0 and above.
+ *
+ * Unlike OneSixLauncher which loads the game in-process via reflection,
+ * ModernLauncher spawns Minecraft as a separate child process using the
+ * Java binary specified by MeshMC. This decouples the JVM version
+ * running MeshMC library from the JVM version required by the game,
+ * allowing Minecraft versions that require Java 21+ or newer (e.g., class
+ * file version 69.0 / Java 25) to be launched correctly even when the
+ * launcher itself runs on an older JVM.
+ *
+ * Parameters consumed from ParamBucket:
+ * javaPath - Path to the Java binary for the game process (required)
+ * cp - Classpath entries, one per entry (required)
+ * mainClass - Main class to invoke (default: net.minecraft.client.main.Main)
+ * param - Game arguments, one per entry
+ * jvmArg - JVM arguments to forward to the game process, one per arg
+ * natives - Path to the native libraries directory
+ * serverAddress - Server address for direct-connect on launch (optional)
+ * serverPort - Server port for direct-connect on launch (optional)
+ */
+public class ModernLauncher implements MeshMC
+{
+ @Override
+ public int launch(ParamBucket params)
+ {
+ try
+ {
+ return doLaunch(params);
+ }
+ catch (Exception e)
+ {
+ Utils.log("ModernLauncher encountered a fatal error: " + e.getMessage(), "Error");
+ e.printStackTrace();
+ return 1;
+ }
+ }
+
+ private int doLaunch(ParamBucket params) throws Exception
+ {
+ // --- Java binary ---
+ String javaPath = params.firstSafe("javaPath", "java");
+ Utils.log("Java binary: " + javaPath);
+
+ // --- Main class ---
+ String mainClass = params.firstSafe("mainClass", "net.minecraft.client.main.Main");
+ Utils.log("Main class: " + mainClass);
+
+ // --- Classpath ---
+ // "cp" = regular game libraries
+ // "ext" = native-classifier JARs (e.g. lwjgl-3.x-natives-linux.jar) that
+ // LWJGL 3's SharedLibraryLoader needs to find on the classpath.
+ // OneSixLauncher works without these because it runs in-process and
+ // MeshMC JVM already has them on its own classpath. For
+ // ModernLauncher we must include them explicitly.
+ List<String> cpEntries = params.allSafe("cp", Collections.<String>emptyList());
+ List<String> extEntries = params.allSafe("ext", Collections.<String>emptyList());
+ List<String> allCpEntries = new ArrayList<String>(cpEntries);
+ allCpEntries.addAll(extEntries);
+ if (allCpEntries.isEmpty())
+ {
+ Utils.log("No classpath entries provided to ModernLauncher.", "Error");
+ return 1;
+ }
+ String classpath = buildClassPath(allCpEntries);
+
+ // --- Native library path ---
+ String natives = params.firstSafe("natives", "");
+
+ // --- JVM arguments (Xmx, Xms, GC flags, platform-specific flags, etc.) ---
+ List<String> jvmArgs = params.allSafe("jvmArg", Collections.<String>emptyList());
+
+ // --- Game arguments ---
+ // param entries are already pre-expanded by the C++ launcher (auth tokens,
+ // game directory, asset index, resolution, etc.)
+ List<String> gameArgs = new ArrayList<String>(
+ params.allSafe("param", Collections.<String>emptyList())
+ );
+
+ // Direct-connect: server address / port are passed as separate keys and
+ // must be appended to game args manually (processMinecraftArgs skips them
+ // when a launch script is used).
+ String serverAddress = params.firstSafe("serverAddress", "");
+ String serverPort = params.firstSafe("serverPort", "");
+ if (serverAddress != null && !serverAddress.isEmpty())
+ {
+ gameArgs.add("--server");
+ gameArgs.add(serverAddress);
+ if (serverPort != null && !serverPort.isEmpty())
+ {
+ gameArgs.add("--port");
+ gameArgs.add(serverPort);
+ }
+ }
+
+ // --- Build the full command ---
+ List<String> command = buildCommand(javaPath, jvmArgs, natives, classpath, mainClass, gameArgs);
+
+ // Log the full command for diagnostics (visible in launcher console)
+ StringBuilder cmdLog = new StringBuilder("Spawning game process:\n CMD: ");
+ for (String s : command)
+ {
+ cmdLog.append(s).append(" ");
+ }
+ Utils.log(cmdLog.toString().trim());
+
+ ProcessBuilder pb = new ProcessBuilder(command);
+ // Let the game process inherit the working directory from us (set by LauncherPartLaunch)
+ pb.directory(null);
+
+ // Set JAVA_HOME so the game and any native launchers can locate Java correctly
+ File javaFile = new File(javaPath);
+ File javaParent = javaFile.getParentFile();
+ if (javaParent != null && javaParent.getParentFile() != null)
+ {
+ pb.environment().put("JAVA_HOME", javaParent.getParent());
+ }
+
+ final Process process = pb.start();
+
+ // Pipe stdout and stderr from the child process to our own streams so
+ // the C++ launcher's LoggedProcess can capture and display them.
+ Thread outPipe = pipeStream(process.getInputStream(), System.out, "stdout");
+ Thread errPipe = pipeStream(process.getErrorStream(), System.err, "stderr");
+ outPipe.start();
+ errPipe.start();
+
+ // Shutdown hook: ensure the game process is terminated if MeshMC is
+ // forcefully killed (e.g., SIGKILL from within MeshMC UI).
+ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if (process.isAlive())
+ {
+ process.destroyForcibly();
+ }
+ }
+ }, "ModernLauncher-ShutdownHook"));
+
+ // Block until the game process exits
+ int exitCode = process.waitFor();
+
+ outPipe.join();
+ errPipe.join();
+
+ if (exitCode != 0)
+ {
+ Utils.log("Game process exited with code " + exitCode, "Error");
+ }
+
+ return exitCode;
+ }
+
+ private String buildClassPath(List<String> entries)
+ {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < entries.size(); i++)
+ {
+ if (i > 0)
+ sb.append(File.pathSeparator);
+ sb.append(entries.get(i));
+ }
+ return sb.toString();
+ }
+
+ private List<String> buildCommand(
+ String javaPath,
+ List<String> jvmArgs,
+ String natives,
+ String classpath,
+ String mainClass,
+ List<String> gameArgs)
+ {
+ List<String> cmd = new ArrayList<String>();
+
+ // Java executable
+ cmd.add(javaPath);
+
+ // JVM arguments (memory settings, GC options, platform flags, etc.)
+ cmd.addAll(jvmArgs);
+
+ // Native library path - needed for LWJGL and other native dependencies.
+ // Pass both the standard JVM property and the LWJGL-specific property:
+ // java.library.path - used by java.lang.System.loadLibrary()
+ // org.lwjgl.librarypath - checked first by LWJGL 3.3+ before java.library.path
+ if (natives != null && !natives.isEmpty())
+ {
+ cmd.add("-Djava.library.path=" + natives);
+ cmd.add("-Dorg.lwjgl.librarypath=" + natives);
+ }
+
+ // Classpath
+ if (!classpath.isEmpty())
+ {
+ cmd.add("-cp");
+ cmd.add(classpath);
+ }
+
+ // Main class
+ cmd.add(mainClass);
+
+ // Game arguments
+ cmd.addAll(gameArgs);
+
+ return cmd;
+ }
+
+ /**
+ * Reads bytes from {@code src} and writes them to {@code dst} on a dedicated
+ * daemon thread so neither stream blocks the main thread.
+ */
+ private Thread pipeStream(final InputStream src, final PrintStream dst, final String name)
+ {
+ Thread t = new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ byte[] buffer = new byte[8192];
+ int read;
+ try
+ {
+ while ((read = src.read(buffer)) != -1)
+ {
+ dst.write(buffer, 0, read);
+ dst.flush();
+ }
+ }
+ catch (IOException e)
+ {
+ // Stream closed - expected when the child process exits
+ }
+ }
+ }, "ModernLauncher-" + name + "-pipe");
+ t.setDaemon(true);
+ return t;
+ }
+}
diff --git a/meshmc/libraries/launcher/org/projecttick/onesix/OneSixLauncher.java b/meshmc/libraries/launcher/org/projecttick/onesix/OneSixLauncher.java
new file mode 100644
index 0000000000..25936149f9
--- /dev/null
+++ b/meshmc/libraries/launcher/org/projecttick/onesix/OneSixLauncher.java
@@ -0,0 +1,288 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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.
+ *
+ * Linking this library statically or dynamically with other modules is
+ * making a combined work based on this library. Thus, the terms and
+ * conditions of the GNU General Public License cover the whole
+ * combination.
+ *
+ * As a special exception, the copyright holders of this library give
+ * you permission to link this library with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also meet,
+ * for each linked independent module, the terms and conditions of the
+ * license of that module. An independent module is a module which is
+ * not derived from or based on this library. If you modify this
+ * library, you may extend this exception to your version of the
+ * library, but you are not obliged to do so. If you do not wish to do
+ * so, delete this exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2012-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.projecttick.onesix;
+
+import org.projecttick.*;
+
+import java.applet.Applet;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class OneSixLauncher implements MeshMC
+{
+ // parameters, separated from ParamBucket
+ private List<String> libraries;
+ private List<String> mcparams;
+ private List<String> mods;
+ private List<String> jarmods;
+ private List<String> coremods;
+ private List<String> traits;
+ private String appletClass;
+ private String mainClass;
+ private String nativePath;
+ private String userName, sessionId;
+ private String windowTitle;
+ private String windowParams;
+
+ // secondary parameters
+ private int winSizeW;
+ private int winSizeH;
+ private boolean maximize;
+ private String cwd;
+
+ private String serverAddress;
+ private String serverPort;
+
+ // the much abused system classloader, for convenience (for further abuse)
+ private ClassLoader cl;
+
+ private void processParams(ParamBucket params) throws NotFoundException
+ {
+ libraries = params.all("cp");
+ mcparams = params.allSafe("param", new ArrayList<String>() );
+ mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft");
+ appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet");
+ traits = params.allSafe("traits", new ArrayList<String>());
+ nativePath = params.first("natives");
+
+ userName = params.first("userName");
+ sessionId = params.first("sessionId");
+ windowTitle = params.firstSafe("windowTitle", "Minecraft");
+ windowParams = params.firstSafe("windowParams", "854x480");
+
+ serverAddress = params.firstSafe("serverAddress", null);
+ serverPort = params.firstSafe("serverPort", null);
+
+ cwd = System.getProperty("user.dir");
+
+ winSizeW = 854;
+ winSizeH = 480;
+ maximize = false;
+
+ String[] dimStrings = windowParams.split("x");
+
+ if (windowParams.equalsIgnoreCase("max"))
+ {
+ maximize = true;
+ }
+ else if (dimStrings.length == 2)
+ {
+ try
+ {
+ winSizeW = Integer.parseInt(dimStrings[0]);
+ winSizeH = Integer.parseInt(dimStrings[1]);
+ } catch (NumberFormatException ignored) {}
+ }
+ }
+
+ int legacyLaunch()
+ {
+ // Get the Minecraft Class and set the base folder
+ Class<?> mc;
+ try
+ {
+ mc = cl.loadClass(mainClass);
+
+ Field f = Utils.getMCPathField(mc);
+
+ if (f == null)
+ {
+ System.err.println("Could not find Minecraft path field.");
+ }
+ else
+ {
+ f.setAccessible(true);
+ f.set(null, new File(cwd));
+ }
+ } catch (Exception e)
+ {
+ System.err.println("Could not set base folder. Failed to find/access Minecraft main class:");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+
+ System.setProperty("minecraft.applet.TargetDirectory", cwd);
+
+ if(!traits.contains("noapplet"))
+ {
+ Utils.log("Launching with applet wrapper...");
+ try
+ {
+ Class<?> MCAppletClass = cl.loadClass(appletClass);
+ Applet mcappl = (Applet) MCAppletClass.newInstance();
+ LegacyFrame mcWindow = new LegacyFrame(windowTitle);
+ mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize, serverAddress, serverPort);
+ return 0;
+ } catch (Exception e)
+ {
+ Utils.log("Applet wrapper failed:", "Error");
+ e.printStackTrace(System.err);
+ Utils.log();
+ Utils.log("Falling back to using main class.");
+ }
+ }
+
+ // init params for the main method to chomp on.
+ String[] paramsArray = mcparams.toArray(new String[mcparams.size()]);
+ try
+ {
+ mc.getMethod("main", String[].class).invoke(null, (Object) paramsArray);
+ return 0;
+ } catch (Exception e)
+ {
+ Utils.log("Failed to invoke the Minecraft main class:", "Fatal");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+ }
+
+ int launchWithMainClass()
+ {
+ // window size, title and state, onesix
+ if (maximize)
+ {
+ // FIXME: there is no good way to maximize the minecraft window in onesix.
+ // the following often breaks linux screen setups
+ // mcparams.add("--fullscreen");
+ }
+ else
+ {
+ mcparams.add("--width");
+ mcparams.add(Integer.toString(winSizeW));
+ mcparams.add("--height");
+ mcparams.add(Integer.toString(winSizeH));
+ }
+
+ if (serverAddress != null)
+ {
+ mcparams.add("--server");
+ mcparams.add(serverAddress);
+ mcparams.add("--port");
+ mcparams.add(serverPort);
+ }
+
+ // Get the Minecraft Class.
+ Class<?> mc;
+ try
+ {
+ mc = cl.loadClass(mainClass);
+ } catch (ClassNotFoundException e)
+ {
+ System.err.println("Failed to find Minecraft main class:");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+
+ // get the main method.
+ Method meth;
+ try
+ {
+ meth = mc.getMethod("main", String[].class);
+ } catch (NoSuchMethodException e)
+ {
+ System.err.println("Failed to acquire the main method:");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+
+ // init params for the main method to chomp on.
+ String[] paramsArray = mcparams.toArray(new String[mcparams.size()]);
+ try
+ {
+ // static method doesn't have an instance
+ meth.invoke(null, (Object) paramsArray);
+ } catch (Exception e)
+ {
+ System.err.println("Failed to start Minecraft:");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+ return 0;
+ }
+
+ @Override
+ public int launch(ParamBucket params)
+ {
+ // get and process the launch script params
+ try
+ {
+ processParams(params);
+ } catch (NotFoundException e)
+ {
+ System.err.println("Not enough arguments.");
+ e.printStackTrace(System.err);
+ return -1;
+ }
+
+ // grab the system classloader and ...
+ cl = ClassLoader.getSystemClassLoader();
+
+ if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch") )
+ {
+ // legacy launch uses the applet wrapper
+ return legacyLaunch();
+ }
+ else
+ {
+ // normal launch just calls main()
+ return launchWithMainClass();
+ }
+ }
+}
diff --git a/meshmc/libraries/libnbtplusplus b/meshmc/libraries/libnbtplusplus
new file mode 160000
+Subproject 1a0ffe372f4da8408c5d08a36013536a3396b9e
diff --git a/meshmc/libraries/optional-bare/CMakeLists.txt b/meshmc/libraries/optional-bare/CMakeLists.txt
new file mode 100644
index 0000000000..41621830d0
--- /dev/null
+++ b/meshmc/libraries/optional-bare/CMakeLists.txt
@@ -0,0 +1,5 @@
+cmake_minimum_required(VERSION 3.25)
+project(optional-bare)
+
+add_library(optional-bare INTERFACE)
+target_include_directories(optional-bare INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
diff --git a/meshmc/libraries/optional-bare/LICENSE.txt b/meshmc/libraries/optional-bare/LICENSE.txt
new file mode 100644
index 0000000000..36b7cd93cd
--- /dev/null
+++ b/meshmc/libraries/optional-bare/LICENSE.txt
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/meshmc/libraries/optional-bare/README.md b/meshmc/libraries/optional-bare/README.md
new file mode 100644
index 0000000000..e29ff7c14e
--- /dev/null
+++ b/meshmc/libraries/optional-bare/README.md
@@ -0,0 +1,5 @@
+# optional bare
+
+A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later.
+
+Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81
diff --git a/meshmc/libraries/optional-bare/include/nonstd/optional b/meshmc/libraries/optional-bare/include/nonstd/optional
new file mode 100644
index 0000000000..bcb06d571c
--- /dev/null
+++ b/meshmc/libraries/optional-bare/include/nonstd/optional
@@ -0,0 +1,508 @@
+// SPDX-License-Identifier: BSL-1.0
+// Copyright 2017-2019 by Martin Moene
+//
+// https://github.com/martinmoene/optional-bare
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef NONSTD_OPTIONAL_BARE_HPP
+#define NONSTD_OPTIONAL_BARE_HPP
+
+#define optional_bare_MAJOR 1
+#define optional_bare_MINOR 1
+#define optional_bare_PATCH 0
+
+#define optional_bare_VERSION optional_STRINGIFY(optional_bare_MAJOR) "." optional_STRINGIFY(optional_bare_MINOR) "." optional_STRINGIFY(optional_bare_PATCH)
+
+#define optional_STRINGIFY( x ) optional_STRINGIFY_( x )
+#define optional_STRINGIFY_( x ) #x
+
+// optional-bare configuration:
+
+#define optional_OPTIONAL_DEFAULT 0
+#define optional_OPTIONAL_NONSTD 1
+#define optional_OPTIONAL_STD 2
+
+#if !defined( optional_CONFIG_SELECT_OPTIONAL )
+# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD )
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef optional_CONFIG_NO_EXCEPTIONS
+# if _MSC_VER
+# include <cstddef> // for _HAS_EXCEPTIONS
+# endif
+# if _MSC_VER
+# include <cstddef> // for _HAS_EXCEPTIONS
+# endif
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
+# define optional_CONFIG_NO_EXCEPTIONS 0
+# else
+# define optional_CONFIG_NO_EXCEPTIONS 1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef optional_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+# define optional_CPLUSPLUS __cplusplus
+# endif
+#endif
+
+#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L )
+#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L )
+#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L )
+#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L )
+#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L )
+
+// C++ language version (represent 98 as 3):
+
+#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) )
+
+// Use C++17 std::optional if available and requested:
+
+#if optional_CPP17_OR_GREATER && defined(__has_include )
+# if __has_include( <optional> )
+# define optional_HAVE_STD_OPTIONAL 1
+# else
+# define optional_HAVE_STD_OPTIONAL 0
+# endif
+#else
+# define optional_HAVE_STD_OPTIONAL 0
+#endif
+
+#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) )
+
+//
+// Using std::optional:
+//
+
+#if optional_USES_STD_OPTIONAL
+
+#include <optional>
+#include <utility>
+
+namespace nonstd {
+
+ using std::in_place;
+ using std::in_place_type;
+ using std::in_place_index;
+ using std::in_place_t;
+ using std::in_place_type_t;
+ using std::in_place_index_t;
+
+ using std::optional;
+ using std::bad_optional_access;
+ using std::hash;
+
+ using std::nullopt;
+ using std::nullopt_t;
+
+ using std::operator==;
+ using std::operator!=;
+ using std::operator<;
+ using std::operator<=;
+ using std::operator>;
+ using std::operator>=;
+ using std::make_optional;
+ using std::swap;
+}
+
+#else // optional_USES_STD_OPTIONAL
+
+#include <cassert>
+
+#if ! optional_CONFIG_NO_EXCEPTIONS
+# include <stdexcept>
+#endif
+
+namespace nonstd { namespace optional_bare {
+
+// type for nullopt
+
+struct nullopt_t
+{
+ struct init{};
+ nullopt_t( init ) {}
+};
+
+// extra parenthesis to prevent the most vexing parse:
+
+const nullopt_t nullopt(( nullopt_t::init() ));
+
+// optional access error.
+
+#if ! optional_CONFIG_NO_EXCEPTIONS
+
+class bad_optional_access : public std::logic_error
+{
+public:
+ explicit bad_optional_access()
+ : logic_error( "bad optional access" ) {}
+};
+
+#endif // optional_CONFIG_NO_EXCEPTIONS
+
+// Simplistic optional: requires T to be default constructible, copyable.
+
+template< typename T >
+class optional
+{
+private:
+ typedef void (optional::*safe_bool)() const;
+
+public:
+ typedef T value_type;
+
+ optional()
+ : has_value_( false )
+ {}
+
+ optional( nullopt_t )
+ : has_value_( false )
+ {}
+
+ optional( T const & arg )
+ : has_value_( true )
+ , value_ ( arg )
+ {}
+
+ template< class U >
+ optional( optional<U> const & other )
+ : has_value_( other.has_value() )
+ , value_ ( other.value() )
+ {}
+
+ optional & operator=( nullopt_t )
+ {
+ reset();
+ return *this;
+ }
+
+ template< class U >
+ optional & operator=( optional<U> const & other )
+ {
+ has_value_ = other.has_value();
+ value_ = other.value();
+ return *this;
+ }
+
+ void swap( optional & rhs )
+ {
+ using std::swap;
+ if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); }
+ else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); }
+ else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); }
+ }
+
+ // observers
+
+ value_type const * operator->() const
+ {
+ return assert( has_value() ),
+ &value_;
+ }
+
+ value_type * operator->()
+ {
+ return assert( has_value() ),
+ &value_;
+ }
+
+ value_type const & operator*() const
+ {
+ return assert( has_value() ),
+ value_;
+ }
+
+ value_type & operator*()
+ {
+ return assert( has_value() ),
+ value_;
+ }
+
+#if optional_CPP11_OR_GREATER
+ explicit operator bool() const
+ {
+ return has_value();
+ }
+#else
+ operator safe_bool() const
+ {
+ return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
+ }
+#endif
+
+ bool has_value() const
+ {
+ return has_value_;
+ }
+
+ value_type const & value() const
+ {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert( has_value() );
+#else
+ if ( ! has_value() )
+ throw bad_optional_access();
+#endif
+ return value_;
+ }
+
+ value_type & value()
+ {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert( has_value() );
+#else
+ if ( ! has_value() )
+ throw bad_optional_access();
+#endif
+ return value_;
+ }
+
+ template< class U >
+ value_type value_or( U const & v ) const
+ {
+ return has_value() ? value() : static_cast<value_type>( v );
+ }
+
+ // modifiers
+
+ void reset()
+ {
+ has_value_ = false;
+ }
+
+private:
+ void this_type_does_not_support_comparisons() const {}
+
+ template< typename V >
+ void initialize( V const & value )
+ {
+ assert( ! has_value() );
+ value_ = value;
+ has_value_ = true;
+ }
+
+private:
+ bool has_value_;
+ value_type value_;
+};
+
+// Relational operators
+
+template< typename T, typename U >
+inline bool operator==( optional<T> const & x, optional<U> const & y )
+{
+ return bool(x) != bool(y) ? false : bool(x) == false ? true : *x == *y;
+}
+
+template< typename T, typename U >
+inline bool operator!=( optional<T> const & x, optional<U> const & y )
+{
+ return !(x == y);
+}
+
+template< typename T, typename U >
+inline bool operator<( optional<T> const & x, optional<U> const & y )
+{
+ return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template< typename T, typename U >
+inline bool operator>( optional<T> const & x, optional<U> const & y )
+{
+ return (y < x);
+}
+
+template< typename T, typename U >
+inline bool operator<=( optional<T> const & x, optional<U> const & y )
+{
+ return !(y < x);
+}
+
+template< typename T, typename U >
+inline bool operator>=( optional<T> const & x, optional<U> const & y )
+{
+ return !(x < y);
+}
+
+// Comparison with nullopt
+
+template< typename T >
+inline bool operator==( optional<T> const & x, nullopt_t )
+{
+ return (!x);
+}
+
+template< typename T >
+inline bool operator==( nullopt_t, optional<T> const & x )
+{
+ return (!x);
+}
+
+template< typename T >
+inline bool operator!=( optional<T> const & x, nullopt_t )
+{
+ return bool(x);
+}
+
+template< typename T >
+inline bool operator!=( nullopt_t, optional<T> const & x )
+{
+ return bool(x);
+}
+
+template< typename T >
+inline bool operator<( optional<T> const &, nullopt_t )
+{
+ return false;
+}
+
+template< typename T >
+inline bool operator<( nullopt_t, optional<T> const & x )
+{
+ return bool(x);
+}
+
+template< typename T >
+inline bool operator<=( optional<T> const & x, nullopt_t )
+{
+ return (!x);
+}
+
+template< typename T >
+inline bool operator<=( nullopt_t, optional<T> const & )
+{
+ return true;
+}
+
+template< typename T >
+inline bool operator>( optional<T> const & x, nullopt_t )
+{
+ return bool(x);
+}
+
+template< typename T >
+inline bool operator>( nullopt_t, optional<T> const & )
+{
+ return false;
+}
+
+template< typename T >
+inline bool operator>=( optional<T> const &, nullopt_t )
+{
+ return true;
+}
+
+template< typename T >
+inline bool operator>=( nullopt_t, optional<T> const & x )
+{
+ return (!x);
+}
+
+// Comparison with T
+
+template< typename T, typename U >
+inline bool operator==( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x == v : false;
+}
+
+template< typename T, typename U >
+inline bool operator==( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v == *x : false;
+}
+
+template< typename T, typename U >
+inline bool operator!=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x != v : true;
+}
+
+template< typename T, typename U >
+inline bool operator!=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v != *x : true;
+}
+
+template< typename T, typename U >
+inline bool operator<( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x < v : true;
+}
+
+template< typename T, typename U >
+inline bool operator<( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v < *x : false;
+}
+
+template< typename T, typename U >
+inline bool operator<=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x <= v : true;
+}
+
+template< typename T, typename U >
+inline bool operator<=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v <= *x : false;
+}
+
+template< typename T, typename U >
+inline bool operator>( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x > v : false;
+}
+
+template< typename T, typename U >
+inline bool operator>( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v > *x : true;
+}
+
+template< typename T, typename U >
+inline bool operator>=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x >= v : false;
+}
+
+template< typename T, typename U >
+inline bool operator>=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template< typename T >
+void swap( optional<T> & x, optional<T> & y )
+{
+ x.swap( y );
+}
+
+// Convenience function to create an optional.
+
+template< typename T >
+inline optional<T> make_optional( T const & v )
+{
+ return optional<T>( v );
+}
+
+} // namespace optional-bare
+
+using namespace optional_bare;
+
+} // namespace nonstd
+
+#endif // optional_USES_STD_OPTIONAL
+
+#endif // NONSTD_OPTIONAL_BARE_HPP
diff --git a/meshmc/libraries/rainbow/CMakeLists.txt b/meshmc/libraries/rainbow/CMakeLists.txt
new file mode 100644
index 0000000000..14e81b2346
--- /dev/null
+++ b/meshmc/libraries/rainbow/CMakeLists.txt
@@ -0,0 +1,22 @@
+cmake_minimum_required(VERSION 3.25)
+project(rainbow)
+
+find_package(Qt6Core REQUIRED QUIET)
+find_package(Qt6Gui REQUIRED QUIET)
+
+set(RAINBOW_SOURCES
+src/rainbow.cpp
+)
+
+add_definitions(-DRAINBOW_LIBRARY)
+add_library(MeshMC_rainbow SHARED ${RAINBOW_SOURCES})
+target_include_directories(MeshMC_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
+
+target_link_libraries(MeshMC_rainbow Qt6::Core Qt6::Gui)
+
+# Install it
+install(
+ TARGETS MeshMC_rainbow
+ RUNTIME DESTINATION ${LIBRARY_DEST_DIR}
+ LIBRARY DESTINATION ${LIBRARY_DEST_DIR}
+)
diff --git a/meshmc/libraries/rainbow/COPYING.LIB b/meshmc/libraries/rainbow/COPYING.LIB
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/meshmc/libraries/rainbow/COPYING.LIB
diff --git a/meshmc/libraries/rainbow/include/rainbow.h b/meshmc/libraries/rainbow/include/rainbow.h
new file mode 100644
index 0000000000..ab95d107c9
--- /dev/null
+++ b/meshmc/libraries/rainbow/include/rainbow.h
@@ -0,0 +1,167 @@
+/* SPDX-License-Identifier: LGPL-2.0-or-later
+ *
+ * This was part of the KDE project - see KGuiAddons
+ * Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
+ * Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
+ * Copyright (C) 2007 Thomas Zander <zander@kde.org>
+ * Copyright (C) 2007 Zack Rusin <zack@kde.org>
+ * Copyright (C) 2015 Petr Mrazek <peterix@gmail.com>
+ * Copyright (C) 2026 Project Tick
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include "rainbow_config.h"
+
+#include <QPainter>
+class QColor;
+
+/**
+ * A set of methods used to work with colors.
+ */
+namespace Rainbow
+{
+ /**
+ * Calculate the luma of a color. Luma is weighted sum of gamma-adjusted
+ * R'G'B' components of a color. The result is similar to qGray. The range
+ * is from 0.0 (black) to 1.0 (white).
+ *
+ * Rainbow::darken(), Rainbow::lighten() and Rainbow::shade()
+ * operate on the luma of a color.
+ *
+ * @see http://en.wikipedia.org/wiki/Luma_(video)
+ */
+ RAINBOW_EXPORT qreal luma(const QColor&);
+
+ /**
+ * Calculate hue, chroma and luma of a color in one call.
+ * @since 5.0
+ */
+ RAINBOW_EXPORT void getHcy(const QColor&, qreal* hue, qreal* chroma,
+ qreal* luma, qreal* alpha = 0);
+
+ /**
+ * Calculate the contrast ratio between two colors, according to the
+ * W3C/WCAG2.0 algorithm, (Lmax + 0.05)/(Lmin + 0.05), where Lmax and Lmin
+ * are the luma values of the lighter color and the darker color,
+ * respectively.
+ *
+ * A contrast ration of 5:1 (result == 5.0) is the minimum for "normal"
+ * text to be considered readable (large text can go as low as 3:1). The
+ * ratio ranges from 1:1 (result == 1.0) to 21:1 (result == 21.0).
+ *
+ * @see Rainbow::luma
+ */
+ RAINBOW_EXPORT qreal contrastRatio(const QColor&, const QColor&);
+
+ /**
+ * Adjust the luma of a color by changing its distance from white.
+ *
+ * @li amount == 1.0 gives white
+ * @li amount == 0.5 results in a color whose luma is halfway between 1.0
+ * and that of the original color
+ * @li amount == 0.0 gives the original color
+ * @li amount == -1.0 gives a color that is 'twice as far from white' as
+ * the original color, that is luma(result) == 1.0 - 2*(1.0 - luma(color))
+ *
+ * @param amount factor by which to adjust the luma component of the color
+ * @param chromaInverseGain (optional) factor by which to adjust the chroma
+ * component of the color; 1.0 means no change, 0.0 maximizes chroma
+ * @see Rainbow::shade
+ */
+ RAINBOW_EXPORT QColor lighten(const QColor&, qreal amount = 0.5,
+ qreal chromaInverseGain = 1.0);
+
+ /**
+ * Adjust the luma of a color by changing its distance from black.
+ *
+ * @li amount == 1.0 gives black
+ * @li amount == 0.5 results in a color whose luma is halfway between 0.0
+ * and that of the original color
+ * @li amount == 0.0 gives the original color
+ * @li amount == -1.0 gives a color that is 'twice as far from black' as
+ * the original color, that is luma(result) == 2*luma(color)
+ *
+ * @param amount factor by which to adjust the luma component of the color
+ * @param chromaGain (optional) factor by which to adjust the chroma
+ * component of the color; 1.0 means no change, 0.0 minimizes chroma
+ * @see Rainbow::shade
+ */
+ RAINBOW_EXPORT QColor darken(const QColor&, qreal amount = 0.5,
+ qreal chromaGain = 1.0);
+
+ /**
+ * Adjust the luma and chroma components of a color. The amount is added
+ * to the corresponding component.
+ *
+ * @param lumaAmount amount by which to adjust the luma component of the
+ * color; 0.0 results in no change, -1.0 turns anything black, 1.0 turns
+ * anything white
+ * @param chromaAmount (optional) amount by which to adjust the chroma
+ * component of the color; 0.0 results in no change, -1.0 minimizes chroma,
+ * 1.0 maximizes chroma
+ * @see Rainbow::luma
+ */
+ RAINBOW_EXPORT QColor shade(const QColor&, qreal lumaAmount,
+ qreal chromaAmount = 0.0);
+
+ /**
+ * Create a new color by tinting one color with another. This function is
+ * meant for creating additional colors withings the same class (background,
+ * foreground) from colors in a different class. Therefore when @p amount
+ * is low, the luma of @p base is mostly preserved, while the hue and
+ * chroma of @p color is mostly inherited.
+ *
+ * @param base color to be tinted
+ * @param color color with which to tint
+ * @param amount how strongly to tint the base; 0.0 gives @p base,
+ * 1.0 gives @p color
+ */
+ RAINBOW_EXPORT QColor tint(const QColor& base, const QColor& color,
+ qreal amount = 0.3);
+
+ /**
+ * Blend two colors into a new color by linear combination.
+ * @code
+ QColor lighter = Rainbow::mix(myColor, Qt::white)
+ * @endcode
+ * @param c1 first color.
+ * @param c2 second color.
+ * @param bias weight to be used for the mix. @p bias <= 0 gives @p c1,
+ * @p bias >= 1 gives @p c2. @p bias == 0.5 gives a 50% blend of @p c1
+ * and @p c2.
+ */
+ RAINBOW_EXPORT QColor mix(const QColor& c1, const QColor& c2,
+ qreal bias = 0.5);
+
+ /**
+ * Blend two colors into a new color by painting the second color over the
+ * first using the specified composition mode.
+ * @code
+ QColor white(Qt::white);
+ white.setAlphaF(0.5);
+ QColor lighter = Rainbow::overlayColors(myColor, white);
+ @endcode
+ * @param base the base color (alpha channel is ignored).
+ * @param paint the color to be overlayed onto the base color.
+ * @param comp the CompositionMode used to do the blending.
+ */
+ RAINBOW_EXPORT QColor overlayColors(
+ const QColor& base, const QColor& paint,
+ QPainter::CompositionMode comp = QPainter::CompositionMode_SourceOver);
+} // namespace Rainbow
diff --git a/meshmc/libraries/rainbow/include/rainbow_config.h b/meshmc/libraries/rainbow/include/rainbow_config.h
new file mode 100644
index 0000000000..e5f8860dc9
--- /dev/null
+++ b/meshmc/libraries/rainbow/include/rainbow_config.h
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QtCore/QtGlobal>
+
+#ifdef RAINBOW_STATIC
+#define RAINBOW_EXPORT
+#else
+#ifdef RAINBOW_LIBRARY
+#define RAINBOW_EXPORT Q_DECL_EXPORT
+#else
+#define RAINBOW_EXPORT Q_DECL_IMPORT
+#endif
+#endif \ No newline at end of file
diff --git a/meshmc/libraries/rainbow/src/rainbow.cpp b/meshmc/libraries/rainbow/src/rainbow.cpp
new file mode 100644
index 0000000000..ebe65b3a80
--- /dev/null
+++ b/meshmc/libraries/rainbow/src/rainbow.cpp
@@ -0,0 +1,320 @@
+/* SPDX-License-Identifier: LGPL-2.0-or-later
+ *
+ * This was part of the KDE project - see KGuiAddons
+ * Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
+ * Copyright (C) 2007 Olaf Schmidt <ojschmidt@kde.org>
+ * Copyright (C) 2007 Thomas Zander <zander@kde.org>
+ * Copyright (C) 2007 Zack Rusin <zack@kde.org>
+ * Copyright (C) 2015 Petr Mrazek <peterix@gmail.com>
+ * Copyright (C) 2026 Project Tick
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "../include/rainbow.h"
+
+#include <QColor>
+#include <QImage>
+#include <QtNumeric> // qIsNaN
+
+#include <math.h>
+
+// BEGIN internal helper functions
+
+static inline qreal wrap(qreal a, qreal d = 1.0)
+{
+ qreal r = fmod(a, d);
+ return (r < 0.0 ? d + r : (r > 0.0 ? r : 0.0));
+}
+
+// normalize: like qBound(a, 0.0, 1.0) but without needing the args and with
+// "safer" behavior on NaN (isnan(a) -> return 0.0)
+static inline qreal normalize(qreal a)
+{
+ return (a < 1.0 ? (a > 0.0 ? a : 0.0) : 1.0);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// HCY color space
+
+#define HCY_REC 709 // use 709 for now
+#if HCY_REC == 601
+static const qreal yc[3] = {0.299, 0.587, 0.114};
+#elif HCY_REC == 709
+static const qreal yc[3] = {0.2126, 0.7152, 0.0722};
+#else // use Qt values
+static const qreal yc[3] = {0.34375, 0.5, 0.15625};
+#endif
+
+class KHCY
+{
+ public:
+ explicit KHCY(const QColor& color)
+ {
+ qreal r = gamma(color.redF());
+ qreal g = gamma(color.greenF());
+ qreal b = gamma(color.blueF());
+ a = color.alphaF();
+
+ // luma component
+ y = lumag(r, g, b);
+
+ // hue component
+ qreal p = qMax(qMax(r, g), b);
+ qreal n = qMin(qMin(r, g), b);
+ qreal d = 6.0 * (p - n);
+ if (n == p) {
+ h = 0.0;
+ } else if (r == p) {
+ h = ((g - b) / d);
+ } else if (g == p) {
+ h = ((b - r) / d) + (1.0 / 3.0);
+ } else {
+ h = ((r - g) / d) + (2.0 / 3.0);
+ }
+
+ // chroma component
+ if (r == g && g == b) {
+ c = 0.0;
+ } else {
+ c = qMax((y - n) / y, (p - y) / (1 - y));
+ }
+ }
+ explicit KHCY(qreal h_, qreal c_, qreal y_, qreal a_ = 1.0)
+ {
+ h = h_;
+ c = c_;
+ y = y_;
+ a = a_;
+ }
+
+ QColor qColor() const
+ {
+ // start with sane component values
+ qreal _h = wrap(h);
+ qreal _c = normalize(c);
+ qreal _y = normalize(y);
+
+ // calculate some needed variables
+ qreal _hs = _h * 6.0, th, tm;
+ if (_hs < 1.0) {
+ th = _hs;
+ tm = yc[0] + yc[1] * th;
+ } else if (_hs < 2.0) {
+ th = 2.0 - _hs;
+ tm = yc[1] + yc[0] * th;
+ } else if (_hs < 3.0) {
+ th = _hs - 2.0;
+ tm = yc[1] + yc[2] * th;
+ } else if (_hs < 4.0) {
+ th = 4.0 - _hs;
+ tm = yc[2] + yc[1] * th;
+ } else if (_hs < 5.0) {
+ th = _hs - 4.0;
+ tm = yc[2] + yc[0] * th;
+ } else {
+ th = 6.0 - _hs;
+ tm = yc[0] + yc[2] * th;
+ }
+
+ // calculate RGB channels in sorted order
+ qreal tn, to, tp;
+ if (tm >= _y) {
+ tp = _y + _y * _c * (1.0 - tm) / tm;
+ to = _y + _y * _c * (th - tm) / tm;
+ tn = _y - (_y * _c);
+ } else {
+ tp = _y + (1.0 - _y) * _c;
+ to = _y + (1.0 - _y) * _c * (th - tm) / (1.0 - tm);
+ tn = _y - (1.0 - _y) * _c * tm / (1.0 - tm);
+ }
+
+ // return RGB channels in appropriate order
+ if (_hs < 1.0) {
+ return QColor::fromRgbF(igamma(tp), igamma(to), igamma(tn), a);
+ } else if (_hs < 2.0) {
+ return QColor::fromRgbF(igamma(to), igamma(tp), igamma(tn), a);
+ } else if (_hs < 3.0) {
+ return QColor::fromRgbF(igamma(tn), igamma(tp), igamma(to), a);
+ } else if (_hs < 4.0) {
+ return QColor::fromRgbF(igamma(tn), igamma(to), igamma(tp), a);
+ } else if (_hs < 5.0) {
+ return QColor::fromRgbF(igamma(to), igamma(tn), igamma(tp), a);
+ } else {
+ return QColor::fromRgbF(igamma(tp), igamma(tn), igamma(to), a);
+ }
+ }
+
+ qreal h, c, y, a;
+ static qreal luma(const QColor& color)
+ {
+ return lumag(gamma(color.redF()), gamma(color.greenF()),
+ gamma(color.blueF()));
+ }
+
+ private:
+ static qreal gamma(qreal n)
+ {
+ return pow(normalize(n), 2.2);
+ }
+ static qreal igamma(qreal n)
+ {
+ return pow(normalize(n), 1.0 / 2.2);
+ }
+ static qreal lumag(qreal r, qreal g, qreal b)
+ {
+ return r * yc[0] + g * yc[1] + b * yc[2];
+ }
+};
+
+static inline qreal mixQreal(qreal a, qreal b, qreal bias)
+{
+ return a + (b - a) * bias;
+}
+// END internal helper functions
+
+qreal Rainbow::luma(const QColor& color)
+{
+ return KHCY::luma(color);
+}
+
+void Rainbow::getHcy(const QColor& color, qreal* h, qreal* c, qreal* y,
+ qreal* a)
+{
+ if (!c || !h || !y) {
+ return;
+ }
+ KHCY khcy(color);
+ *c = khcy.c;
+ *h = khcy.h;
+ *y = khcy.y;
+ if (a) {
+ *a = khcy.a;
+ }
+}
+
+static qreal contrastRatioForLuma(qreal y1, qreal y2)
+{
+ if (y1 > y2) {
+ return (y1 + 0.05) / (y2 + 0.05);
+ } else {
+ return (y2 + 0.05) / (y1 + 0.05);
+ }
+}
+
+qreal Rainbow::contrastRatio(const QColor& c1, const QColor& c2)
+{
+ return contrastRatioForLuma(luma(c1), luma(c2));
+}
+
+QColor Rainbow::lighten(const QColor& color, qreal ky, qreal kc)
+{
+ KHCY c(color);
+ c.y = 1.0 - normalize((1.0 - c.y) * (1.0 - ky));
+ c.c = 1.0 - normalize((1.0 - c.c) * kc);
+ return c.qColor();
+}
+
+QColor Rainbow::darken(const QColor& color, qreal ky, qreal kc)
+{
+ KHCY c(color);
+ c.y = normalize(c.y * (1.0 - ky));
+ c.c = normalize(c.c * kc);
+ return c.qColor();
+}
+
+QColor Rainbow::shade(const QColor& color, qreal ky, qreal kc)
+{
+ KHCY c(color);
+ c.y = normalize(c.y + ky);
+ c.c = normalize(c.c + kc);
+ return c.qColor();
+}
+
+static QColor tintHelper(const QColor& base, qreal baseLuma,
+ const QColor& color, qreal amount)
+{
+ KHCY result(Rainbow::mix(base, color, pow(amount, 0.3)));
+ result.y = mixQreal(baseLuma, result.y, amount);
+
+ return result.qColor();
+}
+
+QColor Rainbow::tint(const QColor& base, const QColor& color, qreal amount)
+{
+ if (amount <= 0.0) {
+ return base;
+ }
+ if (amount >= 1.0) {
+ return color;
+ }
+ if (qIsNaN(amount)) {
+ return base;
+ }
+
+ qreal baseLuma = luma(base); // cache value because luma call is expensive
+ double ri = contrastRatioForLuma(baseLuma, luma(color));
+ double rg = 1.0 + ((ri + 1.0) * amount * amount * amount);
+ double u = 1.0, l = 0.0;
+ QColor result;
+ for (int i = 12; i; --i) {
+ double a = 0.5 * (l + u);
+ result = tintHelper(base, baseLuma, color, a);
+ double ra = contrastRatioForLuma(baseLuma, luma(result));
+ if (ra > rg) {
+ u = a;
+ } else {
+ l = a;
+ }
+ }
+ return result;
+}
+
+QColor Rainbow::mix(const QColor& c1, const QColor& c2, qreal bias)
+{
+ if (bias <= 0.0) {
+ return c1;
+ }
+ if (bias >= 1.0) {
+ return c2;
+ }
+ if (qIsNaN(bias)) {
+ return c1;
+ }
+
+ qreal r = mixQreal(c1.redF(), c2.redF(), bias);
+ qreal g = mixQreal(c1.greenF(), c2.greenF(), bias);
+ qreal b = mixQreal(c1.blueF(), c2.blueF(), bias);
+ qreal a = mixQreal(c1.alphaF(), c2.alphaF(), bias);
+
+ return QColor::fromRgbF(r, g, b, a);
+}
+
+QColor Rainbow::overlayColors(const QColor& base, const QColor& paint,
+ QPainter::CompositionMode comp)
+{
+ // This isn't the fastest way, but should be "fast enough".
+ // It's also the only safe way to use QPainter::CompositionMode
+ QImage img(1, 1, QImage::Format_ARGB32_Premultiplied);
+ QPainter p(&img);
+ QColor start = base;
+ start.setAlpha(255); // opaque
+ p.fillRect(0, 0, 1, 1, start);
+ p.setCompositionMode(comp);
+ p.fillRect(0, 0, 1, 1, paint);
+ p.end();
+ return img.pixel(0, 0);
+}
diff --git a/meshmc/libraries/systeminfo/CMakeLists.txt b/meshmc/libraries/systeminfo/CMakeLists.txt
new file mode 100644
index 0000000000..774f3357de
--- /dev/null
+++ b/meshmc/libraries/systeminfo/CMakeLists.txt
@@ -0,0 +1,29 @@
+project(systeminfo)
+
+find_package(Qt6Core)
+
+set(systeminfo_SOURCES
+include/sys.h
+include/distroutils.h
+src/distroutils.cpp
+)
+
+if (WIN32)
+ list(APPEND systeminfo_SOURCES src/sys_win32.cpp)
+elseif (UNIX)
+ if(APPLE)
+ list(APPEND systeminfo_SOURCES src/sys_apple.cpp)
+ else()
+ list(APPEND systeminfo_SOURCES src/sys_unix.cpp)
+ endif()
+endif()
+
+add_library(systeminfo STATIC ${systeminfo_SOURCES})
+target_link_libraries(systeminfo Qt6::Core Qt6::Gui Qt6::Network)
+target_include_directories(systeminfo PUBLIC include)
+
+include (UnitTest)
+add_unit_test(sys
+ SOURCES src/sys_test.cpp
+ LIBS systeminfo
+)
diff --git a/meshmc/libraries/systeminfo/include/distroutils.h b/meshmc/libraries/systeminfo/include/distroutils.h
new file mode 100644
index 0000000000..68df961612
--- /dev/null
+++ b/meshmc/libraries/systeminfo/include/distroutils.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "sys.h"
+#include <QString>
+
+namespace Sys
+{
+ struct LsbInfo {
+ QString distributor;
+ QString version;
+ QString description;
+ QString codename;
+ };
+
+ bool main_lsb_info(LsbInfo& out);
+ bool fallback_lsb_info(Sys::LsbInfo& out);
+ void lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out);
+ Sys::DistributionInfo read_lsb_release();
+
+ QString _extract_distribution(const QString& x);
+ QString _extract_version(const QString& x);
+ Sys::DistributionInfo read_legacy_release();
+
+ Sys::DistributionInfo read_os_release();
+} // namespace Sys
diff --git a/meshmc/libraries/systeminfo/include/sys.h b/meshmc/libraries/systeminfo/include/sys.h
new file mode 100644
index 0000000000..a539e8957c
--- /dev/null
+++ b/meshmc/libraries/systeminfo/include/sys.h
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+
+namespace Sys
+{
+ const uint64_t mebibyte = 1024ull * 1024ull;
+
+ enum class KernelType { Undetermined, Windows, Darwin, Linux };
+
+ struct KernelInfo {
+ QString kernelName;
+ QString kernelVersion;
+
+ KernelType kernelType = KernelType::Undetermined;
+ int kernelMajor = 0;
+ int kernelMinor = 0;
+ int kernelPatch = 0;
+ bool isCursed = false;
+ };
+
+ KernelInfo getKernelInfo();
+
+ struct DistributionInfo {
+ DistributionInfo operator+(const DistributionInfo& rhs) const
+ {
+ DistributionInfo out;
+ if (!distributionName.isEmpty()) {
+ out.distributionName = distributionName;
+ } else {
+ out.distributionName = rhs.distributionName;
+ }
+ if (!distributionVersion.isEmpty()) {
+ out.distributionVersion = distributionVersion;
+ } else {
+ out.distributionVersion = rhs.distributionVersion;
+ }
+ return out;
+ }
+ QString distributionName;
+ QString distributionVersion;
+ };
+
+ DistributionInfo getDistributionInfo();
+
+ uint64_t getSystemRam();
+
+ bool isSystem64bit();
+
+ bool isCPU64bit();
+} // namespace Sys
diff --git a/meshmc/libraries/systeminfo/src/distroutils.cpp b/meshmc/libraries/systeminfo/src/distroutils.cpp
new file mode 100644
index 0000000000..3735874e7b
--- /dev/null
+++ b/meshmc/libraries/systeminfo/src/distroutils.cpp
@@ -0,0 +1,267 @@
+/*
+
+ SPDX-FileCopyrightText: 2026 Project Tick
+ SPDX-FileContributor: Project Tick
+ SPDX-License-Identifier: GPL-3.0-or-later
+
+Code has been taken from https://github.com/natefoo/lionshead and loosely
+translated to C++ laced with Qt.
+
+ MeshMC - A Custom Launcher for Minecraft
+ Copyright (C) 2026 Project Tick
+
+ 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.
+
+ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+ This file incorporates work covered by the following copyright and
+ permission notice:
+
+MIT License
+
+Copyright (c) 2017 Nate Coraor
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+#include "distroutils.h"
+
+#include <QStringList>
+#include <QMap>
+#include <QSettings>
+#include <QFile>
+#include <QProcess>
+#include <QDebug>
+#include <QDir>
+#include <QRegularExpression>
+
+#include <functional>
+
+Sys::DistributionInfo Sys::read_os_release()
+{
+ Sys::DistributionInfo out;
+ QStringList files = {"/etc/os-release", "/usr/lib/os-release"};
+ QString name;
+ QString version;
+ for (auto& file : files) {
+ if (!QFile::exists(file)) {
+ continue;
+ }
+ QSettings settings(file, QSettings::IniFormat);
+ if (settings.contains("ID")) {
+ name = settings.value("ID").toString().toLower();
+ } else if (settings.contains("NAME")) {
+ name = settings.value("NAME").toString().toLower();
+ } else {
+ continue;
+ }
+
+ if (settings.contains("VERSION_ID")) {
+ version = settings.value("VERSION_ID").toString().toLower();
+ } else if (settings.contains("VERSION")) {
+ version = settings.value("VERSION").toString().toLower();
+ }
+ break;
+ }
+ if (name.isEmpty()) {
+ return out;
+ }
+ out.distributionName = name;
+ out.distributionVersion = version;
+ return out;
+}
+
+bool Sys::main_lsb_info(Sys::LsbInfo& out)
+{
+ int status = 0;
+ QProcess lsbProcess;
+ lsbProcess.start("lsb_release -a");
+ lsbProcess.waitForFinished();
+ status = lsbProcess.exitStatus();
+ QString output = lsbProcess.readAllStandardOutput();
+ qDebug() << output;
+ lsbProcess.close();
+ if (status == 0) {
+ auto lines = output.split('\n');
+ for (auto line : lines) {
+ int index = line.indexOf(':');
+ auto key = line.left(index).trimmed();
+ auto value = line.mid(index + 1).toLower().trimmed();
+ if (key == "Distributor ID")
+ out.distributor = value;
+ else if (key == "Release")
+ out.version = value;
+ else if (key == "Description")
+ out.description = value;
+ else if (key == "Codename")
+ out.codename = value;
+ }
+ return !out.distributor.isEmpty();
+ }
+ return false;
+}
+
+bool Sys::fallback_lsb_info(Sys::LsbInfo& out)
+{
+ // running lsb_release failed, try to read the file instead
+ // /etc/lsb-release format, if the file even exists, is non-standard.
+ // Only the `lsb_release` command is specified by LSB. Nonetheless, some
+ // distributions install an /etc/lsb-release as part of the base
+ // distribution, but `lsb_release` remains optional.
+ QString file = "/etc/lsb-release";
+ if (QFile::exists(file)) {
+ QSettings settings(file, QSettings::IniFormat);
+ if (settings.contains("DISTRIB_ID")) {
+ out.distributor = settings.value("DISTRIB_ID").toString().toLower();
+ }
+ if (settings.contains("DISTRIB_RELEASE")) {
+ out.version =
+ settings.value("DISTRIB_RELEASE").toString().toLower();
+ }
+ return !out.distributor.isEmpty();
+ }
+ return false;
+}
+
+void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out)
+{
+ QString dist = lsb.distributor;
+ QString vers = lsb.version;
+ if (dist.startsWith("redhatenterprise")) {
+ dist = "rhel";
+ } else if (dist == "archlinux") {
+ dist = "arch";
+ } else if (dist.startsWith("suse")) {
+ if (lsb.description.startsWith("opensuse")) {
+ dist = "opensuse";
+ } else if (lsb.description.startsWith("suse linux enterprise")) {
+ dist = "sles";
+ }
+ } else if (dist == "debian" and vers == "testing") {
+ vers = lsb.codename;
+ } else {
+ // ubuntu, debian, gentoo, scientific, slackware, ... ?
+ auto parts = dist.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
+ if (parts.size()) {
+ dist = parts[0];
+ }
+ }
+ if (!dist.isEmpty()) {
+ out.distributionName = dist;
+ out.distributionVersion = vers;
+ }
+}
+
+Sys::DistributionInfo Sys::read_lsb_release()
+{
+ LsbInfo lsb;
+ if (!main_lsb_info(lsb)) {
+ if (!fallback_lsb_info(lsb)) {
+ return Sys::DistributionInfo();
+ }
+ }
+ Sys::DistributionInfo out;
+ lsb_postprocess(lsb, out);
+ return out;
+}
+
+QString Sys::_extract_distribution(const QString& x)
+{
+ QString release = x.toLower();
+ if (release.startsWith("red hat enterprise")) {
+ return "rhel";
+ }
+ if (release.startsWith("suse linux enterprise")) {
+ return "sles";
+ }
+ QStringList list =
+ release.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
+ if (list.size()) {
+ return list[0];
+ }
+ return QString();
+}
+
+QString Sys::_extract_version(const QString& x)
+{
+ QRegularExpression versionish_string("\\d+(?:\\.\\d+)*$");
+ QStringList list = x.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
+ for (int i = list.size() - 1; i >= 0; --i) {
+ QString chunk = list[i];
+ if (versionish_string.match(chunk).hasMatch()) {
+ return chunk;
+ }
+ }
+ return QString();
+}
+
+Sys::DistributionInfo Sys::read_legacy_release()
+{
+ struct checkEntry {
+ QString file;
+ std::function<QString(const QString&)> extract_distro;
+ std::function<QString(const QString&)> extract_version;
+ };
+ QList<checkEntry> checks = {
+ {"/etc/arch-release", [](const QString&) { return "arch"; },
+ [](const QString&) { return "rolling"; }},
+ {"/etc/slackware-version", &Sys::_extract_distribution,
+ &Sys::_extract_version},
+ {QString(), &Sys::_extract_distribution, &Sys::_extract_version},
+ {"/etc/debian_version", [](const QString&) { return "debian"; },
+ [](const QString& x) { return x; }},
+ };
+ for (auto& check : checks) {
+ QStringList files;
+ if (check.file.isNull()) {
+ QDir etcDir("/etc");
+ etcDir.setNameFilters({"*-release"});
+ etcDir.setFilter(QDir::Files | QDir::NoDot | QDir::NoDotDot |
+ QDir::Readable | QDir::Hidden);
+ files = etcDir.entryList();
+ } else {
+ files.append(check.file);
+ }
+ for (auto file : files) {
+ QFile relfile(file);
+ if (!relfile.open(QIODevice::ReadOnly | QIODevice::Text))
+ continue;
+ QString contents = QString::fromUtf8(relfile.readLine()).trimmed();
+ QString dist = check.extract_distro(contents);
+ QString vers = check.extract_version(contents);
+ if (!dist.isEmpty()) {
+ Sys::DistributionInfo out;
+ out.distributionName = dist;
+ out.distributionVersion = vers;
+ return out;
+ }
+ }
+ }
+ return Sys::DistributionInfo();
+}
diff --git a/meshmc/libraries/systeminfo/src/sys_apple.cpp b/meshmc/libraries/systeminfo/src/sys_apple.cpp
new file mode 100644
index 0000000000..a6e44f3c08
--- /dev/null
+++ b/meshmc/libraries/systeminfo/src/sys_apple.cpp
@@ -0,0 +1,93 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "sys.h"
+
+#include <sys/utsname.h>
+
+#include <QString>
+#include <QStringList>
+#include <QDebug>
+
+Sys::KernelInfo Sys::getKernelInfo()
+{
+ Sys::KernelInfo out;
+ struct utsname buf;
+ uname(&buf);
+ out.kernelType = KernelType::Darwin;
+ out.kernelName = buf.sysname;
+ QString release = out.kernelVersion = buf.release;
+
+ // TODO: figure out how to detect cursed-ness (macOS emulated on linux via
+ // mad hacks and so on)
+ out.isCursed = false;
+
+ out.kernelMajor = 0;
+ out.kernelMinor = 0;
+ out.kernelPatch = 0;
+ auto sections = release.split('-');
+ if (sections.size() >= 1) {
+ auto versionParts = sections[0].split('.');
+ if (versionParts.size() >= 3) {
+ out.kernelMajor = versionParts[0].toInt();
+ out.kernelMinor = versionParts[1].toInt();
+ out.kernelPatch = versionParts[2].toInt();
+ } else {
+ qWarning() << "Not enough version numbers in " << sections[0]
+ << " found " << versionParts.size();
+ }
+ } else {
+ qWarning() << "Not enough '-' sections in " << release << " found "
+ << sections.size();
+ }
+ return out;
+}
+
+#include <sys/sysctl.h>
+
+uint64_t Sys::getSystemRam()
+{
+ uint64_t memsize;
+ size_t memsizesize = sizeof(memsize);
+ if (!sysctlbyname("hw.memsize", &memsize, &memsizesize, NULL, 0)) {
+ return memsize;
+ } else {
+ return 0;
+ }
+}
+
+bool Sys::isCPU64bit()
+{
+ // not even going to pretend I'm going to support anything else
+ return true;
+}
+
+bool Sys::isSystem64bit()
+{
+ // yep. maybe when we have 128bit CPUs on consumer devices.
+ return true;
+}
+
+Sys::DistributionInfo Sys::getDistributionInfo()
+{
+ DistributionInfo result;
+ return result;
+}
diff --git a/meshmc/libraries/systeminfo/src/sys_test.cpp b/meshmc/libraries/systeminfo/src/sys_test.cpp
new file mode 100644
index 0000000000..a4c90321b3
--- /dev/null
+++ b/meshmc/libraries/systeminfo/src/sys_test.cpp
@@ -0,0 +1,52 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include <sys.h>
+
+class SysTest : public QObject
+{
+ Q_OBJECT
+ private slots:
+
+ void test_kernelNotNull()
+ {
+ auto kinfo = Sys::getKernelInfo();
+ QVERIFY(!kinfo.kernelName.isEmpty());
+ QVERIFY(kinfo.kernelVersion != "0.0");
+ }
+ /*
+ void test_systemDistroNotNull()
+ {
+ auto kinfo = Sys::getDistributionInfo();
+ QVERIFY(!kinfo.distributionName.isEmpty());
+ QVERIFY(!kinfo.distributionVersion.isEmpty());
+ qDebug() << "Distro: " << kinfo.distributionName << "version" <<
+ kinfo.distributionVersion;
+ }
+ */
+};
+
+QTEST_GUILESS_MAIN(SysTest)
+
+#include "sys_test.moc"
diff --git a/meshmc/libraries/systeminfo/src/sys_unix.cpp b/meshmc/libraries/systeminfo/src/sys_unix.cpp
new file mode 100644
index 0000000000..6d081678cf
--- /dev/null
+++ b/meshmc/libraries/systeminfo/src/sys_unix.cpp
@@ -0,0 +1,128 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "sys.h"
+
+#include "distroutils.h"
+
+#include <sys/utsname.h>
+#include <fstream>
+#include <limits>
+
+#include <QString>
+#include <QStringList>
+#include <QDebug>
+
+Sys::KernelInfo Sys::getKernelInfo()
+{
+ Sys::KernelInfo out;
+ struct utsname buf;
+ uname(&buf);
+ // NOTE: we assume linux here. this needs further elaboration
+ out.kernelType = KernelType::Linux;
+ out.kernelName = buf.sysname;
+ QString release = out.kernelVersion = buf.release;
+
+ // linux binary running on WSL is cursed.
+ out.isCursed = release.contains("WSL", Qt::CaseInsensitive) ||
+ release.contains("Microsoft", Qt::CaseInsensitive);
+
+ out.kernelMajor = 0;
+ out.kernelMinor = 0;
+ out.kernelPatch = 0;
+ auto sections = release.split('-');
+ if (sections.size() >= 1) {
+ auto versionParts = sections[0].split('.');
+ if (versionParts.size() >= 3) {
+ out.kernelMajor = versionParts[0].toInt();
+ out.kernelMinor = versionParts[1].toInt();
+ out.kernelPatch = versionParts[2].toInt();
+ } else {
+ qWarning() << "Not enough version numbers in " << sections[0]
+ << " found " << versionParts.size();
+ }
+ } else {
+ qWarning() << "Not enough '-' sections in " << release << " found "
+ << sections.size();
+ }
+ return out;
+}
+
+uint64_t Sys::getSystemRam()
+{
+ std::string token;
+#ifdef Q_OS_LINUX
+ std::ifstream file("/proc/meminfo");
+ while (file >> token) {
+ if (token == "MemTotal:") {
+ uint64_t mem;
+ if (file >> mem) {
+ return mem * 1024ull;
+ } else {
+ return 0;
+ }
+ }
+ // ignore rest of the line
+ file.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
+ }
+#elif defined(Q_OS_FREEBSD)
+ char buff[512];
+ FILE* fp = popen("sysctl hw.physmem", "r");
+ if (fp != NULL) {
+ while (fgets(buff, 512, fp) != NULL) {
+ std::string str(buff);
+ uint64_t mem = std::stoull(str.substr(12, std::string::npos));
+ return mem * 1024ull;
+ }
+ }
+#endif
+ return 0; // nothing found
+}
+
+bool Sys::isCPU64bit()
+{
+ return isSystem64bit();
+}
+
+bool Sys::isSystem64bit()
+{
+ // kernel build arch on linux
+ return QSysInfo::currentCpuArchitecture() == "x86_64";
+}
+
+Sys::DistributionInfo Sys::getDistributionInfo()
+{
+ DistributionInfo systemd_info = read_os_release();
+ DistributionInfo lsb_info = read_lsb_release();
+ DistributionInfo legacy_info = read_legacy_release();
+ DistributionInfo result = systemd_info + lsb_info + legacy_info;
+ if (result.distributionName.isNull()) {
+ result.distributionName = "unknown";
+ }
+ if (result.distributionVersion.isNull()) {
+ if (result.distributionName == "arch") {
+ result.distributionVersion = "rolling";
+ } else {
+ result.distributionVersion = "unknown";
+ }
+ }
+ return result;
+}
diff --git a/meshmc/libraries/systeminfo/src/sys_win32.cpp b/meshmc/libraries/systeminfo/src/sys_win32.cpp
new file mode 100644
index 0000000000..e75a5dcb8b
--- /dev/null
+++ b/meshmc/libraries/systeminfo/src/sys_win32.cpp
@@ -0,0 +1,79 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "sys.h"
+
+#include <windows.h>
+
+Sys::KernelInfo Sys::getKernelInfo()
+{
+ Sys::KernelInfo out;
+ out.kernelType = KernelType::Windows;
+ out.kernelName = "Windows";
+ OSVERSIONINFOW osvi;
+ ZeroMemory(&osvi, sizeof(OSVERSIONINFOW));
+ osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW);
+ GetVersionExW(&osvi);
+ out.kernelVersion =
+ QString("%1.%2").arg(osvi.dwMajorVersion).arg(osvi.dwMinorVersion);
+ out.kernelMajor = osvi.dwMajorVersion;
+ out.kernelMinor = osvi.dwMinorVersion;
+ out.kernelPatch = osvi.dwBuildNumber;
+ return out;
+}
+
+uint64_t Sys::getSystemRam()
+{
+ MEMORYSTATUSEX status;
+ status.dwLength = sizeof(status);
+ GlobalMemoryStatusEx(&status);
+ // bytes
+ return (uint64_t)status.ullTotalPhys;
+}
+
+bool Sys::isSystem64bit()
+{
+#if defined(_WIN64)
+ return true;
+#elif defined(_WIN32)
+ BOOL f64 = false;
+ return IsWow64Process(GetCurrentProcess(), &f64) && f64;
+#else
+ // it's some other kind of system...
+ return false;
+#endif
+}
+
+bool Sys::isCPU64bit()
+{
+ SYSTEM_INFO info;
+ ZeroMemory(&info, sizeof(SYSTEM_INFO));
+ GetNativeSystemInfo(&info);
+ auto arch = info.wProcessorArchitecture;
+ return arch == PROCESSOR_ARCHITECTURE_AMD64 ||
+ arch == PROCESSOR_ARCHITECTURE_IA64;
+}
+
+Sys::DistributionInfo Sys::getDistributionInfo()
+{
+ DistributionInfo result;
+ return result;
+}
diff --git a/meshmc/libraries/tomlc99/CMakeLists.txt b/meshmc/libraries/tomlc99/CMakeLists.txt
new file mode 100644
index 0000000000..60786923c1
--- /dev/null
+++ b/meshmc/libraries/tomlc99/CMakeLists.txt
@@ -0,0 +1,10 @@
+project(tomlc99)
+
+set(tomlc99_SOURCES
+include/toml.h
+src/toml.c
+)
+
+add_library(tomlc99 STATIC ${tomlc99_SOURCES})
+
+target_include_directories(tomlc99 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
diff --git a/meshmc/libraries/tomlc99/LICENSE b/meshmc/libraries/tomlc99/LICENSE
new file mode 100644
index 0000000000..a3292b1600
--- /dev/null
+++ b/meshmc/libraries/tomlc99/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2017 CK Tan
+https://github.com/cktan/tomlc99
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/meshmc/libraries/tomlc99/README.md b/meshmc/libraries/tomlc99/README.md
new file mode 100644
index 0000000000..6715b5be26
--- /dev/null
+++ b/meshmc/libraries/tomlc99/README.md
@@ -0,0 +1,194 @@
+# tomlc99
+
+TOML in c99; v1.0 compliant.
+
+If you are looking for a C++ library, you might try this wrapper: [https://github.com/cktan/tomlcpp](https://github.com/cktan/tomlcpp).
+
+* Compatible with [TOML v1.0.0](https://toml.io/en/v1.0.0).
+* Tested with multiple test suites, including
+[BurntSushi/toml-test](https://github.com/BurntSushi/toml-test) and
+[iarna/toml-spec-tests](https://github.com/iarna/toml-spec-tests).
+* Provides very simple and intuitive interface.
+
+
+## Usage
+
+Please see the `toml.h` file for details. What follows is a simple example that
+parses this config file:
+
+```toml
+[server]
+ host = "www.example.com"
+ port = [ 8080, 8181, 8282 ]
+```
+
+The steps for getting values from our file is usually :
+
+1. Parse the TOML file.
+2. Traverse and locate a table in TOML.
+3. Extract values from the table.
+4. Free up allocated memory.
+
+Below is an example of parsing the values from the example table.
+
+```c
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include "toml.h"
+
+static void error(const char* msg, const char* msg1)
+{
+ fprintf(stderr, "ERROR: %s%s\n", msg, msg1?msg1:"");
+ exit(1);
+}
+
+
+int main()
+{
+ FILE* fp;
+ char errbuf[200];
+
+ // 1. Read and parse toml file
+ fp = fopen("sample.toml", "r");
+ if (!fp) {
+ error("cannot open sample.toml - ", strerror(errno));
+ }
+
+ toml_table_t* conf = toml_parse_file(fp, errbuf, sizeof(errbuf));
+ fclose(fp);
+
+ if (!conf) {
+ error("cannot parse - ", errbuf);
+ }
+
+ // 2. Traverse to a table.
+ toml_table_t* server = toml_table_in(conf, "server");
+ if (!server) {
+ error("missing [server]", "");
+ }
+
+ // 3. Extract values
+ toml_datum_t host = toml_string_in(server, "host");
+ if (!host.ok) {
+ error("cannot read server.host", "");
+ }
+
+ toml_array_t* portarray = toml_array_in(server, "port");
+ if (!portarray) {
+ error("cannot read server.port", "");
+ }
+
+ printf("host: %s\n", host.u.s);
+ printf("port: ");
+ for (int i = 0; ; i++) {
+ toml_datum_t port = toml_int_at(portarray, i);
+ if (!port.ok) break;
+ printf("%d ", (int)port.u.i);
+ }
+ printf("\n");
+
+ // 4. Free memory
+ free(host.u.s);
+ toml_free(conf);
+ return 0;
+}
+```
+
+#### Accessing Table Content
+
+TOML tables are dictionaries where lookups are done using string keys. In
+general, all access functions on tables are named `toml_*_in(...)`.
+
+In the normal case, you know the key and its content type, and retrievals can be done
+using one of these functions:
+```c
+toml_string_in(tab, key);
+toml_bool_in(tab, key);
+toml_int_in(tab, key);
+toml_double_in(tab, key);
+toml_timestamp_in(tab, key);
+toml_table_in(tab, key);
+toml_array_in(tab, key);
+```
+
+You can also interrogate the keys in a table using an integer index:
+```c
+toml_table_t* tab = toml_parse_file(...);
+for (int i = 0; ; i++) {
+ const char* key = toml_key_in(tab, i);
+ if (!key) break;
+ printf("key %d: %s\n", i, key);
+}
+```
+
+#### Accessing Array Content
+
+TOML arrays can be deref-ed using integer indices. In general, all access methods on arrays are named `toml_*_at()`.
+
+To obtain the size of an array:
+```c
+int size = toml_array_nelem(arr);
+```
+
+To obtain the content of an array, use a valid index and call one of these functions:
+```c
+toml_string_at(arr, idx);
+toml_bool_at(arr, idx);
+toml_int_at(arr, idx);
+toml_double_at(arr, idx);
+toml_timestamp_at(arr, idx);
+toml_table_at(arr, idx);
+toml_array_at(arr, idx);
+```
+
+#### toml_datum_t
+
+Some `toml_*_at` and `toml_*_in` functions return a toml_datum_t
+structure. The `ok` flag in the structure indicates if the function
+call was successful. If so, you may proceed to read the value
+corresponding to the type of the content.
+
+For example:
+```
+toml_datum_t host = toml_string_in(tab, "host");
+if (host.ok) {
+ printf("host: %s\n", host.u.s);
+ free(host.u.s); /* FREE applies to string and timestamp types only */
+}
+```
+
+** IMPORTANT: if the accessed value is a string or a timestamp, you must call `free(datum.u.s)` or `free(datum.u.ts)` respectively after usage. **
+
+## Building and installing
+
+A normal *make* suffices. You can also simply include the
+`toml.c` and `toml.h` files in your project.
+
+Invoking `make install` will install the header and library files into
+/usr/local/{include,lib}.
+
+Alternatively, specify `make install prefix=/a/file/path` to install into
+/a/file/path/{include,lib}.
+
+## Testing
+
+To test against the standard test set provided by BurntSushi/toml-test:
+
+```sh
+% make
+% cd test1
+% bash build.sh # do this once
+% bash run.sh # this will run the test suite
+```
+
+
+To test against the standard test set provided by iarna/toml:
+
+```sh
+% make
+% cd test2
+% bash build.sh # do this once
+% bash run.sh # this will run the test suite
+```
diff --git a/meshmc/libraries/tomlc99/include/toml.h b/meshmc/libraries/tomlc99/include/toml.h
new file mode 100644
index 0000000000..82c6092c42
--- /dev/null
+++ b/meshmc/libraries/tomlc99/include/toml.h
@@ -0,0 +1,192 @@
+/*
+ SPDX-FileCopyrightText: 2026 Project Tick
+ SPDX-FileContributor: Project Tick
+ SPDX-License-Identifier: GPL-3.0-or-later
+
+ MeshMC - A Custom Launcher for Minecraft
+ Copyright (C) 2026 Project Tick
+
+ 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.
+
+ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+ This file incorporates work covered by the following copyright and
+ permission notice:
+
+ MIT License
+
+ Copyright (c) 2017 - 2019 CK Tan
+ https://github.com/cktan/tomlc99
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+#ifndef TOML_H
+#define TOML_H
+
+#include <stdio.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+#define TOML_EXTERN extern "C"
+#else
+#define TOML_EXTERN extern
+#endif
+
+typedef struct toml_timestamp_t toml_timestamp_t;
+typedef struct toml_table_t toml_table_t;
+typedef struct toml_array_t toml_array_t;
+typedef struct toml_datum_t toml_datum_t;
+
+/* Parse a file. Return a table on success, or 0 otherwise.
+ * Caller must toml_free(the-return-value) after use.
+ */
+TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, char* errbuf, int errbufsz);
+
+/* Parse a string containing the full config.
+ * Return a table on success, or 0 otherwise.
+ * Caller must toml_free(the-return-value) after use.
+ */
+TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */
+ char* errbuf, int errbufsz);
+
+/* Free the table returned by toml_parse() or toml_parse_file(). Once
+ * this function is called, any handles accessed through this tab
+ * directly or indirectly are no longer valid.
+ */
+TOML_EXTERN void toml_free(toml_table_t* tab);
+
+/* Timestamp types. The year, month, day, hour, minute, second, z
+ * fields may be NULL if they are not relevant. e.g. In a DATE
+ * type, the hour, minute, second and z fields will be NULLs.
+ */
+struct toml_timestamp_t {
+ struct { /* internal. do not use. */
+ int year, month, day;
+ int hour, minute, second, millisec;
+ char z[10];
+ } __buffer;
+ int *year, *month, *day;
+ int *hour, *minute, *second, *millisec;
+ char* z;
+};
+
+/*-----------------------------------------------------------------
+ * Enhanced access methods
+ */
+struct toml_datum_t {
+ int ok;
+ union {
+ toml_timestamp_t* ts; /* ts must be freed after use */
+ char* s; /* string value. s must be freed after use */
+ int b; /* bool value */
+ int64_t i; /* int value */
+ double d; /* double value */
+ } u;
+};
+
+/* on arrays: */
+/* ... retrieve size of array. */
+TOML_EXTERN int toml_array_nelem(const toml_array_t* arr);
+/* ... retrieve values using index. */
+TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx);
+/* ... retrieve array or table using index. */
+TOML_EXTERN toml_array_t* toml_array_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_table_t* toml_table_at(const toml_array_t* arr, int idx);
+
+/* on tables: */
+/* ... retrieve the key in table at keyidx. Return 0 if out of range. */
+TOML_EXTERN const char* toml_key_in(const toml_table_t* tab, int keyidx);
+/* ... retrieve values using key. */
+TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t* arr,
+ const char* key);
+TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t* arr,
+ const char* key);
+TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t* arr,
+ const char* key);
+/* .. retrieve array or table using key. */
+TOML_EXTERN toml_array_t* toml_array_in(const toml_table_t* tab,
+ const char* key);
+TOML_EXTERN toml_table_t* toml_table_in(const toml_table_t* tab,
+ const char* key);
+
+/*-----------------------------------------------------------------
+ * lesser used
+ */
+/* Return the array kind: 't'able, 'a'rray, 'v'alue, 'm'ixed */
+TOML_EXTERN char toml_array_kind(const toml_array_t* arr);
+
+/* For array kind 'v'alue, return the type of values
+ i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp, 'm'ixed
+ 0 if unknown
+*/
+TOML_EXTERN char toml_array_type(const toml_array_t* arr);
+
+/* Return the key of an array */
+TOML_EXTERN const char* toml_array_key(const toml_array_t* arr);
+
+/* Return the number of key-values in a table */
+TOML_EXTERN int toml_table_nkval(const toml_table_t* tab);
+
+/* Return the number of arrays in a table */
+TOML_EXTERN int toml_table_narr(const toml_table_t* tab);
+
+/* Return the number of sub-tables in a table */
+TOML_EXTERN int toml_table_ntab(const toml_table_t* tab);
+
+/* Return the key of a table*/
+TOML_EXTERN const char* toml_table_key(const toml_table_t* tab);
+
+/*--------------------------------------------------------------
+ * misc
+ */
+TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret);
+TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]);
+TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t),
+ void (*xxfree)(void*));
+
+/*--------------------------------------------------------------
+ * deprecated
+ */
+/* A raw value, must be processed by toml_rto* before using. */
+typedef const char* toml_raw_t;
+TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key);
+TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t* arr, int idx);
+TOML_EXTERN int toml_rtos(toml_raw_t s, char** ret);
+TOML_EXTERN int toml_rtob(toml_raw_t s, int* ret);
+TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t* ret);
+TOML_EXTERN int toml_rtod(toml_raw_t s, double* ret);
+TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double* ret, char* buf, int buflen);
+TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t* ret);
+
+#endif /* TOML_H */
diff --git a/meshmc/libraries/tomlc99/src/toml.c b/meshmc/libraries/tomlc99/src/toml.c
new file mode 100644
index 0000000000..2ce22d2c8e
--- /dev/null
+++ b/meshmc/libraries/tomlc99/src/toml.c
@@ -0,0 +1,2468 @@
+/*
+ SPDX-FileCopyrightText: 2026 Project Tick
+ SPDX-FileContributor: Project Tick
+ SPDX-License-Identifier: GPL-3.0-or-later
+
+ MeshMC - A Custom Launcher for Minecraft
+ Copyright (C) 2026 Project Tick
+
+ 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.
+
+ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+ This file incorporates work covered by the following copyright and
+ permission notice:
+
+ MIT License
+
+ Copyright (c) 2017 - 2021 CK Tan
+ https://github.com/cktan/tomlc99
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+*/
+#define _POSIX_C_SOURCE 200809L
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <errno.h>
+#include <stdint.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdbool.h>
+#include "toml.h"
+
+static void* (*ppmalloc)(size_t) = malloc;
+static void (*ppfree)(void*) = free;
+
+void toml_set_memutil(void* (*xxmalloc)(size_t), void (*xxfree)(void*))
+{
+ if (xxmalloc)
+ ppmalloc = xxmalloc;
+ if (xxfree)
+ ppfree = xxfree;
+}
+
+#define MALLOC(a) ppmalloc(a)
+#define FREE(a) ppfree(a)
+
+static void* CALLOC(size_t nmemb, size_t sz)
+{
+ int nb = sz * nmemb;
+ void* p = MALLOC(nb);
+ if (p) {
+ memset(p, 0, nb);
+ }
+ return p;
+}
+
+static char* STRDUP(const char* s)
+{
+ int len = strlen(s);
+ char* p = MALLOC(len + 1);
+ if (p) {
+ memcpy(p, s, len);
+ p[len] = 0;
+ }
+ return p;
+}
+
+static char* STRNDUP(const char* s, size_t n)
+{
+ size_t len = strnlen(s, n);
+ char* p = MALLOC(len + 1);
+ if (p) {
+ memcpy(p, s, len);
+ p[len] = 0;
+ }
+ return p;
+}
+
+/**
+ * Convert a char in utf8 into UCS, and store it in *ret.
+ * Return #bytes consumed or -1 on failure.
+ */
+int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret)
+{
+ const unsigned char* buf = (const unsigned char*)orig;
+ unsigned i = *buf++;
+ int64_t v;
+
+ /* 0x00000000 - 0x0000007F:
+ 0xxxxxxx
+ */
+ if (0 == (i >> 7)) {
+ if (len < 1)
+ return -1;
+ v = i;
+ return *ret = v, 1;
+ }
+ /* 0x00000080 - 0x000007FF:
+ 110xxxxx 10xxxxxx
+ */
+ if (0x6 == (i >> 5)) {
+ if (len < 2)
+ return -1;
+ v = i & 0x1f;
+ for (int j = 0; j < 1; j++) {
+ i = *buf++;
+ if (0x2 != (i >> 6))
+ return -1;
+ v = (v << 6) | (i & 0x3f);
+ }
+ return *ret = v, (const char*)buf - orig;
+ }
+
+ /* 0x00000800 - 0x0000FFFF:
+ 1110xxxx 10xxxxxx 10xxxxxx
+ */
+ if (0xE == (i >> 4)) {
+ if (len < 3)
+ return -1;
+ v = i & 0x0F;
+ for (int j = 0; j < 2; j++) {
+ i = *buf++;
+ if (0x2 != (i >> 6))
+ return -1;
+ v = (v << 6) | (i & 0x3f);
+ }
+ return *ret = v, (const char*)buf - orig;
+ }
+
+ /* 0x00010000 - 0x001FFFFF:
+ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (0x1E == (i >> 3)) {
+ if (len < 4)
+ return -1;
+ v = i & 0x07;
+ for (int j = 0; j < 3; j++) {
+ i = *buf++;
+ if (0x2 != (i >> 6))
+ return -1;
+ v = (v << 6) | (i & 0x3f);
+ }
+ return *ret = v, (const char*)buf - orig;
+ }
+
+ /* 0x00200000 - 0x03FFFFFF:
+ 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (0x3E == (i >> 2)) {
+ if (len < 5)
+ return -1;
+ v = i & 0x03;
+ for (int j = 0; j < 4; j++) {
+ i = *buf++;
+ if (0x2 != (i >> 6))
+ return -1;
+ v = (v << 6) | (i & 0x3f);
+ }
+ return *ret = v, (const char*)buf - orig;
+ }
+
+ /* 0x04000000 - 0x7FFFFFFF:
+ 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (0x7e == (i >> 1)) {
+ if (len < 6)
+ return -1;
+ v = i & 0x01;
+ for (int j = 0; j < 5; j++) {
+ i = *buf++;
+ if (0x2 != (i >> 6))
+ return -1;
+ v = (v << 6) | (i & 0x3f);
+ }
+ return *ret = v, (const char*)buf - orig;
+ }
+ return -1;
+}
+
+/**
+ * Convert a UCS char to utf8 code, and return it in buf.
+ * Return #bytes used in buf to encode the char, or
+ * -1 on error.
+ */
+int toml_ucs_to_utf8(int64_t code, char buf[6])
+{
+ /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16
+ */
+ /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well
+ * as 0xfffe and 0xffff (UCS noncharacters) should not appear in
+ * conforming UTF-8 streams.
+ */
+ if (0xd800 <= code && code <= 0xdfff)
+ return -1;
+ if (0xfffe <= code && code <= 0xffff)
+ return -1;
+
+ /* 0x00000000 - 0x0000007F:
+ 0xxxxxxx
+ */
+ if (code < 0)
+ return -1;
+ if (code <= 0x7F) {
+ buf[0] = (unsigned char)code;
+ return 1;
+ }
+
+ /* 0x00000080 - 0x000007FF:
+ 110xxxxx 10xxxxxx
+ */
+ if (code <= 0x000007FF) {
+ buf[0] = 0xc0 | (code >> 6);
+ buf[1] = 0x80 | (code & 0x3f);
+ return 2;
+ }
+
+ /* 0x00000800 - 0x0000FFFF:
+ 1110xxxx 10xxxxxx 10xxxxxx
+ */
+ if (code <= 0x0000FFFF) {
+ buf[0] = 0xe0 | (code >> 12);
+ buf[1] = 0x80 | ((code >> 6) & 0x3f);
+ buf[2] = 0x80 | (code & 0x3f);
+ return 3;
+ }
+
+ /* 0x00010000 - 0x001FFFFF:
+ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (code <= 0x001FFFFF) {
+ buf[0] = 0xf0 | (code >> 18);
+ buf[1] = 0x80 | ((code >> 12) & 0x3f);
+ buf[2] = 0x80 | ((code >> 6) & 0x3f);
+ buf[3] = 0x80 | (code & 0x3f);
+ return 4;
+ }
+
+ /* 0x00200000 - 0x03FFFFFF:
+ 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (code <= 0x03FFFFFF) {
+ buf[0] = 0xf8 | (code >> 24);
+ buf[1] = 0x80 | ((code >> 18) & 0x3f);
+ buf[2] = 0x80 | ((code >> 12) & 0x3f);
+ buf[3] = 0x80 | ((code >> 6) & 0x3f);
+ buf[4] = 0x80 | (code & 0x3f);
+ return 5;
+ }
+
+ /* 0x04000000 - 0x7FFFFFFF:
+ 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+ */
+ if (code <= 0x7FFFFFFF) {
+ buf[0] = 0xfc | (code >> 30);
+ buf[1] = 0x80 | ((code >> 24) & 0x3f);
+ buf[2] = 0x80 | ((code >> 18) & 0x3f);
+ buf[3] = 0x80 | ((code >> 12) & 0x3f);
+ buf[4] = 0x80 | ((code >> 6) & 0x3f);
+ buf[5] = 0x80 | (code & 0x3f);
+ return 6;
+ }
+
+ return -1;
+}
+
+/*
+ * TOML has 3 data structures: value, array, table.
+ * Each of them can have identification key.
+ */
+typedef struct toml_keyval_t toml_keyval_t;
+struct toml_keyval_t {
+ const char* key; /* key to this value */
+ const char* val; /* the raw value */
+};
+
+typedef struct toml_arritem_t toml_arritem_t;
+struct toml_arritem_t {
+ int valtype; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime,
+ 'D'ate, 'T'imestamp */
+ char* val;
+ toml_array_t* arr;
+ toml_table_t* tab;
+};
+
+struct toml_array_t {
+ const char* key; /* key to this array */
+ int kind; /* element kind: 'v'alue, 'a'rray, or 't'able, 'm'ixed */
+ int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime,
+ 'D'ate, 'T'imestamp, 'm'ixed */
+
+ int nitem; /* number of elements */
+ toml_arritem_t* item;
+};
+
+struct toml_table_t {
+ const char* key; /* key to this table */
+ bool implicit; /* table was created implicitly */
+ bool readonly; /* no more modification allowed */
+
+ /* key-values in the table */
+ int nkval;
+ toml_keyval_t** kval;
+
+ /* arrays in the table */
+ int narr;
+ toml_array_t** arr;
+
+ /* tables in the table */
+ int ntab;
+ toml_table_t** tab;
+};
+
+static inline void xfree(const void* x)
+{
+ if (x)
+ FREE((void*)(intptr_t)x);
+}
+
+enum tokentype_t {
+ INVALID,
+ DOT,
+ COMMA,
+ EQUAL,
+ LBRACE,
+ RBRACE,
+ NEWLINE,
+ LBRACKET,
+ RBRACKET,
+ STRING,
+};
+typedef enum tokentype_t tokentype_t;
+
+typedef struct token_t token_t;
+struct token_t {
+ tokentype_t tok;
+ int lineno;
+ char* ptr; /* points into context->start */
+ int len;
+ int eof;
+};
+
+typedef struct context_t context_t;
+struct context_t {
+ char* start;
+ char* stop;
+ char* errbuf;
+ int errbufsz;
+
+ token_t tok;
+ toml_table_t* root;
+ toml_table_t* curtab;
+
+ struct {
+ int top;
+ char* key[10];
+ token_t tok[10];
+ } tpath;
+};
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+#define FLINE __FILE__ ":" TOSTRING(__LINE__)
+
+static int next_token(context_t* ctx, int dotisspecial);
+
+/*
+ Error reporting. Call when an error is detected. Always return -1.
+*/
+static int e_outofmemory(context_t* ctx, const char* fline)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline);
+ return -1;
+}
+
+static int e_internal(context_t* ctx, const char* fline)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline);
+ return -1;
+}
+
+static int e_syntax(context_t* ctx, int lineno, const char* msg)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg);
+ return -1;
+}
+
+static int e_badkey(context_t* ctx, int lineno)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno);
+ return -1;
+}
+
+static int e_keyexists(context_t* ctx, int lineno)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno);
+ return -1;
+}
+
+static int e_forbid(context_t* ctx, int lineno, const char* msg)
+{
+ snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg);
+ return -1;
+}
+
+static void* expand(void* p, int sz, int newsz)
+{
+ void* s = MALLOC(newsz);
+ if (!s)
+ return 0;
+
+ memcpy(s, p, sz);
+ FREE(p);
+ return s;
+}
+
+static void** expand_ptrarr(void** p, int n)
+{
+ void** s = MALLOC((n + 1) * sizeof(void*));
+ if (!s)
+ return 0;
+
+ s[n] = 0;
+ memcpy(s, p, n * sizeof(void*));
+ FREE(p);
+ return s;
+}
+
+static toml_arritem_t* expand_arritem(toml_arritem_t* p, int n)
+{
+ toml_arritem_t* pp = expand(p, n * sizeof(*p), (n + 1) * sizeof(*p));
+ if (!pp)
+ return 0;
+
+ memset(&pp[n], 0, sizeof(pp[n]));
+ return pp;
+}
+
+static char* norm_lit_str(const char* src, int srclen, int multiline,
+ char* errbuf, int errbufsz)
+{
+ char* dst = 0; /* will write to dst[] and return it */
+ int max = 0; /* max size of dst[] */
+ int off = 0; /* cur offset in dst[] */
+ const char* sp = src;
+ const char* sq = src + srclen;
+ int ch;
+
+ /* scan forward on src */
+ for (;;) {
+ if (off >= max - 10) { /* have some slack for misc stuff */
+ int newmax = max + 50;
+ char* x = expand(dst, max, newmax);
+ if (!x) {
+ xfree(dst);
+ snprintf(errbuf, errbufsz, "out of memory");
+ return 0;
+ }
+ dst = x;
+ max = newmax;
+ }
+
+ /* finished? */
+ if (sp >= sq)
+ break;
+
+ ch = *sp++;
+ /* control characters other than tab is not allowed */
+ if ((0 <= ch && ch <= 0x08) || (0x0a <= ch && ch <= 0x1f) ||
+ (ch == 0x7f)) {
+ if (!(multiline && (ch == '\r' || ch == '\n'))) {
+ xfree(dst);
+ snprintf(errbuf, errbufsz, "invalid char U+%04x", ch);
+ return 0;
+ }
+ }
+
+ // a plain copy suffice
+ dst[off++] = ch;
+ }
+
+ dst[off++] = 0;
+ return dst;
+}
+
+/*
+ * Convert src to raw unescaped utf-8 string.
+ * Returns NULL if error with errmsg in errbuf.
+ */
+static char* norm_basic_str(const char* src, int srclen, int multiline,
+ char* errbuf, int errbufsz)
+{
+ char* dst = 0; /* will write to dst[] and return it */
+ int max = 0; /* max size of dst[] */
+ int off = 0; /* cur offset in dst[] */
+ const char* sp = src;
+ const char* sq = src + srclen;
+ int ch;
+
+ /* scan forward on src */
+ for (;;) {
+ if (off >= max - 10) { /* have some slack for misc stuff */
+ int newmax = max + 50;
+ char* x = expand(dst, max, newmax);
+ if (!x) {
+ xfree(dst);
+ snprintf(errbuf, errbufsz, "out of memory");
+ return 0;
+ }
+ dst = x;
+ max = newmax;
+ }
+
+ /* finished? */
+ if (sp >= sq)
+ break;
+
+ ch = *sp++;
+ if (ch != '\\') {
+ /* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F,
+ * U+007F */
+ if ((0 <= ch && ch <= 0x08) || (0x0a <= ch && ch <= 0x1f) ||
+ (ch == 0x7f)) {
+ if (!(multiline && (ch == '\r' || ch == '\n'))) {
+ xfree(dst);
+ snprintf(errbuf, errbufsz, "invalid char U+%04x", ch);
+ return 0;
+ }
+ }
+
+ // a plain copy suffice
+ dst[off++] = ch;
+ continue;
+ }
+
+ /* ch was backslash. we expect the escape char. */
+ if (sp >= sq) {
+ snprintf(errbuf, errbufsz, "last backslash is invalid");
+ xfree(dst);
+ return 0;
+ }
+
+ /* for multi-line, we want to kill line-ending-backslash ... */
+ if (multiline) {
+
+ // if there is only whitespace after the backslash ...
+ if (sp[strspn(sp, " \t\r")] == '\n') {
+ /* skip all the following whitespaces */
+ sp += strspn(sp, " \t\r\n");
+ continue;
+ }
+ }
+
+ /* get the escaped char */
+ ch = *sp++;
+ switch (ch) {
+ case 'u':
+ case 'U': {
+ int64_t ucs = 0;
+ int nhex = (ch == 'u' ? 4 : 8);
+ for (int i = 0; i < nhex; i++) {
+ if (sp >= sq) {
+ snprintf(errbuf, errbufsz, "\\%c expects %d hex chars",
+ ch, nhex);
+ xfree(dst);
+ return 0;
+ }
+ ch = *sp++;
+ int v =
+ ('0' <= ch && ch <= '9')
+ ? ch - '0'
+ : (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1);
+ if (-1 == v) {
+ snprintf(errbuf, errbufsz,
+ "invalid hex chars for \\u or \\U");
+ xfree(dst);
+ return 0;
+ }
+ ucs = ucs * 16 + v;
+ }
+ int n = toml_ucs_to_utf8(ucs, &dst[off]);
+ if (-1 == n) {
+ snprintf(errbuf, errbufsz,
+ "illegal ucs code in \\u or \\U");
+ xfree(dst);
+ return 0;
+ }
+ off += n;
+ }
+ continue;
+
+ case 'b':
+ ch = '\b';
+ break;
+ case 't':
+ ch = '\t';
+ break;
+ case 'n':
+ ch = '\n';
+ break;
+ case 'f':
+ ch = '\f';
+ break;
+ case 'r':
+ ch = '\r';
+ break;
+ case '"':
+ ch = '"';
+ break;
+ case '\\':
+ ch = '\\';
+ break;
+ default:
+ snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch);
+ xfree(dst);
+ return 0;
+ }
+
+ dst[off++] = ch;
+ }
+
+ // Cap with NUL and return it.
+ dst[off++] = 0;
+ return dst;
+}
+
+/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */
+static char* normalize_key(context_t* ctx, token_t strtok)
+{
+ const char* sp = strtok.ptr;
+ const char* sq = strtok.ptr + strtok.len;
+ int lineno = strtok.lineno;
+ char* ret;
+ int ch = *sp;
+ char ebuf[80];
+
+ /* handle quoted string */
+ if (ch == '\'' || ch == '\"') {
+ /* if ''' or """, take 3 chars off front and back. Else, take 1 char
+ * off. */
+ int multiline = 0;
+ if (sp[1] == ch && sp[2] == ch) {
+ sp += 3, sq -= 3;
+ multiline = 1;
+ } else
+ sp++, sq--;
+
+ if (ch == '\'') {
+ /* for single quote, take it verbatim. */
+ if (!(ret = STRNDUP(sp, sq - sp))) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ } else {
+ /* for double quote, we need to normalize */
+ ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf));
+ if (!ret) {
+ e_syntax(ctx, lineno, ebuf);
+ return 0;
+ }
+ }
+
+ /* newlines are not allowed in keys */
+ if (strchr(ret, '\n')) {
+ xfree(ret);
+ e_badkey(ctx, lineno);
+ return 0;
+ }
+ return ret;
+ }
+
+ /* for bare-key allow only this regex: [A-Za-z0-9_-]+ */
+ const char* xp;
+ for (xp = sp; xp != sq; xp++) {
+ int k = *xp;
+ if (isalnum(k))
+ continue;
+ if (k == '_' || k == '-')
+ continue;
+ e_badkey(ctx, lineno);
+ return 0;
+ }
+
+ /* dup and return it */
+ if (!(ret = STRNDUP(sp, sq - sp))) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ return ret;
+}
+
+/*
+ * Look up key in tab. Return 0 if not found, or
+ * 'v'alue, 'a'rray or 't'able depending on the element.
+ */
+static int check_key(toml_table_t* tab, const char* key,
+ toml_keyval_t** ret_val, toml_array_t** ret_arr,
+ toml_table_t** ret_tab)
+{
+ int i;
+ void* dummy;
+
+ if (!ret_tab)
+ ret_tab = (toml_table_t**)&dummy;
+ if (!ret_arr)
+ ret_arr = (toml_array_t**)&dummy;
+ if (!ret_val)
+ ret_val = (toml_keyval_t**)&dummy;
+
+ *ret_tab = 0;
+ *ret_arr = 0;
+ *ret_val = 0;
+
+ for (i = 0; i < tab->nkval; i++) {
+ if (0 == strcmp(key, tab->kval[i]->key)) {
+ *ret_val = tab->kval[i];
+ return 'v';
+ }
+ }
+ for (i = 0; i < tab->narr; i++) {
+ if (0 == strcmp(key, tab->arr[i]->key)) {
+ *ret_arr = tab->arr[i];
+ return 'a';
+ }
+ }
+ for (i = 0; i < tab->ntab; i++) {
+ if (0 == strcmp(key, tab->tab[i]->key)) {
+ *ret_tab = tab->tab[i];
+ return 't';
+ }
+ }
+ return 0;
+}
+
+static int key_kind(toml_table_t* tab, const char* key)
+{
+ return check_key(tab, key, 0, 0, 0);
+}
+
+/* Create a keyval in the table.
+ */
+static toml_keyval_t* create_keyval_in_table(context_t* ctx, toml_table_t* tab,
+ token_t keytok)
+{
+ /* first, normalize the key to be used for lookup.
+ * remember to free it if we error out.
+ */
+ char* newkey = normalize_key(ctx, keytok);
+ if (!newkey)
+ return 0;
+
+ /* if key exists: error out. */
+ toml_keyval_t* dest = 0;
+ if (key_kind(tab, newkey)) {
+ xfree(newkey);
+ e_keyexists(ctx, keytok.lineno);
+ return 0;
+ }
+
+ /* make a new entry */
+ int n = tab->nkval;
+ toml_keyval_t** base;
+ if (0 == (base = (toml_keyval_t**)expand_ptrarr((void**)tab->kval, n))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ tab->kval = base;
+
+ if (0 == (base[n] = (toml_keyval_t*)CALLOC(1, sizeof(*base[n])))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ dest = tab->kval[tab->nkval++];
+
+ /* save the key in the new value struct */
+ dest->key = newkey;
+ return dest;
+}
+
+/* Create a table in the table.
+ */
+static toml_table_t* create_keytable_in_table(context_t* ctx, toml_table_t* tab,
+ token_t keytok)
+{
+ /* first, normalize the key to be used for lookup.
+ * remember to free it if we error out.
+ */
+ char* newkey = normalize_key(ctx, keytok);
+ if (!newkey)
+ return 0;
+
+ /* if key exists: error out */
+ toml_table_t* dest = 0;
+ if (check_key(tab, newkey, 0, 0, &dest)) {
+ xfree(newkey); /* don't need this anymore */
+
+ /* special case: if table exists, but was created implicitly ... */
+ if (dest && dest->implicit) {
+ /* we make it explicit now, and simply return it. */
+ dest->implicit = false;
+ return dest;
+ }
+ e_keyexists(ctx, keytok.lineno);
+ return 0;
+ }
+
+ /* create a new table entry */
+ int n = tab->ntab;
+ toml_table_t** base;
+ if (0 == (base = (toml_table_t**)expand_ptrarr((void**)tab->tab, n))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ tab->tab = base;
+
+ if (0 == (base[n] = (toml_table_t*)CALLOC(1, sizeof(*base[n])))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ dest = tab->tab[tab->ntab++];
+
+ /* save the key in the new table struct */
+ dest->key = newkey;
+ return dest;
+}
+
+/* Create an array in the table.
+ */
+static toml_array_t* create_keyarray_in_table(context_t* ctx, toml_table_t* tab,
+ token_t keytok, char kind)
+{
+ /* first, normalize the key to be used for lookup.
+ * remember to free it if we error out.
+ */
+ char* newkey = normalize_key(ctx, keytok);
+ if (!newkey)
+ return 0;
+
+ /* if key exists: error out */
+ if (key_kind(tab, newkey)) {
+ xfree(newkey); /* don't need this anymore */
+ e_keyexists(ctx, keytok.lineno);
+ return 0;
+ }
+
+ /* make a new array entry */
+ int n = tab->narr;
+ toml_array_t** base;
+ if (0 == (base = (toml_array_t**)expand_ptrarr((void**)tab->arr, n))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ tab->arr = base;
+
+ if (0 == (base[n] = (toml_array_t*)CALLOC(1, sizeof(*base[n])))) {
+ xfree(newkey);
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ toml_array_t* dest = tab->arr[tab->narr++];
+
+ /* save the key in the new array struct */
+ dest->key = newkey;
+ dest->kind = kind;
+ return dest;
+}
+
+static toml_arritem_t* create_value_in_array(context_t* ctx,
+ toml_array_t* parent)
+{
+ const int n = parent->nitem;
+ toml_arritem_t* base = expand_arritem(parent->item, n);
+ if (!base) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ parent->item = base;
+ parent->nitem++;
+ return &parent->item[n];
+}
+
+/* Create an array in an array
+ */
+static toml_array_t* create_array_in_array(context_t* ctx, toml_array_t* parent)
+{
+ const int n = parent->nitem;
+ toml_arritem_t* base = expand_arritem(parent->item, n);
+ if (!base) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ toml_array_t* ret = (toml_array_t*)CALLOC(1, sizeof(toml_array_t));
+ if (!ret) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ base[n].arr = ret;
+ parent->item = base;
+ parent->nitem++;
+ return ret;
+}
+
+/* Create a table in an array
+ */
+static toml_table_t* create_table_in_array(context_t* ctx, toml_array_t* parent)
+{
+ int n = parent->nitem;
+ toml_arritem_t* base = expand_arritem(parent->item, n);
+ if (!base) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ toml_table_t* ret = (toml_table_t*)CALLOC(1, sizeof(toml_table_t));
+ if (!ret) {
+ e_outofmemory(ctx, FLINE);
+ return 0;
+ }
+ base[n].tab = ret;
+ parent->item = base;
+ parent->nitem++;
+ return ret;
+}
+
+static int skip_newlines(context_t* ctx, int isdotspecial)
+{
+ while (ctx->tok.tok == NEWLINE) {
+ if (next_token(ctx, isdotspecial))
+ return -1;
+ if (ctx->tok.eof)
+ break;
+ }
+ return 0;
+}
+
+static int parse_keyval(context_t* ctx, toml_table_t* tab);
+
+static inline int eat_token(context_t* ctx, tokentype_t typ, int isdotspecial,
+ const char* fline)
+{
+ if (ctx->tok.tok != typ)
+ return e_internal(ctx, fline);
+
+ if (next_token(ctx, isdotspecial))
+ return -1;
+
+ return 0;
+}
+
+/* We are at '{ ... }'.
+ * Parse the table.
+ */
+static int parse_inline_table(context_t* ctx, toml_table_t* tab)
+{
+ if (eat_token(ctx, LBRACE, 1, FLINE))
+ return -1;
+
+ for (;;) {
+ if (ctx->tok.tok == NEWLINE)
+ return e_syntax(ctx, ctx->tok.lineno,
+ "newline not allowed in inline table");
+
+ /* until } */
+ if (ctx->tok.tok == RBRACE)
+ break;
+
+ if (ctx->tok.tok != STRING)
+ return e_syntax(ctx, ctx->tok.lineno, "expect a string");
+
+ if (parse_keyval(ctx, tab))
+ return -1;
+
+ if (ctx->tok.tok == NEWLINE)
+ return e_syntax(ctx, ctx->tok.lineno,
+ "newline not allowed in inline table");
+
+ /* on comma, continue to scan for next keyval */
+ if (ctx->tok.tok == COMMA) {
+ if (eat_token(ctx, COMMA, 1, FLINE))
+ return -1;
+ continue;
+ }
+ break;
+ }
+
+ if (eat_token(ctx, RBRACE, 1, FLINE))
+ return -1;
+
+ tab->readonly = 1;
+
+ return 0;
+}
+
+static int valtype(const char* val)
+{
+ toml_timestamp_t ts;
+ if (*val == '\'' || *val == '"')
+ return 's';
+ if (0 == toml_rtob(val, 0))
+ return 'b';
+ if (0 == toml_rtoi(val, 0))
+ return 'i';
+ if (0 == toml_rtod(val, 0))
+ return 'd';
+ if (0 == toml_rtots(val, &ts)) {
+ if (ts.year && ts.hour)
+ return 'T'; /* timestamp */
+ if (ts.year)
+ return 'D'; /* date */
+ return 't'; /* time */
+ }
+ return 'u'; /* unknown */
+}
+
+/* We are at '[...]' */
+static int parse_array(context_t* ctx, toml_array_t* arr)
+{
+ if (eat_token(ctx, LBRACKET, 0, FLINE))
+ return -1;
+
+ for (;;) {
+ if (skip_newlines(ctx, 0))
+ return -1;
+
+ /* until ] */
+ if (ctx->tok.tok == RBRACKET)
+ break;
+
+ switch (ctx->tok.tok) {
+ case STRING: {
+ /* set array kind if this will be the first entry */
+ if (arr->kind == 0)
+ arr->kind = 'v';
+ else if (arr->kind != 'v')
+ arr->kind = 'm';
+
+ char* val = ctx->tok.ptr;
+ int vlen = ctx->tok.len;
+
+ /* make a new value in array */
+ toml_arritem_t* newval = create_value_in_array(ctx, arr);
+ if (!newval)
+ return e_outofmemory(ctx, FLINE);
+
+ if (!(newval->val = STRNDUP(val, vlen)))
+ return e_outofmemory(ctx, FLINE);
+
+ newval->valtype = valtype(newval->val);
+
+ /* set array type if this is the first entry */
+ if (arr->nitem == 1)
+ arr->type = newval->valtype;
+ else if (arr->type != newval->valtype)
+ arr->type = 'm'; /* mixed */
+
+ if (eat_token(ctx, STRING, 0, FLINE))
+ return -1;
+ break;
+ }
+
+ case LBRACKET: { /* [ [array], [array] ... ] */
+ /* set the array kind if this will be the first entry */
+ if (arr->kind == 0)
+ arr->kind = 'a';
+ else if (arr->kind != 'a')
+ arr->kind = 'm';
+
+ toml_array_t* subarr = create_array_in_array(ctx, arr);
+ if (!subarr)
+ return -1;
+ if (parse_array(ctx, subarr))
+ return -1;
+ break;
+ }
+
+ case LBRACE: { /* [ {table}, {table} ... ] */
+ /* set the array kind if this will be the first entry */
+ if (arr->kind == 0)
+ arr->kind = 't';
+ else if (arr->kind != 't')
+ arr->kind = 'm';
+
+ toml_table_t* subtab = create_table_in_array(ctx, arr);
+ if (!subtab)
+ return -1;
+ if (parse_inline_table(ctx, subtab))
+ return -1;
+ break;
+ }
+
+ default:
+ return e_syntax(ctx, ctx->tok.lineno, "syntax error");
+ }
+
+ if (skip_newlines(ctx, 0))
+ return -1;
+
+ /* on comma, continue to scan for next element */
+ if (ctx->tok.tok == COMMA) {
+ if (eat_token(ctx, COMMA, 0, FLINE))
+ return -1;
+ continue;
+ }
+ break;
+ }
+
+ if (eat_token(ctx, RBRACKET, 1, FLINE))
+ return -1;
+ return 0;
+}
+
+/* handle lines like these:
+ key = "value"
+ key = [ array ]
+ key = { table }
+*/
+static int parse_keyval(context_t* ctx, toml_table_t* tab)
+{
+ if (tab->readonly) {
+ return e_forbid(ctx, ctx->tok.lineno,
+ "cannot insert new entry into existing table");
+ }
+
+ token_t key = ctx->tok;
+ if (eat_token(ctx, STRING, 1, FLINE))
+ return -1;
+
+ if (ctx->tok.tok == DOT) {
+ /* handle inline dotted key.
+ e.g.
+ physical.color = "orange"
+ physical.shape = "round"
+ */
+ toml_table_t* subtab = 0;
+ {
+ char* subtabstr = normalize_key(ctx, key);
+ if (!subtabstr)
+ return -1;
+
+ subtab = toml_table_in(tab, subtabstr);
+ xfree(subtabstr);
+ }
+ if (!subtab) {
+ subtab = create_keytable_in_table(ctx, tab, key);
+ if (!subtab)
+ return -1;
+ }
+ if (next_token(ctx, 1))
+ return -1;
+ if (parse_keyval(ctx, subtab))
+ return -1;
+ return 0;
+ }
+
+ if (ctx->tok.tok != EQUAL) {
+ return e_syntax(ctx, ctx->tok.lineno, "missing =");
+ }
+
+ if (next_token(ctx, 0))
+ return -1;
+
+ switch (ctx->tok.tok) {
+ case STRING: { /* key = "value" */
+ toml_keyval_t* keyval = create_keyval_in_table(ctx, tab, key);
+ if (!keyval)
+ return -1;
+ token_t val = ctx->tok;
+
+ assert(keyval->val == 0);
+ if (!(keyval->val = STRNDUP(val.ptr, val.len)))
+ return e_outofmemory(ctx, FLINE);
+
+ if (next_token(ctx, 1))
+ return -1;
+
+ return 0;
+ }
+
+ case LBRACKET: { /* key = [ array ] */
+ toml_array_t* arr = create_keyarray_in_table(ctx, tab, key, 0);
+ if (!arr)
+ return -1;
+ if (parse_array(ctx, arr))
+ return -1;
+ return 0;
+ }
+
+ case LBRACE: { /* key = { table } */
+ toml_table_t* nxttab = create_keytable_in_table(ctx, tab, key);
+ if (!nxttab)
+ return -1;
+ if (parse_inline_table(ctx, nxttab))
+ return -1;
+ return 0;
+ }
+
+ default:
+ return e_syntax(ctx, ctx->tok.lineno, "syntax error");
+ }
+ return 0;
+}
+
+typedef struct tabpath_t tabpath_t;
+struct tabpath_t {
+ int cnt;
+ token_t key[10];
+};
+
+/* at [x.y.z] or [[x.y.z]]
+ * Scan forward and fill tabpath until it enters ] or ]]
+ * There will be at least one entry on return.
+ */
+static int fill_tabpath(context_t* ctx)
+{
+ int lineno = ctx->tok.lineno;
+ int i;
+
+ /* clear tpath */
+ for (i = 0; i < ctx->tpath.top; i++) {
+ char** p = &ctx->tpath.key[i];
+ xfree(*p);
+ *p = 0;
+ }
+ ctx->tpath.top = 0;
+
+ for (;;) {
+ if (ctx->tpath.top >= 10)
+ return e_syntax(ctx, lineno,
+ "table path is too deep; max allowed is 10.");
+
+ if (ctx->tok.tok != STRING)
+ return e_syntax(ctx, lineno, "invalid or missing key");
+
+ char* key = normalize_key(ctx, ctx->tok);
+ if (!key)
+ return -1;
+ ctx->tpath.tok[ctx->tpath.top] = ctx->tok;
+ ctx->tpath.key[ctx->tpath.top] = key;
+ ctx->tpath.top++;
+
+ if (next_token(ctx, 1))
+ return -1;
+
+ if (ctx->tok.tok == RBRACKET)
+ break;
+
+ if (ctx->tok.tok != DOT)
+ return e_syntax(ctx, lineno, "invalid key");
+
+ if (next_token(ctx, 1))
+ return -1;
+ }
+
+ if (ctx->tpath.top <= 0)
+ return e_syntax(ctx, lineno, "empty table selector");
+
+ return 0;
+}
+
+/* Walk tabpath from the root, and create new tables on the way.
+ * Sets ctx->curtab to the final table.
+ */
+static int walk_tabpath(context_t* ctx)
+{
+ /* start from root */
+ toml_table_t* curtab = ctx->root;
+
+ for (int i = 0; i < ctx->tpath.top; i++) {
+ const char* key = ctx->tpath.key[i];
+
+ toml_keyval_t* nextval = 0;
+ toml_array_t* nextarr = 0;
+ toml_table_t* nexttab = 0;
+ switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) {
+ case 't':
+ /* found a table. nexttab is where we will go next. */
+ break;
+
+ case 'a':
+ /* found an array. nexttab is the last table in the array. */
+ if (nextarr->kind != 't')
+ return e_internal(ctx, FLINE);
+
+ if (nextarr->nitem == 0)
+ return e_internal(ctx, FLINE);
+
+ nexttab = nextarr->item[nextarr->nitem - 1].tab;
+ break;
+
+ case 'v':
+ return e_keyexists(ctx, ctx->tpath.tok[i].lineno);
+
+ default: { /* Not found. Let's create an implicit table. */
+ int n = curtab->ntab;
+ toml_table_t** base =
+ (toml_table_t**)expand_ptrarr((void**)curtab->tab, n);
+ if (0 == base)
+ return e_outofmemory(ctx, FLINE);
+
+ curtab->tab = base;
+
+ if (0 == (base[n] = (toml_table_t*)CALLOC(1, sizeof(*base[n]))))
+ return e_outofmemory(ctx, FLINE);
+
+ if (0 == (base[n]->key = STRDUP(key)))
+ return e_outofmemory(ctx, FLINE);
+
+ nexttab = curtab->tab[curtab->ntab++];
+
+ /* tabs created by walk_tabpath are considered implicit */
+ nexttab->implicit = true;
+ } break;
+ }
+
+ /* switch to next tab */
+ curtab = nexttab;
+ }
+
+ /* save it */
+ ctx->curtab = curtab;
+
+ return 0;
+}
+
+/* handle lines like [x.y.z] or [[x.y.z]] */
+static int parse_select(context_t* ctx)
+{
+ assert(ctx->tok.tok == LBRACKET);
+
+ /* true if [[ */
+ int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '[');
+ /* need to detect '[[' on our own because next_token() will skip whitespace,
+ and '[ [' would be taken as '[[', which is wrong. */
+
+ /* eat [ or [[ */
+ if (eat_token(ctx, LBRACKET, 1, FLINE))
+ return -1;
+ if (llb) {
+ assert(ctx->tok.tok == LBRACKET);
+ if (eat_token(ctx, LBRACKET, 1, FLINE))
+ return -1;
+ }
+
+ if (fill_tabpath(ctx))
+ return -1;
+
+ /* For [x.y.z] or [[x.y.z]], remove z from tpath.
+ */
+ token_t z = ctx->tpath.tok[ctx->tpath.top - 1];
+ xfree(ctx->tpath.key[ctx->tpath.top - 1]);
+ ctx->tpath.top--;
+
+ /* set up ctx->curtab */
+ if (walk_tabpath(ctx))
+ return -1;
+
+ if (!llb) {
+ /* [x.y.z] -> create z = {} in x.y */
+ toml_table_t* curtab = create_keytable_in_table(ctx, ctx->curtab, z);
+ if (!curtab)
+ return -1;
+ ctx->curtab = curtab;
+ } else {
+ /* [[x.y.z]] -> create z = [] in x.y */
+ toml_array_t* arr = 0;
+ {
+ char* zstr = normalize_key(ctx, z);
+ if (!zstr)
+ return -1;
+ arr = toml_array_in(ctx->curtab, zstr);
+ xfree(zstr);
+ }
+ if (!arr) {
+ arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't');
+ if (!arr)
+ return -1;
+ }
+ if (arr->kind != 't')
+ return e_syntax(ctx, z.lineno, "array mismatch");
+
+ /* add to z[] */
+ toml_table_t* dest;
+ {
+ toml_table_t* t = create_table_in_array(ctx, arr);
+ if (!t)
+ return -1;
+
+ if (0 == (t->key = STRDUP("__anon__")))
+ return e_outofmemory(ctx, FLINE);
+
+ dest = t;
+ }
+
+ ctx->curtab = dest;
+ }
+
+ if (ctx->tok.tok != RBRACKET) {
+ return e_syntax(ctx, ctx->tok.lineno, "expects ]");
+ }
+ if (llb) {
+ if (!(ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) {
+ return e_syntax(ctx, ctx->tok.lineno, "expects ]]");
+ }
+ if (eat_token(ctx, RBRACKET, 1, FLINE))
+ return -1;
+ }
+
+ if (eat_token(ctx, RBRACKET, 1, FLINE))
+ return -1;
+
+ if (ctx->tok.tok != NEWLINE)
+ return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]");
+
+ return 0;
+}
+
+toml_table_t* toml_parse(char* conf, char* errbuf, int errbufsz)
+{
+ context_t ctx;
+
+ // clear errbuf
+ if (errbufsz <= 0)
+ errbufsz = 0;
+ if (errbufsz > 0)
+ errbuf[0] = 0;
+
+ // init context
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.start = conf;
+ ctx.stop = ctx.start + strlen(conf);
+ ctx.errbuf = errbuf;
+ ctx.errbufsz = errbufsz;
+
+ // start with an artificial newline of length 0
+ ctx.tok.tok = NEWLINE;
+ ctx.tok.lineno = 1;
+ ctx.tok.ptr = conf;
+ ctx.tok.len = 0;
+
+ // make a root table
+ if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) {
+ e_outofmemory(&ctx, FLINE);
+ // Do not goto fail, root table not set up yet
+ return 0;
+ }
+
+ // set root as default table
+ ctx.curtab = ctx.root;
+
+ /* Scan forward until EOF */
+ for (token_t tok = ctx.tok; !tok.eof; tok = ctx.tok) {
+ switch (tok.tok) {
+
+ case NEWLINE:
+ if (next_token(&ctx, 1))
+ goto fail;
+ break;
+
+ case STRING:
+ if (parse_keyval(&ctx, ctx.curtab))
+ goto fail;
+
+ if (ctx.tok.tok != NEWLINE) {
+ e_syntax(&ctx, ctx.tok.lineno, "extra chars after value");
+ goto fail;
+ }
+
+ if (eat_token(&ctx, NEWLINE, 1, FLINE))
+ goto fail;
+ break;
+
+ case LBRACKET: /* [ x.y.z ] or [[ x.y.z ]] */
+ if (parse_select(&ctx))
+ goto fail;
+ break;
+
+ default:
+ e_syntax(&ctx, tok.lineno, "syntax error");
+ goto fail;
+ }
+ }
+
+ /* success */
+ for (int i = 0; i < ctx.tpath.top; i++)
+ xfree(ctx.tpath.key[i]);
+ return ctx.root;
+
+fail:
+ // Something bad has happened. Free resources and return error.
+ for (int i = 0; i < ctx.tpath.top; i++)
+ xfree(ctx.tpath.key[i]);
+ toml_free(ctx.root);
+ return 0;
+}
+
+toml_table_t* toml_parse_file(FILE* fp, char* errbuf, int errbufsz)
+{
+ int bufsz = 0;
+ char* buf = 0;
+ int off = 0;
+
+ /* read from fp into buf */
+ while (!feof(fp)) {
+
+ if (off == bufsz) {
+ int xsz = bufsz + 1000;
+ char* x = expand(buf, bufsz, xsz);
+ if (!x) {
+ snprintf(errbuf, errbufsz, "out of memory");
+ xfree(buf);
+ return 0;
+ }
+ buf = x;
+ bufsz = xsz;
+ }
+
+ errno = 0;
+ int n = fread(buf + off, 1, bufsz - off, fp);
+ if (ferror(fp)) {
+ snprintf(errbuf, errbufsz, "%s",
+ errno ? strerror(errno) : "Error reading file");
+ xfree(buf);
+ return 0;
+ }
+ off += n;
+ }
+
+ /* tag on a NUL to cap the string */
+ if (off == bufsz) {
+ int xsz = bufsz + 1;
+ char* x = expand(buf, bufsz, xsz);
+ if (!x) {
+ snprintf(errbuf, errbufsz, "out of memory");
+ xfree(buf);
+ return 0;
+ }
+ buf = x;
+ bufsz = xsz;
+ }
+ buf[off] = 0;
+
+ /* parse it, cleanup and finish */
+ toml_table_t* ret = toml_parse(buf, errbuf, errbufsz);
+ xfree(buf);
+ return ret;
+}
+
+static void xfree_kval(toml_keyval_t* p)
+{
+ if (!p)
+ return;
+ xfree(p->key);
+ xfree(p->val);
+ xfree(p);
+}
+
+static void xfree_tab(toml_table_t* p);
+
+static void xfree_arr(toml_array_t* p)
+{
+ if (!p)
+ return;
+
+ xfree(p->key);
+ const int n = p->nitem;
+ for (int i = 0; i < n; i++) {
+ toml_arritem_t* a = &p->item[i];
+ if (a->val)
+ xfree(a->val);
+ else if (a->arr)
+ xfree_arr(a->arr);
+ else if (a->tab)
+ xfree_tab(a->tab);
+ }
+ xfree(p->item);
+ xfree(p);
+}
+
+static void xfree_tab(toml_table_t* p)
+{
+ int i;
+
+ if (!p)
+ return;
+
+ xfree(p->key);
+
+ for (i = 0; i < p->nkval; i++)
+ xfree_kval(p->kval[i]);
+ xfree(p->kval);
+
+ for (i = 0; i < p->narr; i++)
+ xfree_arr(p->arr[i]);
+ xfree(p->arr);
+
+ for (i = 0; i < p->ntab; i++)
+ xfree_tab(p->tab[i]);
+ xfree(p->tab);
+
+ xfree(p);
+}
+
+void toml_free(toml_table_t* tab)
+{
+ xfree_tab(tab);
+}
+
+static void set_token(context_t* ctx, tokentype_t tok, int lineno, char* ptr,
+ int len)
+{
+ token_t t;
+ t.tok = tok;
+ t.lineno = lineno;
+ t.ptr = ptr;
+ t.len = len;
+ t.eof = 0;
+ ctx->tok = t;
+}
+
+static void set_eof(context_t* ctx, int lineno)
+{
+ set_token(ctx, NEWLINE, lineno, ctx->stop, 0);
+ ctx->tok.eof = 1;
+}
+
+/* Scan p for n digits compositing entirely of [0-9] */
+static int scan_digits(const char* p, int n)
+{
+ int ret = 0;
+ for (; n > 0 && isdigit(*p); n--, p++) {
+ ret = 10 * ret + (*p - '0');
+ }
+ return n ? -1 : ret;
+}
+
+static int scan_date(const char* p, int* YY, int* MM, int* DD)
+{
+ int year, month, day;
+ year = scan_digits(p, 4);
+ month = (year >= 0 && p[4] == '-') ? scan_digits(p + 5, 2) : -1;
+ day = (month >= 0 && p[7] == '-') ? scan_digits(p + 8, 2) : -1;
+ if (YY)
+ *YY = year;
+ if (MM)
+ *MM = month;
+ if (DD)
+ *DD = day;
+ return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1;
+}
+
+static int scan_time(const char* p, int* hh, int* mm, int* ss)
+{
+ int hour, minute, second;
+ hour = scan_digits(p, 2);
+ minute = (hour >= 0 && p[2] == ':') ? scan_digits(p + 3, 2) : -1;
+ second = (minute >= 0 && p[5] == ':') ? scan_digits(p + 6, 2) : -1;
+ if (hh)
+ *hh = hour;
+ if (mm)
+ *mm = minute;
+ if (ss)
+ *ss = second;
+ return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1;
+}
+
+static int scan_string(context_t* ctx, char* p, int lineno, int dotisspecial)
+{
+ char* orig = p;
+ if (0 == strncmp(p, "'''", 3)) {
+ char* q = p + 3;
+
+ while (1) {
+ q = strstr(q, "'''");
+ if (0 == q) {
+ return e_syntax(ctx, lineno, "unterminated triple-s-quote");
+ }
+ while (q[3] == '\'')
+ q++;
+ break;
+ }
+
+ set_token(ctx, STRING, lineno, orig, q + 3 - orig);
+ return 0;
+ }
+
+ if (0 == strncmp(p, "\"\"\"", 3)) {
+ char* q = p + 3;
+
+ while (1) {
+ q = strstr(q, "\"\"\"");
+ if (0 == q) {
+ return e_syntax(ctx, lineno, "unterminated triple-d-quote");
+ }
+ if (q[-1] == '\\') {
+ q++;
+ continue;
+ }
+ while (q[3] == '\"')
+ q++;
+ break;
+ }
+
+ // the string is [p+3, q-1]
+
+ int hexreq = 0; /* #hex required */
+ int escape = 0;
+ for (p += 3; p < q; p++) {
+ if (escape) {
+ escape = 0;
+ if (strchr("btnfr\"\\", *p))
+ continue;
+ if (*p == 'u') {
+ hexreq = 4;
+ continue;
+ }
+ if (*p == 'U') {
+ hexreq = 8;
+ continue;
+ }
+ if (p[strspn(p, " \t\r")] == '\n')
+ continue; /* allow for line ending backslash */
+ return e_syntax(ctx, lineno, "bad escape char");
+ }
+ if (hexreq) {
+ hexreq--;
+ if (strchr("0123456789ABCDEF", *p))
+ continue;
+ return e_syntax(ctx, lineno, "expect hex char");
+ }
+ if (*p == '\\') {
+ escape = 1;
+ continue;
+ }
+ }
+ if (escape)
+ return e_syntax(ctx, lineno, "expect an escape char");
+ if (hexreq)
+ return e_syntax(ctx, lineno, "expected more hex char");
+
+ set_token(ctx, STRING, lineno, orig, q + 3 - orig);
+ return 0;
+ }
+
+ if ('\'' == *p) {
+ for (p++; *p && *p != '\n' && *p != '\''; p++)
+ ;
+ if (*p != '\'') {
+ return e_syntax(ctx, lineno, "unterminated s-quote");
+ }
+
+ set_token(ctx, STRING, lineno, orig, p + 1 - orig);
+ return 0;
+ }
+
+ if ('\"' == *p) {
+ int hexreq = 0; /* #hex required */
+ int escape = 0;
+ for (p++; *p; p++) {
+ if (escape) {
+ escape = 0;
+ if (strchr("btnfr\"\\", *p))
+ continue;
+ if (*p == 'u') {
+ hexreq = 4;
+ continue;
+ }
+ if (*p == 'U') {
+ hexreq = 8;
+ continue;
+ }
+ return e_syntax(ctx, lineno, "bad escape char");
+ }
+ if (hexreq) {
+ hexreq--;
+ if (strchr("0123456789ABCDEF", *p))
+ continue;
+ return e_syntax(ctx, lineno, "expect hex char");
+ }
+ if (*p == '\\') {
+ escape = 1;
+ continue;
+ }
+ if (*p == '\'') {
+ if (p[1] == '\'' && p[2] == '\'') {
+ return e_syntax(ctx, lineno,
+ "triple-s-quote inside string lit");
+ }
+ continue;
+ }
+ if (*p == '\n')
+ break;
+ if (*p == '"')
+ break;
+ }
+ if (*p != '"') {
+ return e_syntax(ctx, lineno, "unterminated quote");
+ }
+
+ set_token(ctx, STRING, lineno, orig, p + 1 - orig);
+ return 0;
+ }
+
+ /* check for timestamp without quotes */
+ if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) {
+ // forward thru the timestamp
+ for (; strchr("0123456789.:+-T Z", toupper(*p)); p++)
+ ;
+ // squeeze out any spaces at end of string
+ for (; p[-1] == ' '; p--)
+ ;
+ // tokenize
+ set_token(ctx, STRING, lineno, orig, p - orig);
+ return 0;
+ }
+
+ /* literals */
+ for (; *p && *p != '\n'; p++) {
+ int ch = *p;
+ if (ch == '.' && dotisspecial)
+ break;
+ if ('A' <= ch && ch <= 'Z')
+ continue;
+ if ('a' <= ch && ch <= 'z')
+ continue;
+ if (strchr("0123456789+-_.", ch))
+ continue;
+ break;
+ }
+
+ set_token(ctx, STRING, lineno, orig, p - orig);
+ return 0;
+}
+
+static int next_token(context_t* ctx, int dotisspecial)
+{
+ int lineno = ctx->tok.lineno;
+ char* p = ctx->tok.ptr;
+ int i;
+
+ /* eat this tok */
+ for (i = 0; i < ctx->tok.len; i++) {
+ if (*p++ == '\n')
+ lineno++;
+ }
+
+ /* make next tok */
+ while (p < ctx->stop) {
+ /* skip comment. stop just before the \n. */
+ if (*p == '#') {
+ for (p++; p < ctx->stop && *p != '\n'; p++)
+ ;
+ continue;
+ }
+
+ if (dotisspecial && *p == '.') {
+ set_token(ctx, DOT, lineno, p, 1);
+ return 0;
+ }
+
+ switch (*p) {
+ case ',':
+ set_token(ctx, COMMA, lineno, p, 1);
+ return 0;
+ case '=':
+ set_token(ctx, EQUAL, lineno, p, 1);
+ return 0;
+ case '{':
+ set_token(ctx, LBRACE, lineno, p, 1);
+ return 0;
+ case '}':
+ set_token(ctx, RBRACE, lineno, p, 1);
+ return 0;
+ case '[':
+ set_token(ctx, LBRACKET, lineno, p, 1);
+ return 0;
+ case ']':
+ set_token(ctx, RBRACKET, lineno, p, 1);
+ return 0;
+ case '\n':
+ set_token(ctx, NEWLINE, lineno, p, 1);
+ return 0;
+ case '\r':
+ case ' ':
+ case '\t':
+ /* ignore white spaces */
+ p++;
+ continue;
+ }
+
+ return scan_string(ctx, p, lineno, dotisspecial);
+ }
+
+ set_eof(ctx, lineno);
+ return 0;
+}
+
+const char* toml_key_in(const toml_table_t* tab, int keyidx)
+{
+ if (keyidx < tab->nkval)
+ return tab->kval[keyidx]->key;
+
+ keyidx -= tab->nkval;
+ if (keyidx < tab->narr)
+ return tab->arr[keyidx]->key;
+
+ keyidx -= tab->narr;
+ if (keyidx < tab->ntab)
+ return tab->tab[keyidx]->key;
+
+ return 0;
+}
+
+toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key)
+{
+ int i;
+ for (i = 0; i < tab->nkval; i++) {
+ if (0 == strcmp(key, tab->kval[i]->key))
+ return tab->kval[i]->val;
+ }
+ return 0;
+}
+
+toml_array_t* toml_array_in(const toml_table_t* tab, const char* key)
+{
+ int i;
+ for (i = 0; i < tab->narr; i++) {
+ if (0 == strcmp(key, tab->arr[i]->key))
+ return tab->arr[i];
+ }
+ return 0;
+}
+
+toml_table_t* toml_table_in(const toml_table_t* tab, const char* key)
+{
+ int i;
+ for (i = 0; i < tab->ntab; i++) {
+ if (0 == strcmp(key, tab->tab[i]->key))
+ return tab->tab[i];
+ }
+ return 0;
+}
+
+toml_raw_t toml_raw_at(const toml_array_t* arr, int idx)
+{
+ return (0 <= idx && idx < arr->nitem) ? arr->item[idx].val : 0;
+}
+
+char toml_array_kind(const toml_array_t* arr)
+{
+ return arr->kind;
+}
+
+char toml_array_type(const toml_array_t* arr)
+{
+ if (arr->kind != 'v')
+ return 0;
+
+ if (arr->nitem == 0)
+ return 0;
+
+ return arr->type;
+}
+
+int toml_array_nelem(const toml_array_t* arr)
+{
+ return arr->nitem;
+}
+
+const char* toml_array_key(const toml_array_t* arr)
+{
+ return arr ? arr->key : (const char*)NULL;
+}
+
+int toml_table_nkval(const toml_table_t* tab)
+{
+ return tab->nkval;
+}
+
+int toml_table_narr(const toml_table_t* tab)
+{
+ return tab->narr;
+}
+
+int toml_table_ntab(const toml_table_t* tab)
+{
+ return tab->ntab;
+}
+
+const char* toml_table_key(const toml_table_t* tab)
+{
+ return tab ? tab->key : (const char*)NULL;
+}
+
+toml_array_t* toml_array_at(const toml_array_t* arr, int idx)
+{
+ return (0 <= idx && idx < arr->nitem) ? arr->item[idx].arr : 0;
+}
+
+toml_table_t* toml_table_at(const toml_array_t* arr, int idx)
+{
+ return (0 <= idx && idx < arr->nitem) ? arr->item[idx].tab : 0;
+}
+
+int toml_rtots(toml_raw_t src_, toml_timestamp_t* ret)
+{
+ if (!src_)
+ return -1;
+
+ const char* p = src_;
+ int must_parse_time = 0;
+
+ memset(ret, 0, sizeof(*ret));
+
+ int* year = &ret->__buffer.year;
+ int* month = &ret->__buffer.month;
+ int* day = &ret->__buffer.day;
+ int* hour = &ret->__buffer.hour;
+ int* minute = &ret->__buffer.minute;
+ int* second = &ret->__buffer.second;
+ int* millisec = &ret->__buffer.millisec;
+
+ /* parse date YYYY-MM-DD */
+ if (0 == scan_date(p, year, month, day)) {
+ ret->year = year;
+ ret->month = month;
+ ret->day = day;
+
+ p += 10;
+ if (*p) {
+ // parse the T or space separator
+ if (*p != 'T' && *p != ' ')
+ return -1;
+ must_parse_time = 1;
+ p++;
+ }
+ }
+
+ /* parse time HH:MM:SS */
+ if (0 == scan_time(p, hour, minute, second)) {
+ ret->hour = hour;
+ ret->minute = minute;
+ ret->second = second;
+
+ /* optionally, parse millisec */
+ p += 8;
+ if (*p == '.') {
+ char* qq;
+ p++;
+ errno = 0;
+ *millisec = strtol(p, &qq, 0);
+ if (errno) {
+ return -1;
+ }
+ while (*millisec > 999) {
+ *millisec /= 10;
+ }
+
+ ret->millisec = millisec;
+ p = qq;
+ }
+
+ if (*p) {
+ /* parse and copy Z */
+ char* z = ret->__buffer.z;
+ ret->z = z;
+ if (*p == 'Z' || *p == 'z') {
+ *z++ = 'Z';
+ p++;
+ *z = 0;
+
+ } else if (*p == '+' || *p == '-') {
+ *z++ = *p++;
+
+ if (!(isdigit(p[0]) && isdigit(p[1])))
+ return -1;
+ *z++ = *p++;
+ *z++ = *p++;
+
+ if (*p == ':') {
+ *z++ = *p++;
+
+ if (!(isdigit(p[0]) && isdigit(p[1])))
+ return -1;
+ *z++ = *p++;
+ *z++ = *p++;
+ }
+
+ *z = 0;
+ }
+ }
+ }
+ if (*p != 0)
+ return -1;
+
+ if (must_parse_time && !ret->hour)
+ return -1;
+
+ return 0;
+}
+
+/* Raw to boolean */
+int toml_rtob(toml_raw_t src, int* ret_)
+{
+ if (!src)
+ return -1;
+ int dummy;
+ int* ret = ret_ ? ret_ : &dummy;
+
+ if (0 == strcmp(src, "true")) {
+ *ret = 1;
+ return 0;
+ }
+ if (0 == strcmp(src, "false")) {
+ *ret = 0;
+ return 0;
+ }
+ return -1;
+}
+
+/* Raw to integer */
+int toml_rtoi(toml_raw_t src, int64_t* ret_)
+{
+ if (!src)
+ return -1;
+
+ char buf[100];
+ char* p = buf;
+ char* q = p + sizeof(buf);
+ const char* s = src;
+ int base = 0;
+ int64_t dummy;
+ int64_t* ret = ret_ ? ret_ : &dummy;
+
+ /* allow +/- */
+ if (s[0] == '+' || s[0] == '-')
+ *p++ = *s++;
+
+ /* disallow +_100 */
+ if (s[0] == '_')
+ return -1;
+
+ /* if 0 ... */
+ if ('0' == s[0]) {
+ switch (s[1]) {
+ case 'x':
+ base = 16;
+ s += 2;
+ break;
+ case 'o':
+ base = 8;
+ s += 2;
+ break;
+ case 'b':
+ base = 2;
+ s += 2;
+ break;
+ case '\0':
+ return *ret = 0, 0;
+ default:
+ /* ensure no other digits after it */
+ if (s[1])
+ return -1;
+ }
+ }
+
+ /* just strip underscores and pass to strtoll */
+ while (*s && p < q) {
+ int ch = *s++;
+ switch (ch) {
+ case '_':
+ // disallow '__'
+ if (s[0] == '_')
+ return -1;
+ continue; /* skip _ */
+ default:
+ break;
+ }
+ *p++ = ch;
+ }
+ if (*s || p == q)
+ return -1;
+
+ /* last char cannot be '_' */
+ if (s[-1] == '_')
+ return -1;
+
+ /* cap with NUL */
+ *p = 0;
+
+ /* Run strtoll on buf to get the integer */
+ char* endp;
+ errno = 0;
+ *ret = strtoll(buf, &endp, base);
+ return (errno || *endp) ? -1 : 0;
+}
+
+int toml_rtod_ex(toml_raw_t src, double* ret_, char* buf, int buflen)
+{
+ if (!src)
+ return -1;
+
+ char* p = buf;
+ char* q = p + buflen;
+ const char* s = src;
+ double dummy;
+ double* ret = ret_ ? ret_ : &dummy;
+
+ /* allow +/- */
+ if (s[0] == '+' || s[0] == '-')
+ *p++ = *s++;
+
+ /* disallow +_1.00 */
+ if (s[0] == '_')
+ return -1;
+
+ /* decimal point, if used, must be surrounded by at least one digit on each
+ * side */
+ {
+ char* dot = strchr(s, '.');
+ if (dot) {
+ if (dot == s || !isdigit(dot[-1]) || !isdigit(dot[1]))
+ return -1;
+ }
+ }
+
+ /* zero must be followed by . or 'e', or NUL */
+ if (s[0] == '0' && s[1] && !strchr("eE.", s[1]))
+ return -1;
+
+ /* just strip underscores and pass to strtod */
+ while (*s && p < q) {
+ int ch = *s++;
+ if (ch == '_') {
+ // disallow '__'
+ if (s[0] == '_')
+ return -1;
+ // disallow last char '_'
+ if (s[0] == 0)
+ return -1;
+ continue; /* skip _ */
+ }
+ *p++ = ch;
+ }
+ if (*s || p == q)
+ return -1; /* reached end of string or buffer is full? */
+
+ /* cap with NUL */
+ *p = 0;
+
+ /* Run strtod on buf to get the value */
+ char* endp;
+ errno = 0;
+ *ret = strtod(buf, &endp);
+ return (errno || *endp) ? -1 : 0;
+}
+
+int toml_rtod(toml_raw_t src, double* ret_)
+{
+ char buf[100];
+ return toml_rtod_ex(src, ret_, buf, sizeof(buf));
+}
+
+int toml_rtos(toml_raw_t src, char** ret)
+{
+ int multiline = 0;
+ const char* sp;
+ const char* sq;
+
+ *ret = 0;
+ if (!src)
+ return -1;
+
+ int qchar = src[0];
+ int srclen = strlen(src);
+ if (!(qchar == '\'' || qchar == '"')) {
+ return -1;
+ }
+
+ // triple quotes?
+ if (qchar == src[1] && qchar == src[2]) {
+ multiline = 1;
+ sp = src + 3;
+ sq = src + srclen - 3;
+ /* last 3 chars in src must be qchar */
+ if (!(sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar))
+ return -1;
+
+ /* skip new line immediate after qchar */
+ if (sp[0] == '\n')
+ sp++;
+ else if (sp[0] == '\r' && sp[1] == '\n')
+ sp += 2;
+
+ } else {
+ sp = src + 1;
+ sq = src + srclen - 1;
+ /* last char in src must be qchar */
+ if (!(sp <= sq && *sq == qchar))
+ return -1;
+ }
+
+ if (qchar == '\'') {
+ *ret = norm_lit_str(sp, sq - sp, multiline, 0, 0);
+ } else {
+ *ret = norm_basic_str(sp, sq - sp, multiline, 0, 0);
+ }
+
+ return *ret ? 0 : -1;
+}
+
+toml_datum_t toml_string_at(const toml_array_t* arr, int idx)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtos(toml_raw_at(arr, idx), &ret.u.s));
+ return ret;
+}
+
+toml_datum_t toml_bool_at(const toml_array_t* arr, int idx)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtob(toml_raw_at(arr, idx), &ret.u.b));
+ return ret;
+}
+
+toml_datum_t toml_int_at(const toml_array_t* arr, int idx)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtoi(toml_raw_at(arr, idx), &ret.u.i));
+ return ret;
+}
+
+toml_datum_t toml_double_at(const toml_array_t* arr, int idx)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtod(toml_raw_at(arr, idx), &ret.u.d));
+ return ret;
+}
+
+toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx)
+{
+ toml_timestamp_t ts;
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtots(toml_raw_at(arr, idx), &ts));
+ if (ret.ok) {
+ ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts)));
+ if (ret.ok) {
+ *ret.u.ts = ts;
+ if (ret.u.ts->year)
+ ret.u.ts->year = &ret.u.ts->__buffer.year;
+ if (ret.u.ts->month)
+ ret.u.ts->month = &ret.u.ts->__buffer.month;
+ if (ret.u.ts->day)
+ ret.u.ts->day = &ret.u.ts->__buffer.day;
+ if (ret.u.ts->hour)
+ ret.u.ts->hour = &ret.u.ts->__buffer.hour;
+ if (ret.u.ts->minute)
+ ret.u.ts->minute = &ret.u.ts->__buffer.minute;
+ if (ret.u.ts->second)
+ ret.u.ts->second = &ret.u.ts->__buffer.second;
+ if (ret.u.ts->millisec)
+ ret.u.ts->millisec = &ret.u.ts->__buffer.millisec;
+ if (ret.u.ts->z)
+ ret.u.ts->z = ret.u.ts->__buffer.z;
+ }
+ }
+ return ret;
+}
+
+toml_datum_t toml_string_in(const toml_table_t* arr, const char* key)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ toml_raw_t raw = toml_raw_in(arr, key);
+ if (raw) {
+ ret.ok = (0 == toml_rtos(raw, &ret.u.s));
+ }
+ return ret;
+}
+
+toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtob(toml_raw_in(arr, key), &ret.u.b));
+ return ret;
+}
+
+toml_datum_t toml_int_in(const toml_table_t* arr, const char* key)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtoi(toml_raw_in(arr, key), &ret.u.i));
+ return ret;
+}
+
+toml_datum_t toml_double_in(const toml_table_t* arr, const char* key)
+{
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtod(toml_raw_in(arr, key), &ret.u.d));
+ return ret;
+}
+
+toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key)
+{
+ toml_timestamp_t ts;
+ toml_datum_t ret;
+ memset(&ret, 0, sizeof(ret));
+ ret.ok = (0 == toml_rtots(toml_raw_in(arr, key), &ts));
+ if (ret.ok) {
+ ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts)));
+ if (ret.ok) {
+ *ret.u.ts = ts;
+ if (ret.u.ts->year)
+ ret.u.ts->year = &ret.u.ts->__buffer.year;
+ if (ret.u.ts->month)
+ ret.u.ts->month = &ret.u.ts->__buffer.month;
+ if (ret.u.ts->day)
+ ret.u.ts->day = &ret.u.ts->__buffer.day;
+ if (ret.u.ts->hour)
+ ret.u.ts->hour = &ret.u.ts->__buffer.hour;
+ if (ret.u.ts->minute)
+ ret.u.ts->minute = &ret.u.ts->__buffer.minute;
+ if (ret.u.ts->second)
+ ret.u.ts->second = &ret.u.ts->__buffer.second;
+ if (ret.u.ts->millisec)
+ ret.u.ts->millisec = &ret.u.ts->__buffer.millisec;
+ if (ret.u.ts->z)
+ ret.u.ts->z = ret.u.ts->__buffer.z;
+ }
+ }
+ return ret;
+}
diff --git a/meshmc/libraries/xz-embedded/CMakeLists.txt b/meshmc/libraries/xz-embedded/CMakeLists.txt
new file mode 100644
index 0000000000..e884a4d46a
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.25)
+project(xz-embedded LANGUAGES C)
+
+option(XZ_BUILD_BCJ "Build xz-embedded with BCJ support (native binary optimization)" OFF)
+option(XZ_BUILD_CRC64 "Build xz-embedded with CRC64 checksum support" ON)
+option(XZ_BUILD_MINIDEC "Build a tiny utility that decompresses xz streams" OFF)
+
+# See include/xz.h for manual feature configuration
+# tweak this list and xz.h to fit your needs
+
+set(XZ_SOURCES
+ src/xz_crc32.c
+ src/xz_crc64.c
+ src/xz_dec_lzma2.c
+ src/xz_dec_stream.c
+# src/xz_dec_bcj.c
+)
+add_library(xz-embedded STATIC ${XZ_SOURCES})
+target_include_directories(xz-embedded PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
+set_property(TARGET xz-embedded PROPERTY C_STANDARD 99)
+
+if(${XZ_BUILD_MINIDEC})
+ add_executable(xzminidec xzminidec.c)
+ target_link_libraries(xzminidec xz-embedded)
+ set_property(TARGET xzminidec PROPERTY C_STANDARD 99)
+endif()
diff --git a/meshmc/libraries/xz-embedded/include/xz.h b/meshmc/libraries/xz-embedded/include/xz.h
new file mode 100644
index 0000000000..7bc022c83e
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/include/xz.h
@@ -0,0 +1,315 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * XZ decompressor
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#ifndef XZ_H
+#define XZ_H
+
+#ifdef __KERNEL__
+#include <linux/stddef.h>
+#include <linux/types.h>
+#else
+#include <stddef.h>
+#include <stdint.h>
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Definitions that determine available features */
+#define XZ_DEC_ANY_CHECK 1
+#define XZ_USE_CRC64 1
+
+// native machine code compression stuff
+/*
+#define XZ_DEC_X86
+#define XZ_DEC_POWERPC
+#define XZ_DEC_IA64
+#define XZ_DEC_ARM
+#define XZ_DEC_ARMTHUMB
+#define XZ_DEC_SPARC
+*/
+
+/* In Linux, this is used to make extern functions static when needed. */
+#ifndef XZ_EXTERN
+#define XZ_EXTERN extern
+#endif
+
+/**
+ * enum xz_mode - Operation mode
+ *
+ * @XZ_SINGLE: Single-call mode. This uses less RAM than
+ * than multi-call modes, because the LZMA2
+ * dictionary doesn't need to be allocated as
+ * part of the decoder state. All required data
+ * structures are allocated at initialization,
+ * so xz_dec_run() cannot return XZ_MEM_ERROR.
+ * @XZ_PREALLOC: Multi-call mode with preallocated LZMA2
+ * dictionary buffer. All data structures are
+ * allocated at initialization, so xz_dec_run()
+ * cannot return XZ_MEM_ERROR.
+ * @XZ_DYNALLOC: Multi-call mode. The LZMA2 dictionary is
+ * allocated once the required size has been
+ * parsed from the stream headers. If the
+ * allocation fails, xz_dec_run() will return
+ * XZ_MEM_ERROR.
+ *
+ * It is possible to enable support only for a subset of the above
+ * modes at compile time by defining XZ_DEC_SINGLE, XZ_DEC_PREALLOC,
+ * or XZ_DEC_DYNALLOC. The xz_dec kernel module is always compiled
+ * with support for all operation modes, but the preboot code may
+ * be built with fewer features to minimize code size.
+ */
+enum xz_mode { XZ_SINGLE, XZ_PREALLOC, XZ_DYNALLOC };
+
+/**
+ * enum xz_ret - Return codes
+ * @XZ_OK: Everything is OK so far. More input or more
+ * output space is required to continue. This
+ * return code is possible only in multi-call mode
+ * (XZ_PREALLOC or XZ_DYNALLOC).
+ * @XZ_STREAM_END: Operation finished successfully.
+ * @XZ_UNSUPPORTED_CHECK: Integrity check type is not supported. Decoding
+ * is still possible in multi-call mode by simply
+ * calling xz_dec_run() again.
+ * Note that this return value is used only if
+ * XZ_DEC_ANY_CHECK was defined at build time,
+ * which is not used in the kernel. Unsupported
+ * check types return XZ_OPTIONS_ERROR if
+ * XZ_DEC_ANY_CHECK was not defined at build time.
+ * @XZ_MEM_ERROR: Allocating memory failed. This return code is
+ * possible only if the decoder was initialized
+ * with XZ_DYNALLOC. The amount of memory that was
+ * tried to be allocated was no more than the
+ * dict_max argument given to xz_dec_init().
+ * @XZ_MEMLIMIT_ERROR: A bigger LZMA2 dictionary would be needed than
+ * allowed by the dict_max argument given to
+ * xz_dec_init(). This return value is possible
+ * only in multi-call mode (XZ_PREALLOC or
+ * XZ_DYNALLOC); the single-call mode (XZ_SINGLE)
+ * ignores the dict_max argument.
+ * @XZ_FORMAT_ERROR: File format was not recognized (wrong magic
+ * bytes).
+ * @XZ_OPTIONS_ERROR: This implementation doesn't support the requested
+ * compression options. In the decoder this means
+ * that the header CRC32 matches, but the header
+ * itself specifies something that we don't support.
+ * @XZ_DATA_ERROR: Compressed data is corrupt.
+ * @XZ_BUF_ERROR: Cannot make any progress. Details are slightly
+ * different between multi-call and single-call
+ * mode; more information below.
+ *
+ * In multi-call mode, XZ_BUF_ERROR is returned when two consecutive calls
+ * to XZ code cannot consume any input and cannot produce any new output.
+ * This happens when there is no new input available, or the output buffer
+ * is full while at least one output byte is still pending. Assuming your
+ * code is not buggy, you can get this error only when decoding a compressed
+ * stream that is truncated or otherwise corrupt.
+ *
+ * In single-call mode, XZ_BUF_ERROR is returned only when the output buffer
+ * is too small or the compressed input is corrupt in a way that makes the
+ * decoder produce more output than the caller expected. When it is
+ * (relatively) clear that the compressed input is truncated, XZ_DATA_ERROR
+ * is used instead of XZ_BUF_ERROR.
+ */
+enum xz_ret {
+ XZ_OK,
+ XZ_STREAM_END,
+ XZ_UNSUPPORTED_CHECK,
+ XZ_MEM_ERROR,
+ XZ_MEMLIMIT_ERROR,
+ XZ_FORMAT_ERROR,
+ XZ_OPTIONS_ERROR,
+ XZ_DATA_ERROR,
+ XZ_BUF_ERROR
+};
+
+/**
+ * struct xz_buf - Passing input and output buffers to XZ code
+ * @in: Beginning of the input buffer. This may be NULL if and only
+ * if in_pos is equal to in_size.
+ * @in_pos: Current position in the input buffer. This must not exceed
+ * in_size.
+ * @in_size: Size of the input buffer
+ * @out: Beginning of the output buffer. This may be NULL if and only
+ * if out_pos is equal to out_size.
+ * @out_pos: Current position in the output buffer. This must not exceed
+ * out_size.
+ * @out_size: Size of the output buffer
+ *
+ * Only the contents of the output buffer from out[out_pos] onward, and
+ * the variables in_pos and out_pos are modified by the XZ code.
+ */
+struct xz_buf {
+ const uint8_t* in;
+ size_t in_pos;
+ size_t in_size;
+
+ uint8_t* out;
+ size_t out_pos;
+ size_t out_size;
+};
+
+/**
+ * struct xz_dec - Opaque type to hold the XZ decoder state
+ */
+struct xz_dec;
+
+/**
+ * xz_dec_init() - Allocate and initialize a XZ decoder state
+ * @mode: Operation mode
+ * @dict_max: Maximum size of the LZMA2 dictionary (history buffer) for
+ * multi-call decoding. This is ignored in single-call mode
+ * (mode == XZ_SINGLE). LZMA2 dictionary is always 2^n bytes
+ * or 2^n + 2^(n-1) bytes (the latter sizes are less common
+ * in practice), so other values for dict_max don't make sense.
+ * In the kernel, dictionary sizes of 64 KiB, 128 KiB, 256 KiB,
+ * 512 KiB, and 1 MiB are probably the only reasonable values,
+ * except for kernel and initramfs images where a bigger
+ * dictionary can be fine and useful.
+ *
+ * Single-call mode (XZ_SINGLE): xz_dec_run() decodes the whole stream at
+ * once. The caller must provide enough output space or the decoding will
+ * fail. The output space is used as the dictionary buffer, which is why
+ * there is no need to allocate the dictionary as part of the decoder's
+ * internal state.
+ *
+ * Because the output buffer is used as the workspace, streams encoded using
+ * a big dictionary are not a problem in single-call mode. It is enough that
+ * the output buffer is big enough to hold the actual uncompressed data; it
+ * can be smaller than the dictionary size stored in the stream headers.
+ *
+ * Multi-call mode with preallocated dictionary (XZ_PREALLOC): dict_max bytes
+ * of memory is preallocated for the LZMA2 dictionary. This way there is no
+ * risk that xz_dec_run() could run out of memory, since xz_dec_run() will
+ * never allocate any memory. Instead, if the preallocated dictionary is too
+ * small for decoding the given input stream, xz_dec_run() will return
+ * XZ_MEMLIMIT_ERROR. Thus, it is important to know what kind of data will be
+ * decoded to avoid allocating excessive amount of memory for the dictionary.
+ *
+ * Multi-call mode with dynamically allocated dictionary (XZ_DYNALLOC):
+ * dict_max specifies the maximum allowed dictionary size that xz_dec_run()
+ * may allocate once it has parsed the dictionary size from the stream
+ * headers. This way excessive allocations can be avoided while still
+ * limiting the maximum memory usage to a sane value to prevent running the
+ * system out of memory when decompressing streams from untrusted sources.
+ *
+ * On success, xz_dec_init() returns a pointer to struct xz_dec, which is
+ * ready to be used with xz_dec_run(). If memory allocation fails,
+ * xz_dec_init() returns NULL.
+ */
+XZ_EXTERN struct xz_dec* xz_dec_init(enum xz_mode mode, uint32_t dict_max);
+
+/**
+ * xz_dec_run() - Run the XZ decoder
+ * @s: Decoder state allocated using xz_dec_init()
+ * @b: Input and output buffers
+ *
+ * The possible return values depend on build options and operation mode.
+ * See enum xz_ret for details.
+ *
+ * Note that if an error occurs in single-call mode (return value is not
+ * XZ_STREAM_END), b->in_pos and b->out_pos are not modified and the
+ * contents of the output buffer from b->out[b->out_pos] onward are
+ * undefined. This is true even after XZ_BUF_ERROR, because with some filter
+ * chains, there may be a second pass over the output buffer, and this pass
+ * cannot be properly done if the output buffer is truncated. Thus, you
+ * cannot give the single-call decoder a too small buffer and then expect to
+ * get that amount valid data from the beginning of the stream. You must use
+ * the multi-call decoder if you don't want to uncompress the whole stream.
+ */
+XZ_EXTERN enum xz_ret xz_dec_run(struct xz_dec* s, struct xz_buf* b);
+
+/**
+ * xz_dec_reset() - Reset an already allocated decoder state
+ * @s: Decoder state allocated using xz_dec_init()
+ *
+ * This function can be used to reset the multi-call decoder state without
+ * freeing and reallocating memory with xz_dec_end() and xz_dec_init().
+ *
+ * In single-call mode, xz_dec_reset() is always called in the beginning of
+ * xz_dec_run(). Thus, explicit call to xz_dec_reset() is useful only in
+ * multi-call mode.
+ */
+XZ_EXTERN void xz_dec_reset(struct xz_dec* s);
+
+/**
+ * xz_dec_end() - Free the memory allocated for the decoder state
+ * @s: Decoder state allocated using xz_dec_init(). If s is NULL,
+ * this function does nothing.
+ */
+XZ_EXTERN void xz_dec_end(struct xz_dec* s);
+
+/*
+ * Standalone build (userspace build or in-kernel build for boot time use)
+ * needs a CRC32 implementation. For normal in-kernel use, kernel's own
+ * CRC32 module is used instead, and users of this module don't need to
+ * care about the functions below.
+ */
+#ifndef XZ_INTERNAL_CRC32
+#ifdef __KERNEL__
+#define XZ_INTERNAL_CRC32 0
+#else
+#define XZ_INTERNAL_CRC32 1
+#endif
+#endif
+
+/*
+ * If CRC64 support has been enabled with XZ_USE_CRC64, a CRC64
+ * implementation is needed too.
+ */
+#ifndef XZ_USE_CRC64
+#undef XZ_INTERNAL_CRC64
+#define XZ_INTERNAL_CRC64 0
+#endif
+#ifndef XZ_INTERNAL_CRC64
+#ifdef __KERNEL__
+#error Using CRC64 in the kernel has not been implemented.
+#else
+#define XZ_INTERNAL_CRC64 1
+#endif
+#endif
+
+#if XZ_INTERNAL_CRC32
+/*
+ * This must be called before any other xz_* function to initialize
+ * the CRC32 lookup table.
+ */
+XZ_EXTERN void xz_crc32_init(void);
+
+/*
+ * Update CRC32 value using the polynomial from IEEE-802.3. To start a new
+ * calculation, the third argument must be zero. To continue the calculation,
+ * the previously returned value is passed as the third argument.
+ */
+XZ_EXTERN uint32_t xz_crc32(const uint8_t* buf, size_t size, uint32_t crc);
+#endif
+
+#if XZ_INTERNAL_CRC64
+/*
+ * This must be called before any other xz_* function (except xz_crc32_init())
+ * to initialize the CRC64 lookup table.
+ */
+XZ_EXTERN void xz_crc64_init(void);
+
+/*
+ * Update CRC64 value using the polynomial from ECMA-182. To start a new
+ * calculation, the third argument must be zero. To continue the calculation,
+ * the previously returned value is passed as the third argument.
+ */
+XZ_EXTERN uint64_t xz_crc64(const uint8_t* buf, size_t size, uint64_t crc);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/src/xz_config.h b/meshmc/libraries/xz-embedded/src/xz_config.h
new file mode 100644
index 0000000000..7f75dedd02
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_config.h
@@ -0,0 +1,120 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * Private includes and definitions for userspace use of XZ Embedded
+ *
+ * Author: Lasse Collin <lasse.collin@tukaani.org>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#ifndef XZ_CONFIG_H
+#define XZ_CONFIG_H
+
+/* Uncomment to enable CRC64 support. */
+/* #define XZ_USE_CRC64 */
+
+/* Uncomment as needed to enable BCJ filter decoders. */
+/* #define XZ_DEC_X86 */
+/* #define XZ_DEC_POWERPC */
+/* #define XZ_DEC_IA64 */
+/* #define XZ_DEC_ARM */
+/* #define XZ_DEC_ARMTHUMB */
+/* #define XZ_DEC_SPARC */
+
+/*
+ * MSVC doesn't support modern C but XZ Embedded is mostly C89
+ * so these are enough.
+ */
+#ifdef _MSC_VER
+typedef unsigned char bool;
+#define true 1
+#define false 0
+#define inline __inline
+#else
+#include <stdbool.h>
+#endif
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "xz.h"
+
+#define kmalloc(size, flags) malloc(size)
+#define kfree(ptr) free(ptr)
+#define vmalloc(size) malloc(size)
+#define vfree(ptr) free(ptr)
+
+#define memeq(a, b, size) (memcmp(a, b, size) == 0)
+#define memzero(buf, size) memset(buf, 0, size)
+
+#ifndef min
+#define min(x, y) ((x) < (y) ? (x) : (y))
+#endif
+#define min_t(type, x, y) min(x, y)
+
+/*
+ * Some functions have been marked with __always_inline to keep the
+ * performance reasonable even when the compiler is optimizing for
+ * small code size. You may be able to save a few bytes by #defining
+ * __always_inline to plain inline, but don't complain if the code
+ * becomes slow.
+ *
+ * NOTE: System headers on GNU/Linux may #define this macro already,
+ * so if you want to change it, you need to #undef it first.
+ */
+#ifndef __always_inline
+#ifdef __GNUC__
+#define __always_inline inline __attribute__((__always_inline__))
+#else
+#define __always_inline inline
+#endif
+#endif
+
+/* Inline functions to access unaligned unsigned 32-bit integers */
+#ifndef get_unaligned_le32
+static inline uint32_t get_unaligned_le32(const uint8_t* buf)
+{
+ return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) |
+ ((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
+}
+#endif
+
+#ifndef get_unaligned_be32
+static inline uint32_t get_unaligned_be32(const uint8_t* buf)
+{
+ return (uint32_t)(buf[0] << 24) | ((uint32_t)buf[1] << 16) |
+ ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];
+}
+#endif
+
+#ifndef put_unaligned_le32
+static inline void put_unaligned_le32(uint32_t val, uint8_t* buf)
+{
+ buf[0] = (uint8_t)val;
+ buf[1] = (uint8_t)(val >> 8);
+ buf[2] = (uint8_t)(val >> 16);
+ buf[3] = (uint8_t)(val >> 24);
+}
+#endif
+
+#ifndef put_unaligned_be32
+static inline void put_unaligned_be32(uint32_t val, uint8_t* buf)
+{
+ buf[0] = (uint8_t)(val >> 24);
+ buf[1] = (uint8_t)(val >> 16);
+ buf[2] = (uint8_t)(val >> 8);
+ buf[3] = (uint8_t)val;
+}
+#endif
+
+/*
+ * Use get_unaligned_le32() also for aligned access for simplicity. On
+ * little endian systems, #define get_le32(ptr) (*(const uint32_t *)(ptr))
+ * could save a few bytes in code size.
+ */
+#ifndef get_le32
+#define get_le32 get_unaligned_le32
+#endif
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/src/xz_crc32.c b/meshmc/libraries/xz-embedded/src/xz_crc32.c
new file mode 100644
index 0000000000..a512798c78
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_crc32.c
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * CRC32 using the polynomial from IEEE-802.3
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+/*
+ * This is not the fastest implementation, but it is pretty compact.
+ * The fastest versions of xz_crc32() on modern CPUs without hardware
+ * accelerated CRC instruction are 3-5 times as fast as this version,
+ * but they are bigger and use more memory for the lookup table.
+ */
+
+#include "xz_private.h"
+
+/*
+ * STATIC_RW_DATA is used in the pre-boot environment on some architectures.
+ * See <linux/decompress/mm.h> for details.
+ */
+#ifndef STATIC_RW_DATA
+#define STATIC_RW_DATA static
+#endif
+
+STATIC_RW_DATA uint32_t xz_crc32_table[256];
+
+XZ_EXTERN void xz_crc32_init(void)
+{
+ const uint32_t poly = 0xEDB88320;
+
+ uint32_t i;
+ uint32_t j;
+ uint32_t r;
+
+ for (i = 0; i < 256; ++i) {
+ r = i;
+ for (j = 0; j < 8; ++j)
+ r = (r >> 1) ^ (poly & ~((r & 1) - 1));
+
+ xz_crc32_table[i] = r;
+ }
+
+ return;
+}
+
+XZ_EXTERN uint32_t xz_crc32(const uint8_t* buf, size_t size, uint32_t crc)
+{
+ crc = ~crc;
+
+ while (size != 0) {
+ crc = xz_crc32_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8);
+ --size;
+ }
+
+ return ~crc;
+}
diff --git a/meshmc/libraries/xz-embedded/src/xz_crc64.c b/meshmc/libraries/xz-embedded/src/xz_crc64.c
new file mode 100644
index 0000000000..9360d9184d
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_crc64.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * CRC64 using the polynomial from ECMA-182
+ *
+ * This file is similar to xz_crc32.c. See the comments there.
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#include "xz_private.h"
+
+#ifndef STATIC_RW_DATA
+#define STATIC_RW_DATA static
+#endif
+
+STATIC_RW_DATA uint64_t xz_crc64_table[256];
+
+XZ_EXTERN void xz_crc64_init(void)
+{
+ const uint64_t poly = 0xC96C5795D7870F42;
+
+ uint32_t i;
+ uint32_t j;
+ uint64_t r;
+
+ for (i = 0; i < 256; ++i) {
+ r = i;
+ for (j = 0; j < 8; ++j)
+ r = (r >> 1) ^ (poly & ~((r & 1) - 1));
+
+ xz_crc64_table[i] = r;
+ }
+
+ return;
+}
+
+XZ_EXTERN uint64_t xz_crc64(const uint8_t* buf, size_t size, uint64_t crc)
+{
+ crc = ~crc;
+
+ while (size != 0) {
+ crc = xz_crc64_table[*buf++ ^ (crc & 0xFF)] ^ (crc >> 8);
+ --size;
+ }
+
+ return ~crc;
+}
diff --git a/meshmc/libraries/xz-embedded/src/xz_dec_bcj.c b/meshmc/libraries/xz-embedded/src/xz_dec_bcj.c
new file mode 100644
index 0000000000..622536de65
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_dec_bcj.c
@@ -0,0 +1,565 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * Branch/Call/Jump (BCJ) filter decoders
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#include "xz_private.h"
+
+/*
+ * The rest of the file is inside this ifdef. It makes things a little more
+ * convenient when building without support for any BCJ filters.
+ */
+#ifdef XZ_DEC_BCJ
+
+struct xz_dec_bcj {
+ /* Type of the BCJ filter being used */
+ enum {
+ BCJ_X86 = 4, /* x86 or x86-64 */
+ BCJ_POWERPC = 5, /* Big endian only */
+ BCJ_IA64 = 6, /* Big or little endian */
+ BCJ_ARM = 7, /* Little endian only */
+ BCJ_ARMTHUMB = 8, /* Little endian only */
+ BCJ_SPARC = 9 /* Big or little endian */
+ } type;
+
+ /*
+ * Return value of the next filter in the chain. We need to preserve
+ * this information across calls, because we must not call the next
+ * filter anymore once it has returned XZ_STREAM_END.
+ */
+ enum xz_ret ret;
+
+ /* True if we are operating in single-call mode. */
+ bool single_call;
+
+ /*
+ * Absolute position relative to the beginning of the uncompressed
+ * data (in a single .xz Block). We care only about the lowest 32
+ * bits so this doesn't need to be uint64_t even with big files.
+ */
+ uint32_t pos;
+
+ /* x86 filter state */
+ uint32_t x86_prev_mask;
+
+ /* Temporary space to hold the variables from struct xz_buf */
+ uint8_t* out;
+ size_t out_pos;
+ size_t out_size;
+
+ struct {
+ /* Amount of already filtered data in the beginning of buf */
+ size_t filtered;
+
+ /* Total amount of data currently stored in buf */
+ size_t size;
+
+ /*
+ * Buffer to hold a mix of filtered and unfiltered data. This
+ * needs to be big enough to hold Alignment + 2 * Look-ahead:
+ *
+ * Type Alignment Look-ahead
+ * x86 1 4
+ * PowerPC 4 0
+ * IA-64 16 0
+ * ARM 4 0
+ * ARM-Thumb 2 2
+ * SPARC 4 0
+ */
+ uint8_t buf[16];
+ } temp;
+};
+
+#ifdef XZ_DEC_X86
+/*
+ * This is used to test the most significant byte of a memory address
+ * in an x86 instruction.
+ */
+static inline int bcj_x86_test_msbyte(uint8_t b)
+{
+ return b == 0x00 || b == 0xFF;
+}
+
+static size_t bcj_x86(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ static const bool mask_to_allowed_status[8] = {true, true, true, false,
+ true, false, false, false};
+
+ static const uint8_t mask_to_bit_num[8] = {0, 1, 2, 2, 3, 3, 3, 3};
+
+ size_t i;
+ size_t prev_pos = (size_t)-1;
+ uint32_t prev_mask = s->x86_prev_mask;
+ uint32_t src;
+ uint32_t dest;
+ uint32_t j;
+ uint8_t b;
+
+ if (size <= 4)
+ return 0;
+
+ size -= 4;
+ for (i = 0; i < size; ++i) {
+ if ((buf[i] & 0xFE) != 0xE8)
+ continue;
+
+ prev_pos = i - prev_pos;
+ if (prev_pos > 3) {
+ prev_mask = 0;
+ } else {
+ prev_mask = (prev_mask << (prev_pos - 1)) & 7;
+ if (prev_mask != 0) {
+ b = buf[i + 4 - mask_to_bit_num[prev_mask]];
+ if (!mask_to_allowed_status[prev_mask] ||
+ bcj_x86_test_msbyte(b)) {
+ prev_pos = i;
+ prev_mask = (prev_mask << 1) | 1;
+ continue;
+ }
+ }
+ }
+
+ prev_pos = i;
+
+ if (bcj_x86_test_msbyte(buf[i + 4])) {
+ src = get_unaligned_le32(buf + i + 1);
+ while (true) {
+ dest = src - (s->pos + (uint32_t)i + 5);
+ if (prev_mask == 0)
+ break;
+
+ j = mask_to_bit_num[prev_mask] * 8;
+ b = (uint8_t)(dest >> (24 - j));
+ if (!bcj_x86_test_msbyte(b))
+ break;
+
+ src = dest ^ (((uint32_t)1 << (32 - j)) - 1);
+ }
+
+ dest &= 0x01FFFFFF;
+ dest |= (uint32_t)0 - (dest & 0x01000000);
+ put_unaligned_le32(dest, buf + i + 1);
+ i += 4;
+ } else {
+ prev_mask = (prev_mask << 1) | 1;
+ }
+ }
+
+ prev_pos = i - prev_pos;
+ s->x86_prev_mask = prev_pos > 3 ? 0 : prev_mask << (prev_pos - 1);
+ return i;
+}
+#endif
+
+#ifdef XZ_DEC_POWERPC
+static size_t bcj_powerpc(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ size_t i;
+ uint32_t instr;
+
+ for (i = 0; i + 4 <= size; i += 4) {
+ instr = get_unaligned_be32(buf + i);
+ if ((instr & 0xFC000003) == 0x48000001) {
+ instr &= 0x03FFFFFC;
+ instr -= s->pos + (uint32_t)i;
+ instr &= 0x03FFFFFC;
+ instr |= 0x48000001;
+ put_unaligned_be32(instr, buf + i);
+ }
+ }
+
+ return i;
+}
+#endif
+
+#ifdef XZ_DEC_IA64
+static size_t bcj_ia64(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ static const uint8_t branch_table[32] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 4, 4, 6, 6, 0, 0,
+ 7, 7, 4, 4, 0, 0, 4, 4, 0, 0};
+
+ /*
+ * The local variables take a little bit stack space, but it's less
+ * than what LZMA2 decoder takes, so it doesn't make sense to reduce
+ * stack usage here without doing that for the LZMA2 decoder too.
+ */
+
+ /* Loop counters */
+ size_t i;
+ size_t j;
+
+ /* Instruction slot (0, 1, or 2) in the 128-bit instruction word */
+ uint32_t slot;
+
+ /* Bitwise offset of the instruction indicated by slot */
+ uint32_t bit_pos;
+
+ /* bit_pos split into byte and bit parts */
+ uint32_t byte_pos;
+ uint32_t bit_res;
+
+ /* Address part of an instruction */
+ uint32_t addr;
+
+ /* Mask used to detect which instructions to convert */
+ uint32_t mask;
+
+ /* 41-bit instruction stored somewhere in the lowest 48 bits */
+ uint64_t instr;
+
+ /* Instruction normalized with bit_res for easier manipulation */
+ uint64_t norm;
+
+ for (i = 0; i + 16 <= size; i += 16) {
+ mask = branch_table[buf[i] & 0x1F];
+ for (slot = 0, bit_pos = 5; slot < 3; ++slot, bit_pos += 41) {
+ if (((mask >> slot) & 1) == 0)
+ continue;
+
+ byte_pos = bit_pos >> 3;
+ bit_res = bit_pos & 7;
+ instr = 0;
+ for (j = 0; j < 6; ++j)
+ instr |= (uint64_t)(buf[i + j + byte_pos]) << (8 * j);
+
+ norm = instr >> bit_res;
+
+ if (((norm >> 37) & 0x0F) == 0x05 && ((norm >> 9) & 0x07) == 0) {
+ addr = (norm >> 13) & 0x0FFFFF;
+ addr |= ((uint32_t)(norm >> 36) & 1) << 20;
+ addr <<= 4;
+ addr -= s->pos + (uint32_t)i;
+ addr >>= 4;
+
+ norm &= ~((uint64_t)0x8FFFFF << 13);
+ norm |= (uint64_t)(addr & 0x0FFFFF) << 13;
+ norm |= (uint64_t)(addr & 0x100000) << (36 - 20);
+
+ instr &= (1 << bit_res) - 1;
+ instr |= norm << bit_res;
+
+ for (j = 0; j < 6; j++)
+ buf[i + j + byte_pos] = (uint8_t)(instr >> (8 * j));
+ }
+ }
+ }
+
+ return i;
+}
+#endif
+
+#ifdef XZ_DEC_ARM
+static size_t bcj_arm(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ size_t i;
+ uint32_t addr;
+
+ for (i = 0; i + 4 <= size; i += 4) {
+ if (buf[i + 3] == 0xEB) {
+ addr = (uint32_t)buf[i] | ((uint32_t)buf[i + 1] << 8) |
+ ((uint32_t)buf[i + 2] << 16);
+ addr <<= 2;
+ addr -= s->pos + (uint32_t)i + 8;
+ addr >>= 2;
+ buf[i] = (uint8_t)addr;
+ buf[i + 1] = (uint8_t)(addr >> 8);
+ buf[i + 2] = (uint8_t)(addr >> 16);
+ }
+ }
+
+ return i;
+}
+#endif
+
+#ifdef XZ_DEC_ARMTHUMB
+static size_t bcj_armthumb(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ size_t i;
+ uint32_t addr;
+
+ for (i = 0; i + 4 <= size; i += 2) {
+ if ((buf[i + 1] & 0xF8) == 0xF0 && (buf[i + 3] & 0xF8) == 0xF8) {
+ addr = (((uint32_t)buf[i + 1] & 0x07) << 19) |
+ ((uint32_t)buf[i] << 11) |
+ (((uint32_t)buf[i + 3] & 0x07) << 8) | (uint32_t)buf[i + 2];
+ addr <<= 1;
+ addr -= s->pos + (uint32_t)i + 4;
+ addr >>= 1;
+ buf[i + 1] = (uint8_t)(0xF0 | ((addr >> 19) & 0x07));
+ buf[i] = (uint8_t)(addr >> 11);
+ buf[i + 3] = (uint8_t)(0xF8 | ((addr >> 8) & 0x07));
+ buf[i + 2] = (uint8_t)addr;
+ i += 2;
+ }
+ }
+
+ return i;
+}
+#endif
+
+#ifdef XZ_DEC_SPARC
+static size_t bcj_sparc(struct xz_dec_bcj* s, uint8_t* buf, size_t size)
+{
+ size_t i;
+ uint32_t instr;
+
+ for (i = 0; i + 4 <= size; i += 4) {
+ instr = get_unaligned_be32(buf + i);
+ if ((instr >> 22) == 0x100 || (instr >> 22) == 0x1FF) {
+ instr <<= 2;
+ instr -= s->pos + (uint32_t)i;
+ instr >>= 2;
+ instr = ((uint32_t)0x40000000 - (instr & 0x400000)) | 0x40000000 |
+ (instr & 0x3FFFFF);
+ put_unaligned_be32(instr, buf + i);
+ }
+ }
+
+ return i;
+}
+#endif
+
+/*
+ * Apply the selected BCJ filter. Update *pos and s->pos to match the amount
+ * of data that got filtered.
+ *
+ * NOTE: This is implemented as a switch statement to avoid using function
+ * pointers, which could be problematic in the kernel boot code, which must
+ * avoid pointers to static data (at least on x86).
+ */
+static void bcj_apply(struct xz_dec_bcj* s, uint8_t* buf, size_t* pos,
+ size_t size)
+{
+ size_t filtered;
+
+ buf += *pos;
+ size -= *pos;
+
+ switch (s->type) {
+#ifdef XZ_DEC_X86
+ case BCJ_X86:
+ filtered = bcj_x86(s, buf, size);
+ break;
+#endif
+#ifdef XZ_DEC_POWERPC
+ case BCJ_POWERPC:
+ filtered = bcj_powerpc(s, buf, size);
+ break;
+#endif
+#ifdef XZ_DEC_IA64
+ case BCJ_IA64:
+ filtered = bcj_ia64(s, buf, size);
+ break;
+#endif
+#ifdef XZ_DEC_ARM
+ case BCJ_ARM:
+ filtered = bcj_arm(s, buf, size);
+ break;
+#endif
+#ifdef XZ_DEC_ARMTHUMB
+ case BCJ_ARMTHUMB:
+ filtered = bcj_armthumb(s, buf, size);
+ break;
+#endif
+#ifdef XZ_DEC_SPARC
+ case BCJ_SPARC:
+ filtered = bcj_sparc(s, buf, size);
+ break;
+#endif
+ default:
+ /* Never reached but silence compiler warnings. */
+ filtered = 0;
+ break;
+ }
+
+ *pos += filtered;
+ s->pos += filtered;
+}
+
+/*
+ * Flush pending filtered data from temp to the output buffer.
+ * Move the remaining mixture of possibly filtered and unfiltered
+ * data to the beginning of temp.
+ */
+static void bcj_flush(struct xz_dec_bcj* s, struct xz_buf* b)
+{
+ size_t copy_size;
+
+ copy_size = min_t(size_t, s->temp.filtered, b->out_size - b->out_pos);
+ memcpy(b->out + b->out_pos, s->temp.buf, copy_size);
+ b->out_pos += copy_size;
+
+ s->temp.filtered -= copy_size;
+ s->temp.size -= copy_size;
+ memmove(s->temp.buf, s->temp.buf + copy_size, s->temp.size);
+}
+
+/*
+ * The BCJ filter functions are primitive in sense that they process the
+ * data in chunks of 1-16 bytes. To hide this issue, this function does
+ * some buffering.
+ */
+XZ_EXTERN enum xz_ret xz_dec_bcj_run(struct xz_dec_bcj* s,
+ struct xz_dec_lzma2* lzma2,
+ struct xz_buf* b)
+{
+ size_t out_start;
+
+ /*
+ * Flush pending already filtered data to the output buffer. Return
+ * immediatelly if we couldn't flush everything, or if the next
+ * filter in the chain had already returned XZ_STREAM_END.
+ */
+ if (s->temp.filtered > 0) {
+ bcj_flush(s, b);
+ if (s->temp.filtered > 0)
+ return XZ_OK;
+
+ if (s->ret == XZ_STREAM_END)
+ return XZ_STREAM_END;
+ }
+
+ /*
+ * If we have more output space than what is currently pending in
+ * temp, copy the unfiltered data from temp to the output buffer
+ * and try to fill the output buffer by decoding more data from the
+ * next filter in the chain. Apply the BCJ filter on the new data
+ * in the output buffer. If everything cannot be filtered, copy it
+ * to temp and rewind the output buffer position accordingly.
+ *
+ * This needs to be always run when temp.size == 0 to handle a special
+ * case where the output buffer is full and the next filter has no
+ * more output coming but hasn't returned XZ_STREAM_END yet.
+ */
+ if (s->temp.size < b->out_size - b->out_pos || s->temp.size == 0) {
+ out_start = b->out_pos;
+ memcpy(b->out + b->out_pos, s->temp.buf, s->temp.size);
+ b->out_pos += s->temp.size;
+
+ s->ret = xz_dec_lzma2_run(lzma2, b);
+ if (s->ret != XZ_STREAM_END && (s->ret != XZ_OK || s->single_call))
+ return s->ret;
+
+ bcj_apply(s, b->out, &out_start, b->out_pos);
+
+ /*
+ * As an exception, if the next filter returned XZ_STREAM_END,
+ * we can do that too, since the last few bytes that remain
+ * unfiltered are meant to remain unfiltered.
+ */
+ if (s->ret == XZ_STREAM_END)
+ return XZ_STREAM_END;
+
+ s->temp.size = b->out_pos - out_start;
+ b->out_pos -= s->temp.size;
+ memcpy(s->temp.buf, b->out + b->out_pos, s->temp.size);
+
+ /*
+ * If there wasn't enough input to the next filter to fill
+ * the output buffer with unfiltered data, there's no point
+ * to try decoding more data to temp.
+ */
+ if (b->out_pos + s->temp.size < b->out_size)
+ return XZ_OK;
+ }
+
+ /*
+ * We have unfiltered data in temp. If the output buffer isn't full
+ * yet, try to fill the temp buffer by decoding more data from the
+ * next filter. Apply the BCJ filter on temp. Then we hopefully can
+ * fill the actual output buffer by copying filtered data from temp.
+ * A mix of filtered and unfiltered data may be left in temp; it will
+ * be taken care on the next call to this function.
+ */
+ if (b->out_pos < b->out_size) {
+ /* Make b->out{,_pos,_size} temporarily point to s->temp. */
+ s->out = b->out;
+ s->out_pos = b->out_pos;
+ s->out_size = b->out_size;
+ b->out = s->temp.buf;
+ b->out_pos = s->temp.size;
+ b->out_size = sizeof(s->temp.buf);
+
+ s->ret = xz_dec_lzma2_run(lzma2, b);
+
+ s->temp.size = b->out_pos;
+ b->out = s->out;
+ b->out_pos = s->out_pos;
+ b->out_size = s->out_size;
+
+ if (s->ret != XZ_OK && s->ret != XZ_STREAM_END)
+ return s->ret;
+
+ bcj_apply(s, s->temp.buf, &s->temp.filtered, s->temp.size);
+
+ /*
+ * If the next filter returned XZ_STREAM_END, we mark that
+ * everything is filtered, since the last unfiltered bytes
+ * of the stream are meant to be left as is.
+ */
+ if (s->ret == XZ_STREAM_END)
+ s->temp.filtered = s->temp.size;
+
+ bcj_flush(s, b);
+ if (s->temp.filtered > 0)
+ return XZ_OK;
+ }
+
+ return s->ret;
+}
+
+XZ_EXTERN struct xz_dec_bcj* xz_dec_bcj_create(bool single_call)
+{
+ struct xz_dec_bcj* s = kmalloc(sizeof(*s), GFP_KERNEL);
+ if (s != NULL)
+ s->single_call = single_call;
+
+ return s;
+}
+
+XZ_EXTERN enum xz_ret xz_dec_bcj_reset(struct xz_dec_bcj* s, uint8_t id)
+{
+ switch (id) {
+#ifdef XZ_DEC_X86
+ case BCJ_X86:
+#endif
+#ifdef XZ_DEC_POWERPC
+ case BCJ_POWERPC:
+#endif
+#ifdef XZ_DEC_IA64
+ case BCJ_IA64:
+#endif
+#ifdef XZ_DEC_ARM
+ case BCJ_ARM:
+#endif
+#ifdef XZ_DEC_ARMTHUMB
+ case BCJ_ARMTHUMB:
+#endif
+#ifdef XZ_DEC_SPARC
+ case BCJ_SPARC:
+#endif
+ break;
+
+ default:
+ /* Unsupported Filter ID */
+ return XZ_OPTIONS_ERROR;
+ }
+
+ s->type = id;
+ s->ret = XZ_OK;
+ s->pos = 0;
+ s->x86_prev_mask = 0;
+ s->temp.filtered = 0;
+ s->temp.size = 0;
+
+ return XZ_OK;
+}
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/src/xz_dec_lzma2.c b/meshmc/libraries/xz-embedded/src/xz_dec_lzma2.c
new file mode 100644
index 0000000000..421c05838e
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_dec_lzma2.c
@@ -0,0 +1,1149 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * LZMA2 decoder
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#include "xz_private.h"
+#include "xz_lzma2.h"
+
+/*
+ * Range decoder initialization eats the first five bytes of each LZMA chunk.
+ */
+#define RC_INIT_BYTES 5
+
+/*
+ * Minimum number of usable input buffer to safely decode one LZMA symbol.
+ * The worst case is that we decode 22 bits using probabilities and 26
+ * direct bits. This may decode at maximum of 20 bytes of input. However,
+ * lzma_main() does an extra normalization before returning, thus we
+ * need to put 21 here.
+ */
+#define LZMA_IN_REQUIRED 21
+
+/*
+ * Dictionary (history buffer)
+ *
+ * These are always true:
+ * start <= pos <= full <= end
+ * pos <= limit <= end
+ *
+ * In multi-call mode, also these are true:
+ * end == size
+ * size <= size_max
+ * allocated <= size
+ *
+ * Most of these variables are size_t to support single-call mode,
+ * in which the dictionary variables address the actual output
+ * buffer directly.
+ */
+struct dictionary {
+ /* Beginning of the history buffer */
+ uint8_t* buf;
+
+ /* Old position in buf (before decoding more data) */
+ size_t start;
+
+ /* Position in buf */
+ size_t pos;
+
+ /*
+ * How full dictionary is. This is used to detect corrupt input that
+ * would read beyond the beginning of the uncompressed stream.
+ */
+ size_t full;
+
+ /* Write limit; we don't write to buf[limit] or later bytes. */
+ size_t limit;
+
+ /*
+ * End of the dictionary buffer. In multi-call mode, this is
+ * the same as the dictionary size. In single-call mode, this
+ * indicates the size of the output buffer.
+ */
+ size_t end;
+
+ /*
+ * Size of the dictionary as specified in Block Header. This is used
+ * together with "full" to detect corrupt input that would make us
+ * read beyond the beginning of the uncompressed stream.
+ */
+ uint32_t size;
+
+ /*
+ * Maximum allowed dictionary size in multi-call mode.
+ * This is ignored in single-call mode.
+ */
+ uint32_t size_max;
+
+ /*
+ * Amount of memory currently allocated for the dictionary.
+ * This is used only with XZ_DYNALLOC. (With XZ_PREALLOC,
+ * size_max is always the same as the allocated size.)
+ */
+ uint32_t allocated;
+
+ /* Operation mode */
+ enum xz_mode mode;
+};
+
+/* Range decoder */
+struct rc_dec {
+ uint32_t range;
+ uint32_t code;
+
+ /*
+ * Number of initializing bytes remaining to be read
+ * by rc_read_init().
+ */
+ uint32_t init_bytes_left;
+
+ /*
+ * Buffer from which we read our input. It can be either
+ * temp.buf or the caller-provided input buffer.
+ */
+ const uint8_t* in;
+ size_t in_pos;
+ size_t in_limit;
+};
+
+/* Probabilities for a length decoder. */
+struct lzma_len_dec {
+ /* Probability of match length being at least 10 */
+ uint16_t choice;
+
+ /* Probability of match length being at least 18 */
+ uint16_t choice2;
+
+ /* Probabilities for match lengths 2-9 */
+ uint16_t low[POS_STATES_MAX][LEN_LOW_SYMBOLS];
+
+ /* Probabilities for match lengths 10-17 */
+ uint16_t mid[POS_STATES_MAX][LEN_MID_SYMBOLS];
+
+ /* Probabilities for match lengths 18-273 */
+ uint16_t high[LEN_HIGH_SYMBOLS];
+};
+
+struct lzma_dec {
+ /* Distances of latest four matches */
+ uint32_t rep0;
+ uint32_t rep1;
+ uint32_t rep2;
+ uint32_t rep3;
+
+ /* Types of the most recently seen LZMA symbols */
+ enum lzma_state state;
+
+ /*
+ * Length of a match. This is updated so that dict_repeat can
+ * be called again to finish repeating the whole match.
+ */
+ uint32_t len;
+
+ /*
+ * LZMA properties or related bit masks (number of literal
+ * context bits, a mask dervied from the number of literal
+ * position bits, and a mask dervied from the number
+ * position bits)
+ */
+ uint32_t lc;
+ uint32_t literal_pos_mask; /* (1 << lp) - 1 */
+ uint32_t pos_mask; /* (1 << pb) - 1 */
+
+ /* If 1, it's a match. Otherwise it's a single 8-bit literal. */
+ uint16_t is_match[STATES][POS_STATES_MAX];
+
+ /* If 1, it's a repeated match. The distance is one of rep0 .. rep3. */
+ uint16_t is_rep[STATES];
+
+ /*
+ * If 0, distance of a repeated match is rep0.
+ * Otherwise check is_rep1.
+ */
+ uint16_t is_rep0[STATES];
+
+ /*
+ * If 0, distance of a repeated match is rep1.
+ * Otherwise check is_rep2.
+ */
+ uint16_t is_rep1[STATES];
+
+ /* If 0, distance of a repeated match is rep2. Otherwise it is rep3. */
+ uint16_t is_rep2[STATES];
+
+ /*
+ * If 1, the repeated match has length of one byte. Otherwise
+ * the length is decoded from rep_len_decoder.
+ */
+ uint16_t is_rep0_long[STATES][POS_STATES_MAX];
+
+ /*
+ * Probability tree for the highest two bits of the match
+ * distance. There is a separate probability tree for match
+ * lengths of 2 (i.e. MATCH_LEN_MIN), 3, 4, and [5, 273].
+ */
+ uint16_t dist_slot[DIST_STATES][DIST_SLOTS];
+
+ /*
+ * Probility trees for additional bits for match distance
+ * when the distance is in the range [4, 127].
+ */
+ uint16_t dist_special[FULL_DISTANCES - DIST_MODEL_END];
+
+ /*
+ * Probability tree for the lowest four bits of a match
+ * distance that is equal to or greater than 128.
+ */
+ uint16_t dist_align[ALIGN_SIZE];
+
+ /* Length of a normal match */
+ struct lzma_len_dec match_len_dec;
+
+ /* Length of a repeated match */
+ struct lzma_len_dec rep_len_dec;
+
+ /* Probabilities of literals */
+ uint16_t literal[LITERAL_CODERS_MAX][LITERAL_CODER_SIZE];
+};
+
+struct lzma2_dec {
+ /* Position in xz_dec_lzma2_run(). */
+ enum lzma2_seq {
+ SEQ_CONTROL,
+ SEQ_UNCOMPRESSED_1,
+ SEQ_UNCOMPRESSED_2,
+ SEQ_COMPRESSED_0,
+ SEQ_COMPRESSED_1,
+ SEQ_PROPERTIES,
+ SEQ_LZMA_PREPARE,
+ SEQ_LZMA_RUN,
+ SEQ_COPY
+ } sequence;
+
+ /* Next position after decoding the compressed size of the chunk. */
+ enum lzma2_seq next_sequence;
+
+ /* Uncompressed size of LZMA chunk (2 MiB at maximum) */
+ uint32_t uncompressed;
+
+ /*
+ * Compressed size of LZMA chunk or compressed/uncompressed
+ * size of uncompressed chunk (64 KiB at maximum)
+ */
+ uint32_t compressed;
+
+ /*
+ * True if dictionary reset is needed. This is false before
+ * the first chunk (LZMA or uncompressed).
+ */
+ bool need_dict_reset;
+
+ /*
+ * True if new LZMA properties are needed. This is false
+ * before the first LZMA chunk.
+ */
+ bool need_props;
+};
+
+struct xz_dec_lzma2 {
+ /*
+ * The order below is important on x86 to reduce code size and
+ * it shouldn't hurt on other platforms. Everything up to and
+ * including lzma.pos_mask are in the first 128 bytes on x86-32,
+ * which allows using smaller instructions to access those
+ * variables. On x86-64, fewer variables fit into the first 128
+ * bytes, but this is still the best order without sacrificing
+ * the readability by splitting the structures.
+ */
+ struct rc_dec rc;
+ struct dictionary dict;
+ struct lzma2_dec lzma2;
+ struct lzma_dec lzma;
+
+ /*
+ * Temporary buffer which holds small number of input bytes between
+ * decoder calls. See lzma2_lzma() for details.
+ */
+ struct {
+ uint32_t size;
+ uint8_t buf[3 * LZMA_IN_REQUIRED];
+ } temp;
+};
+
+/**************
+ * Dictionary *
+ **************/
+
+/*
+ * Reset the dictionary state. When in single-call mode, set up the beginning
+ * of the dictionary to point to the actual output buffer.
+ */
+static void dict_reset(struct dictionary* dict, struct xz_buf* b)
+{
+ if (DEC_IS_SINGLE(dict->mode)) {
+ dict->buf = b->out + b->out_pos;
+ dict->end = b->out_size - b->out_pos;
+ }
+
+ dict->start = 0;
+ dict->pos = 0;
+ dict->limit = 0;
+ dict->full = 0;
+}
+
+/* Set dictionary write limit */
+static void dict_limit(struct dictionary* dict, size_t out_max)
+{
+ if (dict->end - dict->pos <= out_max)
+ dict->limit = dict->end;
+ else
+ dict->limit = dict->pos + out_max;
+}
+
+/* Return true if at least one byte can be written into the dictionary. */
+static inline bool dict_has_space(const struct dictionary* dict)
+{
+ return dict->pos < dict->limit;
+}
+
+/*
+ * Get a byte from the dictionary at the given distance. The distance is
+ * assumed to valid, or as a special case, zero when the dictionary is
+ * still empty. This special case is needed for single-call decoding to
+ * avoid writing a '\0' to the end of the destination buffer.
+ */
+static inline uint32_t dict_get(const struct dictionary* dict, uint32_t dist)
+{
+ size_t offset = dict->pos - dist - 1;
+
+ if (dist >= dict->pos)
+ offset += dict->end;
+
+ return dict->full > 0 ? dict->buf[offset] : 0;
+}
+
+/*
+ * Put one byte into the dictionary. It is assumed that there is space for it.
+ */
+static inline void dict_put(struct dictionary* dict, uint8_t byte)
+{
+ dict->buf[dict->pos++] = byte;
+
+ if (dict->full < dict->pos)
+ dict->full = dict->pos;
+}
+
+/*
+ * Repeat given number of bytes from the given distance. If the distance is
+ * invalid, false is returned. On success, true is returned and *len is
+ * updated to indicate how many bytes were left to be repeated.
+ */
+static bool dict_repeat(struct dictionary* dict, uint32_t* len, uint32_t dist)
+{
+ size_t back;
+ uint32_t left;
+
+ if (dist >= dict->full || dist >= dict->size)
+ return false;
+
+ left = min_t(size_t, dict->limit - dict->pos, *len);
+ *len -= left;
+
+ back = dict->pos - dist - 1;
+ if (dist >= dict->pos)
+ back += dict->end;
+
+ do {
+ dict->buf[dict->pos++] = dict->buf[back++];
+ if (back == dict->end)
+ back = 0;
+ } while (--left > 0);
+
+ if (dict->full < dict->pos)
+ dict->full = dict->pos;
+
+ return true;
+}
+
+/* Copy uncompressed data as is from input to dictionary and output buffers. */
+static void dict_uncompressed(struct dictionary* dict, struct xz_buf* b,
+ uint32_t* left)
+{
+ size_t copy_size;
+
+ while (*left > 0 && b->in_pos < b->in_size && b->out_pos < b->out_size) {
+ copy_size = min(b->in_size - b->in_pos, b->out_size - b->out_pos);
+ if (copy_size > dict->end - dict->pos)
+ copy_size = dict->end - dict->pos;
+ if (copy_size > *left)
+ copy_size = *left;
+
+ *left -= copy_size;
+
+ memcpy(dict->buf + dict->pos, b->in + b->in_pos, copy_size);
+ dict->pos += copy_size;
+
+ if (dict->full < dict->pos)
+ dict->full = dict->pos;
+
+ if (DEC_IS_MULTI(dict->mode)) {
+ if (dict->pos == dict->end)
+ dict->pos = 0;
+
+ memcpy(b->out + b->out_pos, b->in + b->in_pos, copy_size);
+ }
+
+ dict->start = dict->pos;
+
+ b->out_pos += copy_size;
+ b->in_pos += copy_size;
+ }
+}
+
+/*
+ * Flush pending data from dictionary to b->out. It is assumed that there is
+ * enough space in b->out. This is guaranteed because caller uses dict_limit()
+ * before decoding data into the dictionary.
+ */
+static uint32_t dict_flush(struct dictionary* dict, struct xz_buf* b)
+{
+ size_t copy_size = dict->pos - dict->start;
+
+ if (DEC_IS_MULTI(dict->mode)) {
+ if (dict->pos == dict->end)
+ dict->pos = 0;
+
+ memcpy(b->out + b->out_pos, dict->buf + dict->start, copy_size);
+ }
+
+ dict->start = dict->pos;
+ b->out_pos += copy_size;
+ return copy_size;
+}
+
+/*****************
+ * Range decoder *
+ *****************/
+
+/* Reset the range decoder. */
+static void rc_reset(struct rc_dec* rc)
+{
+ rc->range = (uint32_t)-1;
+ rc->code = 0;
+ rc->init_bytes_left = RC_INIT_BYTES;
+}
+
+/*
+ * Read the first five initial bytes into rc->code if they haven't been
+ * read already. (Yes, the first byte gets completely ignored.)
+ */
+static bool rc_read_init(struct rc_dec* rc, struct xz_buf* b)
+{
+ while (rc->init_bytes_left > 0) {
+ if (b->in_pos == b->in_size)
+ return false;
+
+ rc->code = (rc->code << 8) + b->in[b->in_pos++];
+ --rc->init_bytes_left;
+ }
+
+ return true;
+}
+
+/* Return true if there may not be enough input for the next decoding loop. */
+static inline bool rc_limit_exceeded(const struct rc_dec* rc)
+{
+ return rc->in_pos > rc->in_limit;
+}
+
+/*
+ * Return true if it is possible (from point of view of range decoder) that
+ * we have reached the end of the LZMA chunk.
+ */
+static inline bool rc_is_finished(const struct rc_dec* rc)
+{
+ return rc->code == 0;
+}
+
+/* Read the next input byte if needed. */
+static __always_inline void rc_normalize(struct rc_dec* rc)
+{
+ if (rc->range < RC_TOP_VALUE) {
+ rc->range <<= RC_SHIFT_BITS;
+ rc->code = (rc->code << RC_SHIFT_BITS) + rc->in[rc->in_pos++];
+ }
+}
+
+/*
+ * Decode one bit. In some versions, this function has been splitted in three
+ * functions so that the compiler is supposed to be able to more easily avoid
+ * an extra branch. In this particular version of the LZMA decoder, this
+ * doesn't seem to be a good idea (tested with GCC 3.3.6, 3.4.6, and 4.3.3
+ * on x86). Using a non-splitted version results in nicer looking code too.
+ *
+ * NOTE: This must return an int. Do not make it return a bool or the speed
+ * of the code generated by GCC 3.x decreases 10-15 %. (GCC 4.3 doesn't care,
+ * and it generates 10-20 % faster code than GCC 3.x from this file anyway.)
+ */
+static __always_inline int rc_bit(struct rc_dec* rc, uint16_t* prob)
+{
+ uint32_t bound;
+ int bit;
+
+ rc_normalize(rc);
+ bound = (rc->range >> RC_BIT_MODEL_TOTAL_BITS) * *prob;
+ if (rc->code < bound) {
+ rc->range = bound;
+ *prob += (RC_BIT_MODEL_TOTAL - *prob) >> RC_MOVE_BITS;
+ bit = 0;
+ } else {
+ rc->range -= bound;
+ rc->code -= bound;
+ *prob -= *prob >> RC_MOVE_BITS;
+ bit = 1;
+ }
+
+ return bit;
+}
+
+/* Decode a bittree starting from the most significant bit. */
+static __always_inline uint32_t rc_bittree(struct rc_dec* rc, uint16_t* probs,
+ uint32_t limit)
+{
+ uint32_t symbol = 1;
+
+ do {
+ if (rc_bit(rc, &probs[symbol]))
+ symbol = (symbol << 1) + 1;
+ else
+ symbol <<= 1;
+ } while (symbol < limit);
+
+ return symbol;
+}
+
+/* Decode a bittree starting from the least significant bit. */
+static __always_inline void rc_bittree_reverse(struct rc_dec* rc,
+ uint16_t* probs, uint32_t* dest,
+ uint32_t limit)
+{
+ uint32_t symbol = 1;
+ uint32_t i = 0;
+
+ do {
+ if (rc_bit(rc, &probs[symbol])) {
+ symbol = (symbol << 1) + 1;
+ *dest += 1 << i;
+ } else {
+ symbol <<= 1;
+ }
+ } while (++i < limit);
+}
+
+/* Decode direct bits (fixed fifty-fifty probability) */
+static inline void rc_direct(struct rc_dec* rc, uint32_t* dest, uint32_t limit)
+{
+ uint32_t mask;
+
+ do {
+ rc_normalize(rc);
+ rc->range >>= 1;
+ rc->code -= rc->range;
+ mask = (uint32_t)0 - (rc->code >> 31);
+ rc->code += rc->range & mask;
+ *dest = (*dest << 1) + (mask + 1);
+ } while (--limit > 0);
+}
+
+/********
+ * LZMA *
+ ********/
+
+/* Get pointer to literal coder probability array. */
+static uint16_t* lzma_literal_probs(struct xz_dec_lzma2* s)
+{
+ uint32_t prev_byte = dict_get(&s->dict, 0);
+ uint32_t low = prev_byte >> (8 - s->lzma.lc);
+ uint32_t high = (s->dict.pos & s->lzma.literal_pos_mask) << s->lzma.lc;
+ return s->lzma.literal[low + high];
+}
+
+/* Decode a literal (one 8-bit byte) */
+static void lzma_literal(struct xz_dec_lzma2* s)
+{
+ uint16_t* probs;
+ uint32_t symbol;
+ uint32_t match_byte;
+ uint32_t match_bit;
+ uint32_t offset;
+ uint32_t i;
+
+ probs = lzma_literal_probs(s);
+
+ if (lzma_state_is_literal(s->lzma.state)) {
+ symbol = rc_bittree(&s->rc, probs, 0x100);
+ } else {
+ symbol = 1;
+ match_byte = dict_get(&s->dict, s->lzma.rep0) << 1;
+ offset = 0x100;
+
+ do {
+ match_bit = match_byte & offset;
+ match_byte <<= 1;
+ i = offset + match_bit + symbol;
+
+ if (rc_bit(&s->rc, &probs[i])) {
+ symbol = (symbol << 1) + 1;
+ offset &= match_bit;
+ } else {
+ symbol <<= 1;
+ offset &= ~match_bit;
+ }
+ } while (symbol < 0x100);
+ }
+
+ dict_put(&s->dict, (uint8_t)symbol);
+ lzma_state_literal(&s->lzma.state);
+}
+
+/* Decode the length of the match into s->lzma.len. */
+static void lzma_len(struct xz_dec_lzma2* s, struct lzma_len_dec* l,
+ uint32_t pos_state)
+{
+ uint16_t* probs;
+ uint32_t limit;
+
+ if (!rc_bit(&s->rc, &l->choice)) {
+ probs = l->low[pos_state];
+ limit = LEN_LOW_SYMBOLS;
+ s->lzma.len = MATCH_LEN_MIN;
+ } else {
+ if (!rc_bit(&s->rc, &l->choice2)) {
+ probs = l->mid[pos_state];
+ limit = LEN_MID_SYMBOLS;
+ s->lzma.len = MATCH_LEN_MIN + LEN_LOW_SYMBOLS;
+ } else {
+ probs = l->high;
+ limit = LEN_HIGH_SYMBOLS;
+ s->lzma.len = MATCH_LEN_MIN + LEN_LOW_SYMBOLS + LEN_MID_SYMBOLS;
+ }
+ }
+
+ s->lzma.len += rc_bittree(&s->rc, probs, limit) - limit;
+}
+
+/* Decode a match. The distance will be stored in s->lzma.rep0. */
+static void lzma_match(struct xz_dec_lzma2* s, uint32_t pos_state)
+{
+ uint16_t* probs;
+ uint32_t dist_slot;
+ uint32_t limit;
+
+ lzma_state_match(&s->lzma.state);
+
+ s->lzma.rep3 = s->lzma.rep2;
+ s->lzma.rep2 = s->lzma.rep1;
+ s->lzma.rep1 = s->lzma.rep0;
+
+ lzma_len(s, &s->lzma.match_len_dec, pos_state);
+
+ probs = s->lzma.dist_slot[lzma_get_dist_state(s->lzma.len)];
+ dist_slot = rc_bittree(&s->rc, probs, DIST_SLOTS) - DIST_SLOTS;
+
+ if (dist_slot < DIST_MODEL_START) {
+ s->lzma.rep0 = dist_slot;
+ } else {
+ limit = (dist_slot >> 1) - 1;
+ s->lzma.rep0 = 2 + (dist_slot & 1);
+
+ if (dist_slot < DIST_MODEL_END) {
+ s->lzma.rep0 <<= limit;
+ probs = s->lzma.dist_special + s->lzma.rep0 - dist_slot - 1;
+ rc_bittree_reverse(&s->rc, probs, &s->lzma.rep0, limit);
+ } else {
+ rc_direct(&s->rc, &s->lzma.rep0, limit - ALIGN_BITS);
+ s->lzma.rep0 <<= ALIGN_BITS;
+ rc_bittree_reverse(&s->rc, s->lzma.dist_align, &s->lzma.rep0,
+ ALIGN_BITS);
+ }
+ }
+}
+
+/*
+ * Decode a repeated match. The distance is one of the four most recently
+ * seen matches. The distance will be stored in s->lzma.rep0.
+ */
+static void lzma_rep_match(struct xz_dec_lzma2* s, uint32_t pos_state)
+{
+ uint32_t tmp;
+
+ if (!rc_bit(&s->rc, &s->lzma.is_rep0[s->lzma.state])) {
+ if (!rc_bit(&s->rc, &s->lzma.is_rep0_long[s->lzma.state][pos_state])) {
+ lzma_state_short_rep(&s->lzma.state);
+ s->lzma.len = 1;
+ return;
+ }
+ } else {
+ if (!rc_bit(&s->rc, &s->lzma.is_rep1[s->lzma.state])) {
+ tmp = s->lzma.rep1;
+ } else {
+ if (!rc_bit(&s->rc, &s->lzma.is_rep2[s->lzma.state])) {
+ tmp = s->lzma.rep2;
+ } else {
+ tmp = s->lzma.rep3;
+ s->lzma.rep3 = s->lzma.rep2;
+ }
+
+ s->lzma.rep2 = s->lzma.rep1;
+ }
+
+ s->lzma.rep1 = s->lzma.rep0;
+ s->lzma.rep0 = tmp;
+ }
+
+ lzma_state_long_rep(&s->lzma.state);
+ lzma_len(s, &s->lzma.rep_len_dec, pos_state);
+}
+
+/* LZMA decoder core */
+static bool lzma_main(struct xz_dec_lzma2* s)
+{
+ uint32_t pos_state;
+
+ /*
+ * If the dictionary was reached during the previous call, try to
+ * finish the possibly pending repeat in the dictionary.
+ */
+ if (dict_has_space(&s->dict) && s->lzma.len > 0)
+ dict_repeat(&s->dict, &s->lzma.len, s->lzma.rep0);
+
+ /*
+ * Decode more LZMA symbols. One iteration may consume up to
+ * LZMA_IN_REQUIRED - 1 bytes.
+ */
+ while (dict_has_space(&s->dict) && !rc_limit_exceeded(&s->rc)) {
+ pos_state = s->dict.pos & s->lzma.pos_mask;
+
+ if (!rc_bit(&s->rc, &s->lzma.is_match[s->lzma.state][pos_state])) {
+ lzma_literal(s);
+ } else {
+ if (rc_bit(&s->rc, &s->lzma.is_rep[s->lzma.state]))
+ lzma_rep_match(s, pos_state);
+ else
+ lzma_match(s, pos_state);
+
+ if (!dict_repeat(&s->dict, &s->lzma.len, s->lzma.rep0))
+ return false;
+ }
+ }
+
+ /*
+ * Having the range decoder always normalized when we are outside
+ * this function makes it easier to correctly handle end of the chunk.
+ */
+ rc_normalize(&s->rc);
+
+ return true;
+}
+
+/*
+ * Reset the LZMA decoder and range decoder state. Dictionary is nore reset
+ * here, because LZMA state may be reset without resetting the dictionary.
+ */
+static void lzma_reset(struct xz_dec_lzma2* s)
+{
+ uint16_t* probs;
+ size_t i;
+
+ s->lzma.state = STATE_LIT_LIT;
+ s->lzma.rep0 = 0;
+ s->lzma.rep1 = 0;
+ s->lzma.rep2 = 0;
+ s->lzma.rep3 = 0;
+
+ /*
+ * All probabilities are initialized to the same value. This hack
+ * makes the code smaller by avoiding a separate loop for each
+ * probability array.
+ *
+ * This could be optimized so that only that part of literal
+ * probabilities that are actually required. In the common case
+ * we would write 12 KiB less.
+ */
+ probs = s->lzma.is_match[0];
+ for (i = 0; i < PROBS_TOTAL; ++i)
+ probs[i] = RC_BIT_MODEL_TOTAL / 2;
+
+ rc_reset(&s->rc);
+}
+
+/*
+ * Decode and validate LZMA properties (lc/lp/pb) and calculate the bit masks
+ * from the decoded lp and pb values. On success, the LZMA decoder state is
+ * reset and true is returned.
+ */
+static bool lzma_props(struct xz_dec_lzma2* s, uint8_t props)
+{
+ if (props > (4 * 5 + 4) * 9 + 8)
+ return false;
+
+ s->lzma.pos_mask = 0;
+ while (props >= 9 * 5) {
+ props -= 9 * 5;
+ ++s->lzma.pos_mask;
+ }
+
+ s->lzma.pos_mask = (1 << s->lzma.pos_mask) - 1;
+
+ s->lzma.literal_pos_mask = 0;
+ while (props >= 9) {
+ props -= 9;
+ ++s->lzma.literal_pos_mask;
+ }
+
+ s->lzma.lc = props;
+
+ if (s->lzma.lc + s->lzma.literal_pos_mask > 4)
+ return false;
+
+ s->lzma.literal_pos_mask = (1 << s->lzma.literal_pos_mask) - 1;
+
+ lzma_reset(s);
+
+ return true;
+}
+
+/*********
+ * LZMA2 *
+ *********/
+
+/*
+ * The LZMA decoder assumes that if the input limit (s->rc.in_limit) hasn't
+ * been exceeded, it is safe to read up to LZMA_IN_REQUIRED bytes. This
+ * wrapper function takes care of making the LZMA decoder's assumption safe.
+ *
+ * As long as there is plenty of input left to be decoded in the current LZMA
+ * chunk, we decode directly from the caller-supplied input buffer until
+ * there's LZMA_IN_REQUIRED bytes left. Those remaining bytes are copied into
+ * s->temp.buf, which (hopefully) gets filled on the next call to this
+ * function. We decode a few bytes from the temporary buffer so that we can
+ * continue decoding from the caller-supplied input buffer again.
+ */
+static bool lzma2_lzma(struct xz_dec_lzma2* s, struct xz_buf* b)
+{
+ size_t in_avail;
+ uint32_t tmp;
+
+ in_avail = b->in_size - b->in_pos;
+ if (s->temp.size > 0 || s->lzma2.compressed == 0) {
+ tmp = 2 * LZMA_IN_REQUIRED - s->temp.size;
+ if (tmp > s->lzma2.compressed - s->temp.size)
+ tmp = s->lzma2.compressed - s->temp.size;
+ if (tmp > in_avail)
+ tmp = in_avail;
+
+ memcpy(s->temp.buf + s->temp.size, b->in + b->in_pos, tmp);
+
+ if (s->temp.size + tmp == s->lzma2.compressed) {
+ memzero(s->temp.buf + s->temp.size + tmp,
+ sizeof(s->temp.buf) - s->temp.size - tmp);
+ s->rc.in_limit = s->temp.size + tmp;
+ } else if (s->temp.size + tmp < LZMA_IN_REQUIRED) {
+ s->temp.size += tmp;
+ b->in_pos += tmp;
+ return true;
+ } else {
+ s->rc.in_limit = s->temp.size + tmp - LZMA_IN_REQUIRED;
+ }
+
+ s->rc.in = s->temp.buf;
+ s->rc.in_pos = 0;
+
+ if (!lzma_main(s) || s->rc.in_pos > s->temp.size + tmp)
+ return false;
+
+ s->lzma2.compressed -= s->rc.in_pos;
+
+ if (s->rc.in_pos < s->temp.size) {
+ s->temp.size -= s->rc.in_pos;
+ memmove(s->temp.buf, s->temp.buf + s->rc.in_pos, s->temp.size);
+ return true;
+ }
+
+ b->in_pos += s->rc.in_pos - s->temp.size;
+ s->temp.size = 0;
+ }
+
+ in_avail = b->in_size - b->in_pos;
+ if (in_avail >= LZMA_IN_REQUIRED) {
+ s->rc.in = b->in;
+ s->rc.in_pos = b->in_pos;
+
+ if (in_avail >= s->lzma2.compressed + LZMA_IN_REQUIRED)
+ s->rc.in_limit = b->in_pos + s->lzma2.compressed;
+ else
+ s->rc.in_limit = b->in_size - LZMA_IN_REQUIRED;
+
+ if (!lzma_main(s))
+ return false;
+
+ in_avail = s->rc.in_pos - b->in_pos;
+ if (in_avail > s->lzma2.compressed)
+ return false;
+
+ s->lzma2.compressed -= in_avail;
+ b->in_pos = s->rc.in_pos;
+ }
+
+ in_avail = b->in_size - b->in_pos;
+ if (in_avail < LZMA_IN_REQUIRED) {
+ if (in_avail > s->lzma2.compressed)
+ in_avail = s->lzma2.compressed;
+
+ memcpy(s->temp.buf, b->in + b->in_pos, in_avail);
+ s->temp.size = in_avail;
+ b->in_pos += in_avail;
+ }
+
+ return true;
+}
+
+/*
+ * Take care of the LZMA2 control layer, and forward the job of actual LZMA
+ * decoding or copying of uncompressed chunks to other functions.
+ */
+XZ_EXTERN enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2* s, struct xz_buf* b)
+{
+ uint32_t tmp;
+
+ while (b->in_pos < b->in_size || s->lzma2.sequence == SEQ_LZMA_RUN) {
+ switch (s->lzma2.sequence) {
+ case SEQ_CONTROL:
+ /*
+ * LZMA2 control byte
+ *
+ * Exact values:
+ * 0x00 End marker
+ * 0x01 Dictionary reset followed by
+ * an uncompressed chunk
+ * 0x02 Uncompressed chunk (no dictionary reset)
+ *
+ * Highest three bits (s->control & 0xE0):
+ * 0xE0 Dictionary reset, new properties and state
+ * reset, followed by LZMA compressed chunk
+ * 0xC0 New properties and state reset, followed
+ * by LZMA compressed chunk (no dictionary
+ * reset)
+ * 0xA0 State reset using old properties,
+ * followed by LZMA compressed chunk (no
+ * dictionary reset)
+ * 0x80 LZMA chunk (no dictionary or state reset)
+ *
+ * For LZMA compressed chunks, the lowest five bits
+ * (s->control & 1F) are the highest bits of the
+ * uncompressed size (bits 16-20).
+ *
+ * A new LZMA2 stream must begin with a dictionary
+ * reset. The first LZMA chunk must set new
+ * properties and reset the LZMA state.
+ *
+ * Values that don't match anything described above
+ * are invalid and we return XZ_DATA_ERROR.
+ */
+ tmp = b->in[b->in_pos++];
+
+ if (tmp == 0x00)
+ return XZ_STREAM_END;
+
+ if (tmp >= 0xE0 || tmp == 0x01) {
+ s->lzma2.need_props = true;
+ s->lzma2.need_dict_reset = false;
+ dict_reset(&s->dict, b);
+ } else if (s->lzma2.need_dict_reset) {
+ return XZ_DATA_ERROR;
+ }
+
+ if (tmp >= 0x80) {
+ s->lzma2.uncompressed = (tmp & 0x1F) << 16;
+ s->lzma2.sequence = SEQ_UNCOMPRESSED_1;
+
+ if (tmp >= 0xC0) {
+ /*
+ * When there are new properties,
+ * state reset is done at
+ * SEQ_PROPERTIES.
+ */
+ s->lzma2.need_props = false;
+ s->lzma2.next_sequence = SEQ_PROPERTIES;
+ } else if (s->lzma2.need_props) {
+ return XZ_DATA_ERROR;
+ } else {
+ s->lzma2.next_sequence = SEQ_LZMA_PREPARE;
+ if (tmp >= 0xA0)
+ lzma_reset(s);
+ }
+ } else {
+ if (tmp > 0x02)
+ return XZ_DATA_ERROR;
+
+ s->lzma2.sequence = SEQ_COMPRESSED_0;
+ s->lzma2.next_sequence = SEQ_COPY;
+ }
+
+ break;
+
+ case SEQ_UNCOMPRESSED_1:
+ s->lzma2.uncompressed += (uint32_t)b->in[b->in_pos++] << 8;
+ s->lzma2.sequence = SEQ_UNCOMPRESSED_2;
+ break;
+
+ case SEQ_UNCOMPRESSED_2:
+ s->lzma2.uncompressed += (uint32_t)b->in[b->in_pos++] + 1;
+ s->lzma2.sequence = SEQ_COMPRESSED_0;
+ break;
+
+ case SEQ_COMPRESSED_0:
+ s->lzma2.compressed = (uint32_t)b->in[b->in_pos++] << 8;
+ s->lzma2.sequence = SEQ_COMPRESSED_1;
+ break;
+
+ case SEQ_COMPRESSED_1:
+ s->lzma2.compressed += (uint32_t)b->in[b->in_pos++] + 1;
+ s->lzma2.sequence = s->lzma2.next_sequence;
+ break;
+
+ case SEQ_PROPERTIES:
+ if (!lzma_props(s, b->in[b->in_pos++]))
+ return XZ_DATA_ERROR;
+
+ s->lzma2.sequence = SEQ_LZMA_PREPARE;
+
+ case SEQ_LZMA_PREPARE:
+ if (s->lzma2.compressed < RC_INIT_BYTES)
+ return XZ_DATA_ERROR;
+
+ if (!rc_read_init(&s->rc, b))
+ return XZ_OK;
+
+ s->lzma2.compressed -= RC_INIT_BYTES;
+ s->lzma2.sequence = SEQ_LZMA_RUN;
+
+ case SEQ_LZMA_RUN:
+ /*
+ * Set dictionary limit to indicate how much we want
+ * to be encoded at maximum. Decode new data into the
+ * dictionary. Flush the new data from dictionary to
+ * b->out. Check if we finished decoding this chunk.
+ * In case the dictionary got full but we didn't fill
+ * the output buffer yet, we may run this loop
+ * multiple times without changing s->lzma2.sequence.
+ */
+ dict_limit(&s->dict, min_t(size_t, b->out_size - b->out_pos,
+ s->lzma2.uncompressed));
+ if (!lzma2_lzma(s, b))
+ return XZ_DATA_ERROR;
+
+ s->lzma2.uncompressed -= dict_flush(&s->dict, b);
+
+ if (s->lzma2.uncompressed == 0) {
+ if (s->lzma2.compressed > 0 || s->lzma.len > 0 ||
+ !rc_is_finished(&s->rc))
+ return XZ_DATA_ERROR;
+
+ rc_reset(&s->rc);
+ s->lzma2.sequence = SEQ_CONTROL;
+ } else if (b->out_pos == b->out_size ||
+ (b->in_pos == b->in_size &&
+ s->temp.size < s->lzma2.compressed)) {
+ return XZ_OK;
+ }
+
+ break;
+
+ case SEQ_COPY:
+ dict_uncompressed(&s->dict, b, &s->lzma2.compressed);
+ if (s->lzma2.compressed > 0)
+ return XZ_OK;
+
+ s->lzma2.sequence = SEQ_CONTROL;
+ break;
+ }
+ }
+
+ return XZ_OK;
+}
+
+XZ_EXTERN struct xz_dec_lzma2* xz_dec_lzma2_create(enum xz_mode mode,
+ uint32_t dict_max)
+{
+ struct xz_dec_lzma2* s = kmalloc(sizeof(*s), GFP_KERNEL);
+ if (s == NULL)
+ return NULL;
+
+ s->dict.mode = mode;
+ s->dict.size_max = dict_max;
+
+ if (DEC_IS_PREALLOC(mode)) {
+ s->dict.buf = vmalloc(dict_max);
+ if (s->dict.buf == NULL) {
+ kfree(s);
+ return NULL;
+ }
+ } else if (DEC_IS_DYNALLOC(mode)) {
+ s->dict.buf = NULL;
+ s->dict.allocated = 0;
+ }
+
+ return s;
+}
+
+XZ_EXTERN enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2* s, uint8_t props)
+{
+ /* This limits dictionary size to 3 GiB to keep parsing simpler. */
+ if (props > 39)
+ return XZ_OPTIONS_ERROR;
+
+ s->dict.size = 2 + (props & 1);
+ s->dict.size <<= (props >> 1) + 11;
+
+ if (DEC_IS_MULTI(s->dict.mode)) {
+ if (s->dict.size > s->dict.size_max)
+ return XZ_MEMLIMIT_ERROR;
+
+ s->dict.end = s->dict.size;
+
+ if (DEC_IS_DYNALLOC(s->dict.mode)) {
+ if (s->dict.allocated < s->dict.size) {
+ vfree(s->dict.buf);
+ s->dict.buf = vmalloc(s->dict.size);
+ if (s->dict.buf == NULL) {
+ s->dict.allocated = 0;
+ return XZ_MEM_ERROR;
+ }
+ }
+ }
+ }
+
+ s->lzma.len = 0;
+
+ s->lzma2.sequence = SEQ_CONTROL;
+ s->lzma2.need_dict_reset = true;
+
+ s->temp.size = 0;
+
+ return XZ_OK;
+}
+
+XZ_EXTERN void xz_dec_lzma2_end(struct xz_dec_lzma2* s)
+{
+ if (DEC_IS_MULTI(s->dict.mode))
+ vfree(s->dict.buf);
+
+ kfree(s);
+}
diff --git a/meshmc/libraries/xz-embedded/src/xz_dec_stream.c b/meshmc/libraries/xz-embedded/src/xz_dec_stream.c
new file mode 100644
index 0000000000..23337a4401
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_dec_stream.c
@@ -0,0 +1,832 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * .xz Stream decoder
+ *
+ * Author: Lasse Collin <lasse.collin@tukaani.org>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#include "xz_private.h"
+#include "xz_stream.h"
+
+#ifdef XZ_USE_CRC64
+#define IS_CRC64(check_type) ((check_type) == XZ_CHECK_CRC64)
+#else
+#define IS_CRC64(check_type) false
+#endif
+
+/* Hash used to validate the Index field */
+struct xz_dec_hash {
+ vli_type unpadded;
+ vli_type uncompressed;
+ uint32_t crc32;
+};
+
+struct xz_dec {
+ /* Position in dec_main() */
+ enum {
+ SEQ_STREAM_HEADER,
+ SEQ_BLOCK_START,
+ SEQ_BLOCK_HEADER,
+ SEQ_BLOCK_UNCOMPRESS,
+ SEQ_BLOCK_PADDING,
+ SEQ_BLOCK_CHECK,
+ SEQ_INDEX,
+ SEQ_INDEX_PADDING,
+ SEQ_INDEX_CRC32,
+ SEQ_STREAM_FOOTER
+ } sequence;
+
+ /* Position in variable-length integers and Check fields */
+ uint32_t pos;
+
+ /* Variable-length integer decoded by dec_vli() */
+ vli_type vli;
+
+ /* Saved in_pos and out_pos */
+ size_t in_start;
+ size_t out_start;
+
+#ifdef XZ_USE_CRC64
+ /* CRC32 or CRC64 value in Block or CRC32 value in Index */
+ uint64_t crc;
+#else
+ /* CRC32 value in Block or Index */
+ uint32_t crc;
+#endif
+
+ /* Type of the integrity check calculated from uncompressed data */
+ enum xz_check check_type;
+
+ /* Operation mode */
+ enum xz_mode mode;
+
+ /*
+ * True if the next call to xz_dec_run() is allowed to return
+ * XZ_BUF_ERROR.
+ */
+ bool allow_buf_error;
+
+ /* Information stored in Block Header */
+ struct {
+ /*
+ * Value stored in the Compressed Size field, or
+ * VLI_UNKNOWN if Compressed Size is not present.
+ */
+ vli_type compressed;
+
+ /*
+ * Value stored in the Uncompressed Size field, or
+ * VLI_UNKNOWN if Uncompressed Size is not present.
+ */
+ vli_type uncompressed;
+
+ /* Size of the Block Header field */
+ uint32_t size;
+ } block_header;
+
+ /* Information collected when decoding Blocks */
+ struct {
+ /* Observed compressed size of the current Block */
+ vli_type compressed;
+
+ /* Observed uncompressed size of the current Block */
+ vli_type uncompressed;
+
+ /* Number of Blocks decoded so far */
+ vli_type count;
+
+ /*
+ * Hash calculated from the Block sizes. This is used to
+ * validate the Index field.
+ */
+ struct xz_dec_hash hash;
+ } block;
+
+ /* Variables needed when verifying the Index field */
+ struct {
+ /* Position in dec_index() */
+ enum {
+ SEQ_INDEX_COUNT,
+ SEQ_INDEX_UNPADDED,
+ SEQ_INDEX_UNCOMPRESSED
+ } sequence;
+
+ /* Size of the Index in bytes */
+ vli_type size;
+
+ /* Number of Records (matches block.count in valid files) */
+ vli_type count;
+
+ /*
+ * Hash calculated from the Records (matches block.hash in
+ * valid files).
+ */
+ struct xz_dec_hash hash;
+ } index;
+
+ /*
+ * Temporary buffer needed to hold Stream Header, Block Header,
+ * and Stream Footer. The Block Header is the biggest (1 KiB)
+ * so we reserve space according to that. buf[] has to be aligned
+ * to a multiple of four bytes; the size_t variables before it
+ * should guarantee this.
+ */
+ struct {
+ size_t pos;
+ size_t size;
+ uint8_t buf[1024];
+ } temp;
+
+ struct xz_dec_lzma2* lzma2;
+
+#ifdef XZ_DEC_BCJ
+ struct xz_dec_bcj* bcj;
+ bool bcj_active;
+#endif
+};
+
+#ifdef XZ_DEC_ANY_CHECK
+/* Sizes of the Check field with different Check IDs */
+static const uint8_t check_sizes[16] = {0, 4, 4, 4, 8, 8, 8, 16,
+ 16, 16, 32, 32, 32, 64, 64, 64};
+#endif
+
+/*
+ * Fill s->temp by copying data starting from b->in[b->in_pos]. Caller
+ * must have set s->temp.pos to indicate how much data we are supposed
+ * to copy into s->temp.buf. Return true once s->temp.pos has reached
+ * s->temp.size.
+ */
+static bool fill_temp(struct xz_dec* s, struct xz_buf* b)
+{
+ size_t copy_size =
+ min_t(size_t, b->in_size - b->in_pos, s->temp.size - s->temp.pos);
+
+ memcpy(s->temp.buf + s->temp.pos, b->in + b->in_pos, copy_size);
+ b->in_pos += copy_size;
+ s->temp.pos += copy_size;
+
+ if (s->temp.pos == s->temp.size) {
+ s->temp.pos = 0;
+ return true;
+ }
+
+ return false;
+}
+
+/* Decode a variable-length integer (little-endian base-128 encoding) */
+static enum xz_ret dec_vli(struct xz_dec* s, const uint8_t* in, size_t* in_pos,
+ size_t in_size)
+{
+ uint8_t byte;
+
+ if (s->pos == 0)
+ s->vli = 0;
+
+ while (*in_pos < in_size) {
+ byte = in[*in_pos];
+ ++*in_pos;
+
+ s->vli |= (vli_type)(byte & 0x7F) << s->pos;
+
+ if ((byte & 0x80) == 0) {
+ /* Don't allow non-minimal encodings. */
+ if (byte == 0 && s->pos != 0)
+ return XZ_DATA_ERROR;
+
+ s->pos = 0;
+ return XZ_STREAM_END;
+ }
+
+ s->pos += 7;
+ if (s->pos == 7 * VLI_BYTES_MAX)
+ return XZ_DATA_ERROR;
+ }
+
+ return XZ_OK;
+}
+
+/*
+ * Decode the Compressed Data field from a Block. Update and validate
+ * the observed compressed and uncompressed sizes of the Block so that
+ * they don't exceed the values possibly stored in the Block Header
+ * (validation assumes that no integer overflow occurs, since vli_type
+ * is normally uint64_t). Update the CRC32 or CRC64 value if presence of
+ * the CRC32 or CRC64 field was indicated in Stream Header.
+ *
+ * Once the decoding is finished, validate that the observed sizes match
+ * the sizes possibly stored in the Block Header. Update the hash and
+ * Block count, which are later used to validate the Index field.
+ */
+static enum xz_ret dec_block(struct xz_dec* s, struct xz_buf* b)
+{
+ enum xz_ret ret;
+
+ s->in_start = b->in_pos;
+ s->out_start = b->out_pos;
+
+#ifdef XZ_DEC_BCJ
+ if (s->bcj_active)
+ ret = xz_dec_bcj_run(s->bcj, s->lzma2, b);
+ else
+#endif
+ ret = xz_dec_lzma2_run(s->lzma2, b);
+
+ s->block.compressed += b->in_pos - s->in_start;
+ s->block.uncompressed += b->out_pos - s->out_start;
+
+ /*
+ * There is no need to separately check for VLI_UNKNOWN, since
+ * the observed sizes are always smaller than VLI_UNKNOWN.
+ */
+ if (s->block.compressed > s->block_header.compressed ||
+ s->block.uncompressed > s->block_header.uncompressed)
+ return XZ_DATA_ERROR;
+
+ if (s->check_type == XZ_CHECK_CRC32)
+ s->crc =
+ xz_crc32(b->out + s->out_start, b->out_pos - s->out_start, s->crc);
+#ifdef XZ_USE_CRC64
+ else if (s->check_type == XZ_CHECK_CRC64)
+ s->crc =
+ xz_crc64(b->out + s->out_start, b->out_pos - s->out_start, s->crc);
+#endif
+
+ if (ret == XZ_STREAM_END) {
+ if (s->block_header.compressed != VLI_UNKNOWN &&
+ s->block_header.compressed != s->block.compressed)
+ return XZ_DATA_ERROR;
+
+ if (s->block_header.uncompressed != VLI_UNKNOWN &&
+ s->block_header.uncompressed != s->block.uncompressed)
+ return XZ_DATA_ERROR;
+
+ s->block.hash.unpadded += s->block_header.size + s->block.compressed;
+
+#ifdef XZ_DEC_ANY_CHECK
+ s->block.hash.unpadded += check_sizes[s->check_type];
+#else
+ if (s->check_type == XZ_CHECK_CRC32)
+ s->block.hash.unpadded += 4;
+ else if (IS_CRC64(s->check_type))
+ s->block.hash.unpadded += 8;
+#endif
+
+ s->block.hash.uncompressed += s->block.uncompressed;
+ s->block.hash.crc32 =
+ xz_crc32((const uint8_t*)&s->block.hash, sizeof(s->block.hash),
+ s->block.hash.crc32);
+
+ ++s->block.count;
+ }
+
+ return ret;
+}
+
+/* Update the Index size and the CRC32 value. */
+static void index_update(struct xz_dec* s, const struct xz_buf* b)
+{
+ size_t in_used = b->in_pos - s->in_start;
+ s->index.size += in_used;
+ s->crc = xz_crc32(b->in + s->in_start, in_used, s->crc);
+}
+
+/*
+ * Decode the Number of Records, Unpadded Size, and Uncompressed Size
+ * fields from the Index field. That is, Index Padding and CRC32 are not
+ * decoded by this function.
+ *
+ * This can return XZ_OK (more input needed), XZ_STREAM_END (everything
+ * successfully decoded), or XZ_DATA_ERROR (input is corrupt).
+ */
+static enum xz_ret dec_index(struct xz_dec* s, struct xz_buf* b)
+{
+ enum xz_ret ret;
+
+ do {
+ ret = dec_vli(s, b->in, &b->in_pos, b->in_size);
+ if (ret != XZ_STREAM_END) {
+ index_update(s, b);
+ return ret;
+ }
+
+ switch (s->index.sequence) {
+ case SEQ_INDEX_COUNT:
+ s->index.count = s->vli;
+
+ /*
+ * Validate that the Number of Records field
+ * indicates the same number of Records as
+ * there were Blocks in the Stream.
+ */
+ if (s->index.count != s->block.count)
+ return XZ_DATA_ERROR;
+
+ s->index.sequence = SEQ_INDEX_UNPADDED;
+ break;
+
+ case SEQ_INDEX_UNPADDED:
+ s->index.hash.unpadded += s->vli;
+ s->index.sequence = SEQ_INDEX_UNCOMPRESSED;
+ break;
+
+ case SEQ_INDEX_UNCOMPRESSED:
+ s->index.hash.uncompressed += s->vli;
+ s->index.hash.crc32 =
+ xz_crc32((const uint8_t*)&s->index.hash,
+ sizeof(s->index.hash), s->index.hash.crc32);
+ --s->index.count;
+ s->index.sequence = SEQ_INDEX_UNPADDED;
+ break;
+ }
+ } while (s->index.count > 0);
+
+ return XZ_STREAM_END;
+}
+
+/*
+ * Validate that the next four or eight input bytes match the value
+ * of s->crc. s->pos must be zero when starting to validate the first byte.
+ * The "bits" argument allows using the same code for both CRC32 and CRC64.
+ */
+static enum xz_ret crc_validate(struct xz_dec* s, struct xz_buf* b,
+ uint32_t bits)
+{
+ do {
+ if (b->in_pos == b->in_size)
+ return XZ_OK;
+
+ if (((s->crc >> s->pos) & 0xFF) != b->in[b->in_pos++])
+ return XZ_DATA_ERROR;
+
+ s->pos += 8;
+
+ } while (s->pos < bits);
+
+ s->crc = 0;
+ s->pos = 0;
+
+ return XZ_STREAM_END;
+}
+
+#ifdef XZ_DEC_ANY_CHECK
+/*
+ * Skip over the Check field when the Check ID is not supported.
+ * Returns true once the whole Check field has been skipped over.
+ */
+static bool check_skip(struct xz_dec* s, struct xz_buf* b)
+{
+ while (s->pos < check_sizes[s->check_type]) {
+ if (b->in_pos == b->in_size)
+ return false;
+
+ ++b->in_pos;
+ ++s->pos;
+ }
+
+ s->pos = 0;
+
+ return true;
+}
+#endif
+
+/* Decode the Stream Header field (the first 12 bytes of the .xz Stream). */
+static enum xz_ret dec_stream_header(struct xz_dec* s)
+{
+ if (!memeq(s->temp.buf, HEADER_MAGIC, HEADER_MAGIC_SIZE))
+ return XZ_FORMAT_ERROR;
+
+ if (xz_crc32(s->temp.buf + HEADER_MAGIC_SIZE, 2, 0) !=
+ get_le32(s->temp.buf + HEADER_MAGIC_SIZE + 2))
+ return XZ_DATA_ERROR;
+
+ if (s->temp.buf[HEADER_MAGIC_SIZE] != 0)
+ return XZ_OPTIONS_ERROR;
+
+ /*
+ * Of integrity checks, we support none (Check ID = 0),
+ * CRC32 (Check ID = 1), and optionally CRC64 (Check ID = 4).
+ * However, if XZ_DEC_ANY_CHECK is defined, we will accept other
+ * check types too, but then the check won't be verified and
+ * a warning (XZ_UNSUPPORTED_CHECK) will be given.
+ */
+ s->check_type = s->temp.buf[HEADER_MAGIC_SIZE + 1];
+
+#ifdef XZ_DEC_ANY_CHECK
+ if (s->check_type > XZ_CHECK_MAX)
+ return XZ_OPTIONS_ERROR;
+
+ if (s->check_type > XZ_CHECK_CRC32 && !IS_CRC64(s->check_type))
+ return XZ_UNSUPPORTED_CHECK;
+#else
+ if (s->check_type > XZ_CHECK_CRC32 && !IS_CRC64(s->check_type))
+ return XZ_OPTIONS_ERROR;
+#endif
+
+ return XZ_OK;
+}
+
+/* Decode the Stream Footer field (the last 12 bytes of the .xz Stream) */
+static enum xz_ret dec_stream_footer(struct xz_dec* s)
+{
+ if (!memeq(s->temp.buf + 10, FOOTER_MAGIC, FOOTER_MAGIC_SIZE))
+ return XZ_DATA_ERROR;
+
+ if (xz_crc32(s->temp.buf + 4, 6, 0) != get_le32(s->temp.buf))
+ return XZ_DATA_ERROR;
+
+ /*
+ * Validate Backward Size. Note that we never added the size of the
+ * Index CRC32 field to s->index.size, thus we use s->index.size / 4
+ * instead of s->index.size / 4 - 1.
+ */
+ if ((s->index.size >> 2) != get_le32(s->temp.buf + 4))
+ return XZ_DATA_ERROR;
+
+ if (s->temp.buf[8] != 0 || s->temp.buf[9] != s->check_type)
+ return XZ_DATA_ERROR;
+
+ /*
+ * Use XZ_STREAM_END instead of XZ_OK to be more convenient
+ * for the caller.
+ */
+ return XZ_STREAM_END;
+}
+
+/* Decode the Block Header and initialize the filter chain. */
+static enum xz_ret dec_block_header(struct xz_dec* s)
+{
+ enum xz_ret ret;
+
+ /*
+ * Validate the CRC32. We know that the temp buffer is at least
+ * eight bytes so this is safe.
+ */
+ s->temp.size -= 4;
+ if (xz_crc32(s->temp.buf, s->temp.size, 0) !=
+ get_le32(s->temp.buf + s->temp.size))
+ return XZ_DATA_ERROR;
+
+ s->temp.pos = 2;
+
+/*
+ * Catch unsupported Block Flags. We support only one or two filters
+ * in the chain, so we catch that with the same test.
+ */
+#ifdef XZ_DEC_BCJ
+ if (s->temp.buf[1] & 0x3E)
+#else
+ if (s->temp.buf[1] & 0x3F)
+#endif
+ return XZ_OPTIONS_ERROR;
+
+ /* Compressed Size */
+ if (s->temp.buf[1] & 0x40) {
+ if (dec_vli(s, s->temp.buf, &s->temp.pos, s->temp.size) !=
+ XZ_STREAM_END)
+ return XZ_DATA_ERROR;
+
+ s->block_header.compressed = s->vli;
+ } else {
+ s->block_header.compressed = VLI_UNKNOWN;
+ }
+
+ /* Uncompressed Size */
+ if (s->temp.buf[1] & 0x80) {
+ if (dec_vli(s, s->temp.buf, &s->temp.pos, s->temp.size) !=
+ XZ_STREAM_END)
+ return XZ_DATA_ERROR;
+
+ s->block_header.uncompressed = s->vli;
+ } else {
+ s->block_header.uncompressed = VLI_UNKNOWN;
+ }
+
+#ifdef XZ_DEC_BCJ
+ /* If there are two filters, the first one must be a BCJ filter. */
+ s->bcj_active = s->temp.buf[1] & 0x01;
+ if (s->bcj_active) {
+ if (s->temp.size - s->temp.pos < 2)
+ return XZ_OPTIONS_ERROR;
+
+ ret = xz_dec_bcj_reset(s->bcj, s->temp.buf[s->temp.pos++]);
+ if (ret != XZ_OK)
+ return ret;
+
+ /*
+ * We don't support custom start offset,
+ * so Size of Properties must be zero.
+ */
+ if (s->temp.buf[s->temp.pos++] != 0x00)
+ return XZ_OPTIONS_ERROR;
+ }
+#endif
+
+ /* Valid Filter Flags always take at least two bytes. */
+ if (s->temp.size - s->temp.pos < 2)
+ return XZ_DATA_ERROR;
+
+ /* Filter ID = LZMA2 */
+ if (s->temp.buf[s->temp.pos++] != 0x21)
+ return XZ_OPTIONS_ERROR;
+
+ /* Size of Properties = 1-byte Filter Properties */
+ if (s->temp.buf[s->temp.pos++] != 0x01)
+ return XZ_OPTIONS_ERROR;
+
+ /* Filter Properties contains LZMA2 dictionary size. */
+ if (s->temp.size - s->temp.pos < 1)
+ return XZ_DATA_ERROR;
+
+ ret = xz_dec_lzma2_reset(s->lzma2, s->temp.buf[s->temp.pos++]);
+ if (ret != XZ_OK)
+ return ret;
+
+ /* The rest must be Header Padding. */
+ while (s->temp.pos < s->temp.size)
+ if (s->temp.buf[s->temp.pos++] != 0x00)
+ return XZ_OPTIONS_ERROR;
+
+ s->temp.pos = 0;
+ s->block.compressed = 0;
+ s->block.uncompressed = 0;
+
+ return XZ_OK;
+}
+
+static enum xz_ret dec_main(struct xz_dec* s, struct xz_buf* b)
+{
+ enum xz_ret ret;
+
+ /*
+ * Store the start position for the case when we are in the middle
+ * of the Index field.
+ */
+ s->in_start = b->in_pos;
+
+ while (true) {
+ switch (s->sequence) {
+ case SEQ_STREAM_HEADER:
+ /*
+ * Stream Header is copied to s->temp, and then
+ * decoded from there. This way if the caller
+ * gives us only little input at a time, we can
+ * still keep the Stream Header decoding code
+ * simple. Similar approach is used in many places
+ * in this file.
+ */
+ if (!fill_temp(s, b))
+ return XZ_OK;
+
+ /*
+ * If dec_stream_header() returns
+ * XZ_UNSUPPORTED_CHECK, it is still possible
+ * to continue decoding if working in multi-call
+ * mode. Thus, update s->sequence before calling
+ * dec_stream_header().
+ */
+ s->sequence = SEQ_BLOCK_START;
+
+ ret = dec_stream_header(s);
+ if (ret != XZ_OK)
+ return ret;
+
+ case SEQ_BLOCK_START:
+ /* We need one byte of input to continue. */
+ if (b->in_pos == b->in_size)
+ return XZ_OK;
+
+ /* See if this is the beginning of the Index field. */
+ if (b->in[b->in_pos] == 0) {
+ s->in_start = b->in_pos++;
+ s->sequence = SEQ_INDEX;
+ break;
+ }
+
+ /*
+ * Calculate the size of the Block Header and
+ * prepare to decode it.
+ */
+ s->block_header.size = ((uint32_t)b->in[b->in_pos] + 1) * 4;
+
+ s->temp.size = s->block_header.size;
+ s->temp.pos = 0;
+ s->sequence = SEQ_BLOCK_HEADER;
+
+ case SEQ_BLOCK_HEADER:
+ if (!fill_temp(s, b))
+ return XZ_OK;
+
+ ret = dec_block_header(s);
+ if (ret != XZ_OK)
+ return ret;
+
+ s->sequence = SEQ_BLOCK_UNCOMPRESS;
+
+ case SEQ_BLOCK_UNCOMPRESS:
+ ret = dec_block(s, b);
+ if (ret != XZ_STREAM_END)
+ return ret;
+
+ s->sequence = SEQ_BLOCK_PADDING;
+
+ case SEQ_BLOCK_PADDING:
+ /*
+ * Size of Compressed Data + Block Padding
+ * must be a multiple of four. We don't need
+ * s->block.compressed for anything else
+ * anymore, so we use it here to test the size
+ * of the Block Padding field.
+ */
+ while (s->block.compressed & 3) {
+ if (b->in_pos == b->in_size)
+ return XZ_OK;
+
+ if (b->in[b->in_pos++] != 0)
+ return XZ_DATA_ERROR;
+
+ ++s->block.compressed;
+ }
+
+ s->sequence = SEQ_BLOCK_CHECK;
+
+ case SEQ_BLOCK_CHECK:
+ if (s->check_type == XZ_CHECK_CRC32) {
+ ret = crc_validate(s, b, 32);
+ if (ret != XZ_STREAM_END)
+ return ret;
+ } else if (IS_CRC64(s->check_type)) {
+ ret = crc_validate(s, b, 64);
+ if (ret != XZ_STREAM_END)
+ return ret;
+ }
+#ifdef XZ_DEC_ANY_CHECK
+ else if (!check_skip(s, b)) {
+ return XZ_OK;
+ }
+#endif
+
+ s->sequence = SEQ_BLOCK_START;
+ break;
+
+ case SEQ_INDEX:
+ ret = dec_index(s, b);
+ if (ret != XZ_STREAM_END)
+ return ret;
+
+ s->sequence = SEQ_INDEX_PADDING;
+
+ case SEQ_INDEX_PADDING:
+ while ((s->index.size + (b->in_pos - s->in_start)) & 3) {
+ if (b->in_pos == b->in_size) {
+ index_update(s, b);
+ return XZ_OK;
+ }
+
+ if (b->in[b->in_pos++] != 0)
+ return XZ_DATA_ERROR;
+ }
+
+ /* Finish the CRC32 value and Index size. */
+ index_update(s, b);
+
+ /* Compare the hashes to validate the Index field. */
+ if (!memeq(&s->block.hash, &s->index.hash,
+ sizeof(s->block.hash)))
+ return XZ_DATA_ERROR;
+
+ s->sequence = SEQ_INDEX_CRC32;
+
+ case SEQ_INDEX_CRC32:
+ ret = crc_validate(s, b, 32);
+ if (ret != XZ_STREAM_END)
+ return ret;
+
+ s->temp.size = STREAM_HEADER_SIZE;
+ s->sequence = SEQ_STREAM_FOOTER;
+
+ case SEQ_STREAM_FOOTER:
+ if (!fill_temp(s, b))
+ return XZ_OK;
+
+ return dec_stream_footer(s);
+ }
+ }
+
+ /* Never reached */
+}
+
+/*
+ * xz_dec_run() is a wrapper for dec_main() to handle some special cases in
+ * multi-call and single-call decoding.
+ *
+ * In multi-call mode, we must return XZ_BUF_ERROR when it seems clear that we
+ * are not going to make any progress anymore. This is to prevent the caller
+ * from calling us infinitely when the input file is truncated or otherwise
+ * corrupt. Since zlib-style API allows that the caller fills the input buffer
+ * only when the decoder doesn't produce any new output, we have to be careful
+ * to avoid returning XZ_BUF_ERROR too easily: XZ_BUF_ERROR is returned only
+ * after the second consecutive call to xz_dec_run() that makes no progress.
+ *
+ * In single-call mode, if we couldn't decode everything and no error
+ * occurred, either the input is truncated or the output buffer is too small.
+ * Since we know that the last input byte never produces any output, we know
+ * that if all the input was consumed and decoding wasn't finished, the file
+ * must be corrupt. Otherwise the output buffer has to be too small or the
+ * file is corrupt in a way that decoding it produces too big output.
+ *
+ * If single-call decoding fails, we reset b->in_pos and b->out_pos back to
+ * their original values. This is because with some filter chains there won't
+ * be any valid uncompressed data in the output buffer unless the decoding
+ * actually succeeds (that's the price to pay of using the output buffer as
+ * the workspace).
+ */
+XZ_EXTERN enum xz_ret xz_dec_run(struct xz_dec* s, struct xz_buf* b)
+{
+ size_t in_start;
+ size_t out_start;
+ enum xz_ret ret;
+
+ if (DEC_IS_SINGLE(s->mode))
+ xz_dec_reset(s);
+
+ in_start = b->in_pos;
+ out_start = b->out_pos;
+ ret = dec_main(s, b);
+
+ if (DEC_IS_SINGLE(s->mode)) {
+ if (ret == XZ_OK)
+ ret = b->in_pos == b->in_size ? XZ_DATA_ERROR : XZ_BUF_ERROR;
+
+ if (ret != XZ_STREAM_END) {
+ b->in_pos = in_start;
+ b->out_pos = out_start;
+ }
+ } else if (ret == XZ_OK && in_start == b->in_pos &&
+ out_start == b->out_pos) {
+ if (s->allow_buf_error)
+ ret = XZ_BUF_ERROR;
+
+ s->allow_buf_error = true;
+ } else {
+ s->allow_buf_error = false;
+ }
+
+ return ret;
+}
+
+XZ_EXTERN struct xz_dec* xz_dec_init(enum xz_mode mode, uint32_t dict_max)
+{
+ struct xz_dec* s = kmalloc(sizeof(*s), GFP_KERNEL);
+ if (s == NULL)
+ return NULL;
+
+ s->mode = mode;
+
+#ifdef XZ_DEC_BCJ
+ s->bcj = xz_dec_bcj_create(DEC_IS_SINGLE(mode));
+ if (s->bcj == NULL)
+ goto error_bcj;
+#endif
+
+ s->lzma2 = xz_dec_lzma2_create(mode, dict_max);
+ if (s->lzma2 == NULL)
+ goto error_lzma2;
+
+ xz_dec_reset(s);
+ return s;
+
+error_lzma2:
+#ifdef XZ_DEC_BCJ
+ xz_dec_bcj_end(s->bcj);
+error_bcj:
+#endif
+ kfree(s);
+ return NULL;
+}
+
+XZ_EXTERN void xz_dec_reset(struct xz_dec* s)
+{
+ s->sequence = SEQ_STREAM_HEADER;
+ s->allow_buf_error = false;
+ s->pos = 0;
+ s->crc = 0;
+ memzero(&s->block, sizeof(s->block));
+ memzero(&s->index, sizeof(s->index));
+ s->temp.pos = 0;
+ s->temp.size = STREAM_HEADER_SIZE;
+}
+
+XZ_EXTERN void xz_dec_end(struct xz_dec* s)
+{
+ if (s != NULL) {
+ xz_dec_lzma2_end(s->lzma2);
+#ifdef XZ_DEC_BCJ
+ xz_dec_bcj_end(s->bcj);
+#endif
+ kfree(s);
+ }
+}
diff --git a/meshmc/libraries/xz-embedded/src/xz_lzma2.h b/meshmc/libraries/xz-embedded/src/xz_lzma2.h
new file mode 100644
index 0000000000..fa3fe191f8
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_lzma2.h
@@ -0,0 +1,205 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * LZMA2 definitions
+ *
+ * Authors: Lasse Collin <lasse.collin@tukaani.org>
+ * Igor Pavlov <http://7-zip.org/>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#ifndef XZ_LZMA2_H
+#define XZ_LZMA2_H
+
+/* Range coder constants */
+#define RC_SHIFT_BITS 8
+#define RC_TOP_BITS 24
+#define RC_TOP_VALUE (1 << RC_TOP_BITS)
+#define RC_BIT_MODEL_TOTAL_BITS 11
+#define RC_BIT_MODEL_TOTAL (1 << RC_BIT_MODEL_TOTAL_BITS)
+#define RC_MOVE_BITS 5
+
+/*
+ * Maximum number of position states. A position state is the lowest pb
+ * number of bits of the current uncompressed offset. In some places there
+ * are different sets of probabilities for different position states.
+ */
+#define POS_STATES_MAX (1 << 4)
+
+/*
+ * This enum is used to track which LZMA symbols have occurred most recently
+ * and in which order. This information is used to predict the next symbol.
+ *
+ * Symbols:
+ * - Literal: One 8-bit byte
+ * - Match: Repeat a chunk of data at some distance
+ * - Long repeat: Multi-byte match at a recently seen distance
+ * - Short repeat: One-byte repeat at a recently seen distance
+ *
+ * The symbol names are in from STATE_oldest_older_previous. REP means
+ * either short or long repeated match, and NONLIT means any non-literal.
+ */
+enum lzma_state {
+ STATE_LIT_LIT,
+ STATE_MATCH_LIT_LIT,
+ STATE_REP_LIT_LIT,
+ STATE_SHORTREP_LIT_LIT,
+ STATE_MATCH_LIT,
+ STATE_REP_LIT,
+ STATE_SHORTREP_LIT,
+ STATE_LIT_MATCH,
+ STATE_LIT_LONGREP,
+ STATE_LIT_SHORTREP,
+ STATE_NONLIT_MATCH,
+ STATE_NONLIT_REP
+};
+
+/* Total number of states */
+#define STATES 12
+
+/* The lowest 7 states indicate that the previous state was a literal. */
+#define LIT_STATES 7
+
+/* Indicate that the latest symbol was a literal. */
+static inline void lzma_state_literal(enum lzma_state* state)
+{
+ if (*state <= STATE_SHORTREP_LIT_LIT)
+ *state = STATE_LIT_LIT;
+ else if (*state <= STATE_LIT_SHORTREP)
+ *state -= 3;
+ else
+ *state -= 6;
+}
+
+/* Indicate that the latest symbol was a match. */
+static inline void lzma_state_match(enum lzma_state* state)
+{
+ *state = *state < LIT_STATES ? STATE_LIT_MATCH : STATE_NONLIT_MATCH;
+}
+
+/* Indicate that the latest state was a long repeated match. */
+static inline void lzma_state_long_rep(enum lzma_state* state)
+{
+ *state = *state < LIT_STATES ? STATE_LIT_LONGREP : STATE_NONLIT_REP;
+}
+
+/* Indicate that the latest symbol was a short match. */
+static inline void lzma_state_short_rep(enum lzma_state* state)
+{
+ *state = *state < LIT_STATES ? STATE_LIT_SHORTREP : STATE_NONLIT_REP;
+}
+
+/* Test if the previous symbol was a literal. */
+static inline bool lzma_state_is_literal(enum lzma_state state)
+{
+ return state < LIT_STATES;
+}
+
+/* Each literal coder is divided in three sections:
+ * - 0x001-0x0FF: Without match byte
+ * - 0x101-0x1FF: With match byte; match bit is 0
+ * - 0x201-0x2FF: With match byte; match bit is 1
+ *
+ * Match byte is used when the previous LZMA symbol was something else than
+ * a literal (that is, it was some kind of match).
+ */
+#define LITERAL_CODER_SIZE 0x300
+
+/* Maximum number of literal coders */
+#define LITERAL_CODERS_MAX (1 << 4)
+
+/* Minimum length of a match is two bytes. */
+#define MATCH_LEN_MIN 2
+
+/* Match length is encoded with 4, 5, or 10 bits.
+ *
+ * Length Bits
+ * 2-9 4 = Choice=0 + 3 bits
+ * 10-17 5 = Choice=1 + Choice2=0 + 3 bits
+ * 18-273 10 = Choice=1 + Choice2=1 + 8 bits
+ */
+#define LEN_LOW_BITS 3
+#define LEN_LOW_SYMBOLS (1 << LEN_LOW_BITS)
+#define LEN_MID_BITS 3
+#define LEN_MID_SYMBOLS (1 << LEN_MID_BITS)
+#define LEN_HIGH_BITS 8
+#define LEN_HIGH_SYMBOLS (1 << LEN_HIGH_BITS)
+#define LEN_SYMBOLS (LEN_LOW_SYMBOLS + LEN_MID_SYMBOLS + LEN_HIGH_SYMBOLS)
+
+/*
+ * Maximum length of a match is 273 which is a result of the encoding
+ * described above.
+ */
+#define MATCH_LEN_MAX (MATCH_LEN_MIN + LEN_SYMBOLS - 1)
+
+/*
+ * Different sets of probabilities are used for match distances that have
+ * very short match length: Lengths of 2, 3, and 4 bytes have a separate
+ * set of probabilities for each length. The matches with longer length
+ * use a shared set of probabilities.
+ */
+#define DIST_STATES 4
+
+/*
+ * Get the index of the appropriate probability array for decoding
+ * the distance slot.
+ */
+static inline uint32_t lzma_get_dist_state(uint32_t len)
+{
+ return len < DIST_STATES + MATCH_LEN_MIN ? len - MATCH_LEN_MIN
+ : DIST_STATES - 1;
+}
+
+/*
+ * The highest two bits of a 32-bit match distance are encoded using six bits.
+ * This six-bit value is called a distance slot. This way encoding a 32-bit
+ * value takes 6-36 bits, larger values taking more bits.
+ */
+#define DIST_SLOT_BITS 6
+#define DIST_SLOTS (1 << DIST_SLOT_BITS)
+
+/* Match distances up to 127 are fully encoded using probabilities. Since
+ * the highest two bits (distance slot) are always encoded using six bits,
+ * the distances 0-3 don't need any additional bits to encode, since the
+ * distance slot itself is the same as the actual distance. DIST_MODEL_START
+ * indicates the first distance slot where at least one additional bit is
+ * needed.
+ */
+#define DIST_MODEL_START 4
+
+/*
+ * Match distances greater than 127 are encoded in three pieces:
+ * - distance slot: the highest two bits
+ * - direct bits: 2-26 bits below the highest two bits
+ * - alignment bits: four lowest bits
+ *
+ * Direct bits don't use any probabilities.
+ *
+ * The distance slot value of 14 is for distances 128-191.
+ */
+#define DIST_MODEL_END 14
+
+/* Distance slots that indicate a distance <= 127. */
+#define FULL_DISTANCES_BITS (DIST_MODEL_END / 2)
+#define FULL_DISTANCES (1 << FULL_DISTANCES_BITS)
+
+/*
+ * For match distances greater than 127, only the highest two bits and the
+ * lowest four bits (alignment) is encoded using probabilities.
+ */
+#define ALIGN_BITS 4
+#define ALIGN_SIZE (1 << ALIGN_BITS)
+#define ALIGN_MASK (ALIGN_SIZE - 1)
+
+/* Total number of all probability variables */
+#define PROBS_TOTAL (1846 + LITERAL_CODERS_MAX * LITERAL_CODER_SIZE)
+
+/*
+ * LZMA remembers the four most recent match distances. Reusing these
+ * distances tends to take less space than re-encoding the actual
+ * distance value.
+ */
+#define REPS 4
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/src/xz_private.h b/meshmc/libraries/xz-embedded/src/xz_private.h
new file mode 100644
index 0000000000..eabb901d7e
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_private.h
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * Private includes and definitions
+ *
+ * Author: Lasse Collin <lasse.collin@tukaani.org>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#ifndef XZ_PRIVATE_H
+#define XZ_PRIVATE_H
+
+#ifdef __KERNEL__
+#include <linux/xz.h>
+#include <linux/kernel.h>
+#include <asm/unaligned.h>
+/* XZ_PREBOOT may be defined only via decompress_unxz.c. */
+#ifndef XZ_PREBOOT
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+#include <linux/string.h>
+#ifdef CONFIG_XZ_DEC_X86
+#define XZ_DEC_X86
+#endif
+#ifdef CONFIG_XZ_DEC_POWERPC
+#define XZ_DEC_POWERPC
+#endif
+#ifdef CONFIG_XZ_DEC_IA64
+#define XZ_DEC_IA64
+#endif
+#ifdef CONFIG_XZ_DEC_ARM
+#define XZ_DEC_ARM
+#endif
+#ifdef CONFIG_XZ_DEC_ARMTHUMB
+#define XZ_DEC_ARMTHUMB
+#endif
+#ifdef CONFIG_XZ_DEC_SPARC
+#define XZ_DEC_SPARC
+#endif
+#define memeq(a, b, size) (memcmp(a, b, size) == 0)
+#define memzero(buf, size) memset(buf, 0, size)
+#endif
+#define get_le32(p) le32_to_cpup((const uint32_t*)(p))
+#else
+/*
+ * For userspace builds, use a separate header to define the required
+ * macros and functions. This makes it easier to adapt the code into
+ * different environments and avoids clutter in the Linux kernel tree.
+ */
+#include "xz_config.h"
+#endif
+
+/* If no specific decoding mode is requested, enable support for all modes. */
+#if !defined(XZ_DEC_SINGLE) && !defined(XZ_DEC_PREALLOC) && \
+ !defined(XZ_DEC_DYNALLOC)
+#define XZ_DEC_SINGLE
+#define XZ_DEC_PREALLOC
+#define XZ_DEC_DYNALLOC
+#endif
+
+/*
+ * The DEC_IS_foo(mode) macros are used in "if" statements. If only some
+ * of the supported modes are enabled, these macros will evaluate to true or
+ * false at compile time and thus allow the compiler to omit unneeded code.
+ */
+#ifdef XZ_DEC_SINGLE
+#define DEC_IS_SINGLE(mode) ((mode) == XZ_SINGLE)
+#else
+#define DEC_IS_SINGLE(mode) (false)
+#endif
+
+#ifdef XZ_DEC_PREALLOC
+#define DEC_IS_PREALLOC(mode) ((mode) == XZ_PREALLOC)
+#else
+#define DEC_IS_PREALLOC(mode) (false)
+#endif
+
+#ifdef XZ_DEC_DYNALLOC
+#define DEC_IS_DYNALLOC(mode) ((mode) == XZ_DYNALLOC)
+#else
+#define DEC_IS_DYNALLOC(mode) (false)
+#endif
+
+#if !defined(XZ_DEC_SINGLE)
+#define DEC_IS_MULTI(mode) (true)
+#elif defined(XZ_DEC_PREALLOC) || defined(XZ_DEC_DYNALLOC)
+#define DEC_IS_MULTI(mode) ((mode) != XZ_SINGLE)
+#else
+#define DEC_IS_MULTI(mode) (false)
+#endif
+
+/*
+ * If any of the BCJ filter decoders are wanted, define XZ_DEC_BCJ.
+ * XZ_DEC_BCJ is used to enable generic support for BCJ decoders.
+ */
+#ifndef XZ_DEC_BCJ
+#if defined(XZ_DEC_X86) || defined(XZ_DEC_POWERPC) || defined(XZ_DEC_IA64) || \
+ defined(XZ_DEC_ARM) || defined(XZ_DEC_ARM) || defined(XZ_DEC_ARMTHUMB) || \
+ defined(XZ_DEC_SPARC)
+#define XZ_DEC_BCJ
+#endif
+#endif
+
+/*
+ * Allocate memory for LZMA2 decoder. xz_dec_lzma2_reset() must be used
+ * before calling xz_dec_lzma2_run().
+ */
+XZ_EXTERN struct xz_dec_lzma2* xz_dec_lzma2_create(enum xz_mode mode,
+ uint32_t dict_max);
+
+/*
+ * Decode the LZMA2 properties (one byte) and reset the decoder. Return
+ * XZ_OK on success, XZ_MEMLIMIT_ERROR if the preallocated dictionary is not
+ * big enough, and XZ_OPTIONS_ERROR if props indicates something that this
+ * decoder doesn't support.
+ */
+XZ_EXTERN enum xz_ret xz_dec_lzma2_reset(struct xz_dec_lzma2* s, uint8_t props);
+
+/* Decode raw LZMA2 stream from b->in to b->out. */
+XZ_EXTERN enum xz_ret xz_dec_lzma2_run(struct xz_dec_lzma2* s,
+ struct xz_buf* b);
+
+/* Free the memory allocated for the LZMA2 decoder. */
+XZ_EXTERN void xz_dec_lzma2_end(struct xz_dec_lzma2* s);
+
+#ifdef XZ_DEC_BCJ
+/*
+ * Allocate memory for BCJ decoders. xz_dec_bcj_reset() must be used before
+ * calling xz_dec_bcj_run().
+ */
+XZ_EXTERN struct xz_dec_bcj* xz_dec_bcj_create(bool single_call);
+
+/*
+ * Decode the Filter ID of a BCJ filter. This implementation doesn't
+ * support custom start offsets, so no decoding of Filter Properties
+ * is needed. Returns XZ_OK if the given Filter ID is supported.
+ * Otherwise XZ_OPTIONS_ERROR is returned.
+ */
+XZ_EXTERN enum xz_ret xz_dec_bcj_reset(struct xz_dec_bcj* s, uint8_t id);
+
+/*
+ * Decode raw BCJ + LZMA2 stream. This must be used only if there actually is
+ * a BCJ filter in the chain. If the chain has only LZMA2, xz_dec_lzma2_run()
+ * must be called directly.
+ */
+XZ_EXTERN enum xz_ret xz_dec_bcj_run(struct xz_dec_bcj* s,
+ struct xz_dec_lzma2* lzma2,
+ struct xz_buf* b);
+
+/* Free the memory allocated for the BCJ filters. */
+#define xz_dec_bcj_end(s) kfree(s)
+#endif
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/src/xz_stream.h b/meshmc/libraries/xz-embedded/src/xz_stream.h
new file mode 100644
index 0000000000..8e962fbda6
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/src/xz_stream.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * Definitions for handling the .xz file format
+ *
+ * Author: Lasse Collin <lasse.collin@tukaani.org>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+#ifndef XZ_STREAM_H
+#define XZ_STREAM_H
+
+#if defined(__KERNEL__) && !XZ_INTERNAL_CRC32
+#include <linux/crc32.h>
+#undef crc32
+#define xz_crc32(buf, size, crc) (~crc32_le(~(uint32_t)(crc), buf, size))
+#endif
+
+/*
+ * See the .xz file format specification at
+ * http://tukaani.org/xz/xz-file-format.txt
+ * to understand the container format.
+ */
+
+#define STREAM_HEADER_SIZE 12
+
+#define HEADER_MAGIC "\3757zXZ"
+#define HEADER_MAGIC_SIZE 6
+
+#define FOOTER_MAGIC "YZ"
+#define FOOTER_MAGIC_SIZE 2
+
+/*
+ * Variable-length integer can hold a 63-bit unsigned integer or a special
+ * value indicating that the value is unknown.
+ *
+ * Experimental: vli_type can be defined to uint32_t to save a few bytes
+ * in code size (no effect on speed). Doing so limits the uncompressed and
+ * compressed size of the file to less than 256 MiB and may also weaken
+ * error detection slightly.
+ */
+typedef uint64_t vli_type;
+
+#define VLI_MAX ((vli_type) - 1 / 2)
+#define VLI_UNKNOWN ((vli_type) - 1)
+
+/* Maximum encoded size of a VLI */
+#define VLI_BYTES_MAX (sizeof(vli_type) * 8 / 7)
+
+/* Integrity Check types */
+enum xz_check {
+ XZ_CHECK_NONE = 0,
+ XZ_CHECK_CRC32 = 1,
+ XZ_CHECK_CRC64 = 4,
+ XZ_CHECK_SHA256 = 10
+};
+
+/* Maximum possible Check ID */
+#define XZ_CHECK_MAX 15
+
+#endif
diff --git a/meshmc/libraries/xz-embedded/xzminidec.c b/meshmc/libraries/xz-embedded/xzminidec.c
new file mode 100644
index 0000000000..ba69be26e4
--- /dev/null
+++ b/meshmc/libraries/xz-embedded/xzminidec.c
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: Unlicense
+ * SPDX-FileCopyrightText: N/A
+ * Simple XZ decoder command line tool
+ *
+ * Author: Lasse Collin <lasse.collin@tukaani.org>
+ *
+ * This file has been put into the public domain.
+ * You can do whatever you want with this file.
+ */
+
+/*
+ * This is really limited: Not all filters from .xz format are supported,
+ * only CRC32 is supported as the integrity check, and decoding of
+ * concatenated .xz streams is not supported. Thus, you may want to look
+ * at xzdec from XZ Utils if a few KiB bigger tool is not a problem.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include "xz.h"
+
+static uint8_t in[BUFSIZ];
+static uint8_t out[BUFSIZ];
+
+int main(int argc, char** argv)
+{
+ struct xz_buf b;
+ struct xz_dec* s;
+ enum xz_ret ret;
+ const char* msg;
+
+ if (argc >= 2 && strcmp(argv[1], "--help") == 0) {
+ fputs("Uncompress a .xz file from stdin to stdout.\n"
+ "Arguments other than `--help' are ignored.\n",
+ stdout);
+ return 0;
+ }
+
+ xz_crc32_init();
+#ifdef XZ_USE_CRC64
+ xz_crc64_init();
+#endif
+
+ /*
+ * Support up to 64 MiB dictionary. The actually needed memory
+ * is allocated once the headers have been parsed.
+ */
+ s = xz_dec_init(XZ_DYNALLOC, 1 << 26);
+ if (s == NULL) {
+ msg = "Memory allocation failed\n";
+ goto error;
+ }
+
+ b.in = in;
+ b.in_pos = 0;
+ b.in_size = 0;
+ b.out = out;
+ b.out_pos = 0;
+ b.out_size = BUFSIZ;
+
+ while (true) {
+ if (b.in_pos == b.in_size) {
+ b.in_size = fread(in, 1, sizeof(in), stdin);
+ b.in_pos = 0;
+ }
+
+ ret = xz_dec_run(s, &b);
+
+ if (b.out_pos == sizeof(out)) {
+ if (fwrite(out, 1, b.out_pos, stdout) != b.out_pos) {
+ msg = "Write error\n";
+ goto error;
+ }
+
+ b.out_pos = 0;
+ }
+
+ if (ret == XZ_OK)
+ continue;
+
+#ifdef XZ_DEC_ANY_CHECK
+ if (ret == XZ_UNSUPPORTED_CHECK) {
+ fputs(argv[0], stderr);
+ fputs(": ", stderr);
+ fputs("Unsupported check; not verifying "
+ "file integrity\n",
+ stderr);
+ continue;
+ }
+#endif
+
+ if (fwrite(out, 1, b.out_pos, stdout) != b.out_pos || fclose(stdout)) {
+ msg = "Write error\n";
+ goto error;
+ }
+
+ switch (ret) {
+ case XZ_STREAM_END:
+ xz_dec_end(s);
+ return 0;
+
+ case XZ_MEM_ERROR:
+ msg = "Memory allocation failed\n";
+ goto error;
+
+ case XZ_MEMLIMIT_ERROR:
+ msg = "Memory usage limit reached\n";
+ goto error;
+
+ case XZ_FORMAT_ERROR:
+ msg = "Not a .xz file\n";
+ goto error;
+
+ case XZ_OPTIONS_ERROR:
+ msg = "Unsupported options in the .xz headers\n";
+ goto error;
+
+ case XZ_DATA_ERROR:
+ case XZ_BUF_ERROR:
+ msg = "File is corrupt\n";
+ goto error;
+
+ default:
+ msg = "Bug!\n";
+ goto error;
+ }
+ }
+
+error:
+ xz_dec_end(s);
+ fputs(argv[0], stderr);
+ fputs(": ", stderr);
+ fputs(msg, stderr);
+ return 1;
+}