diff options
Diffstat (limited to 'meshmc/libraries')
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, "&"); + break; + + /* the single quote is a valid URL character + * according to the standard; it needs HTML + * entity escaping too */ + case '\'': + HOEDOWN_BUFPUTSL(ob, "'"); + 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: + * + * & --> & + * < --> < + * > --> > + * " --> " + * ' --> ' ' is not recommended + * / --> / 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[] = {"", """, "&", "'", + "/", "<", ">"}; + +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, " <a href=\"#fnref%d\" rev=\"footnote\">↩</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 "'" 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[] = {"'", "'", "'", "'", 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. ' or ' or '. + '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 “ or ” */ + 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, "’"); + 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, "’"); + 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, "©"); + return 2; + } + + if (t1 == 'r' && t2 == ')') { + HOEDOWN_BUFPUTSL(ob, "®"); + return 2; + } + + if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') { + HOEDOWN_BUFPUTSL(ob, "™"); + 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, "—"); + return 2; + } + + if (size >= 2 && text[1] == '-') { + HOEDOWN_BUFPUTSL(ob, "–"); + return 1; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts " 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, """, 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, "�", 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, "…"); + return 2; + } + + if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && + text[4] == '.') { + HOEDOWN_BUFPUTSL(ob, "…"); + 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, "½"); + 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, "¼"); + 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, "¾"); + 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, """); + + 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>", "’", 0 }, + { '\'', "'t>", "’", 0 }, + { '\'', "'re>", "’", 0 }, + { '\'', "'ll>", "’", 0 }, + { '\'', "'ve>", "’", 0 }, + { '\'', "'m>", "’", 0 }, + { '\'', "'d>", "’", 0 }, + { '-', "--", "—", 1 }, + { '-', "<->", "–", 0 }, + { '.', "...", "…", 2 }, + { '.', ". . .", "…", 4 }, + { '(', "(c)", "©", 2 }, + { '(', "(r)", "®", 2 }, + { '(', "(tm)", "™", 3 }, + { '3', "<3/4>", "¾", 2 }, + { '3', "<3/4ths>", "¾", 2 }, + { '1', "<1/2>", "½", 2 }, + { '1', "<1/4>", "¼", 2 }, + { '1', "<1/4th>", "¼", 2 }, + { '&', "�", 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; +} |
