fabric/utils/misc.cpp

fabric/utils/misc.cpp

fabric/utils/misc.cpp

Namespaces

Name
Syntalos

Source code

/*
 * Copyright (C) 2016-2026 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 3
 *
 * This program 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 3 of the license, or
 * (at your option) any later version.
 *
 * This software 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 software.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "misc.h"
#include "config.h"
#include "datactl/edlutils.h"

#include <filesystem>
#include <linux/magic.h>
#include <sys/vfs.h>
#include <blake3.h>

#include <QCoreApplication>
#include <QDir>
#include <QRandomGenerator>
#include <QStandardPaths>
#include <QThread>
#include <QTime>
#include <QProcessEnvironment>

namespace fs = std::filesystem;

namespace Syntalos
{
QString createRandomString(int len)
{
    const auto possibleChars = QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");

    QString str;
    for (int i = 0; i < len; i++) {
        int index = QRandomGenerator::global()->generate() % possibleChars.length();
        QChar nextChar = possibleChars.at(index);
        str.append(nextChar);
    }

    return str;
}

QString simplifyStrForModuleName(const QString &s)
{
    const auto tmp = s.simplified().replace("/", "_").replace("\\", "_");
    if (tmp.isEmpty())
        return QStringLiteral("Unnamed");
    return tmp;
}

QString simplifyStrForFileBasename(const QString &s, bool lowerCase, uint maxLen)
{
    // Qt handles the Unicode-aware whitespace collapsing and lowercasing; makeCompactName()
    // does the rest. When lowercased we dash-separate words (camelCase is lost once lowercased),
    // otherwise we join them (camelCase stays readable).
    auto base = s.simplified();
    if (lowerCase)
        base = base.toLower();
    return QString::fromStdString(
        edl::makeCompactName(
            base.toStdString(),
            {.maxLength = maxLen, .fallback = "unnamed", .wordSeparator = lowerCase ? '-' : '\0'}));
}

std::string simplifyStrForFileBasename(const std::string &s, bool lowerCase, uint maxLen)
{
    auto base = QString::fromStdString(s).simplified();
    if (lowerCase)
        base = base.toLower();
    return edl::makeCompactName(
        base.toStdString(),
        {.maxLength = maxLen, .fallback = "unnamed", .wordSeparator = lowerCase ? '-' : '\0'});
}

QStringList qStringSplitLimit(const QString &str, const QChar &sep, int maxSplit, Qt::CaseSensitivity cs)
{
    QStringList list;
    int start = 0;
    int end;
    while ((end = str.indexOf(sep, start, cs)) != -1) {
        if (start != end)
            list.append(str.mid(start, end - start));
        start = end + 1;
        if (maxSplit > 0) {
            if (list.length() > maxSplit)
                break;
        }
    }
    if (start != str.size())
        list.append(str.mid(start));
    return list;
}

QString syntalosVersionFull()
{
    auto syVersion = QStringLiteral(PROJECT_VERSION);
    auto syVcs = QStringLiteral(SY_VCS_TAG).replace(syVersion, "");
    if (syVcs.contains("-"))
        syVcs = syVcs.section('-', 1);
    if (syVcs.startsWith("v"))
        syVcs.remove(0, 1);
    if (syVcs == QStringLiteral("+")) {
        syVersion = syVersion + QStringLiteral("+");
        syVcs = "";
    }

    return syVcs.isEmpty() ? syVersion : QStringLiteral("%1 (%2)").arg(syVersion, syVcs);
}

QString syntalosVersion()
{
    return QStringLiteral(PROJECT_VERSION);
}

bool isInFlatpakSandbox()
{
    if (qEnvironmentVariable("container") == QStringLiteral("flatpak"))
        return true;

    // We check for FLATPAK_ID as well to make this function work for older versions
    // of Flatpak. 1.14.4 or higher is confirmed to not need this check.
    if (qEnvironmentVariable("FLATPAK_ID").startsWith("org.syntalos"))
        return true;
    return false;
}

QString findHostFile(const QString &path)
{
    if (isInFlatpakSandbox()) {
        const auto hostPath = fs::path(QStringLiteral("/run/host/%1").arg(path).toStdString()).lexically_normal();
        if (fs::exists(hostPath))
            return QString::fromStdString(hostPath.string());
    } else {
        if (fs::exists(fs::path(path.toStdString())))
            return path;
    }
    return QString();
}

bool hostUdevRuleExists(const QString &ruleFilename)
{
    QStringList udevPaths = {"/lib/udev/rules.d", "/usr/lib/udev/rules.d", "/etc/udev/rules.d"};
    for (const auto &root : udevPaths) {
        if (!findHostFile(root + "/" + ruleFilename).isEmpty())
            return true;
    }
    return false;
}

QString appDataRootDir()
{
    if (isInFlatpakSandbox())
        return QDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation))
            .filePath(QStringLiteral(".var/app/org.syntalos.syntalos/data"));

    // NOTE: We deliberately do not use AppDataLocation here, as that depends on the
    // application name and helper binaries (e.g. the encode helper) must share this directory.
    return QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))
        .filePath(QStringLiteral("Syntalos"));
}

QString tempDirRoot()
{
    return QDir::tempPath();
}

static bool isFileOnTmpfs(const QString &fname)
{
    struct statfs info;
    statfs(qPrintable(fname), &info);

    if (info.f_type == TMPFS_MAGIC)
        return true;
    return false;
}

QString tempDirLargeRoot()
{
    QString tmpDir = QStringLiteral("/var/tmp");
    if (fs::exists(tmpDir.toStdString()) && !isFileOnTmpfs(tmpDir))
        return tmpDir;

    tmpDir = tempDirRoot();
    if (!isFileOnTmpfs(tmpDir))
        return tmpDir;

    return QStandardPaths::writableLocation(QStandardPaths::TempLocation);
}

void delay(int waitMsec)
{
    if (waitMsec <= 54) {
        // if it's just a short wait, we don't bother with the event loop
        QThread::usleep(waitMsec * 1000);
        return;
    }

    QTime doneTime = QTime::currentTime().addMSecs(waitMsec);
    while (QTime::currentTime() < doneTime) {
        QThread::usleep(500);
        QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
    }
}

bool isBinaryInPath(const QString &binaryName)
{
    QString path = QProcessEnvironment::systemEnvironment().value("PATH");
    QStringList directories = path.split(QDir::listSeparator());

    // check for the binary in all directories in PATH
    for (const QString &dir : directories) {
        QFileInfo fileInfo(QDir(dir), binaryName);
        if (fileInfo.exists() && fileInfo.isExecutable()) {
            return true;
        }
    }

    return false;
}

QByteArray blake3HashForData(const QByteArray &data)
{
    blake3_hasher hasher;
    blake3_hasher_init(&hasher);
    blake3_hasher_update(&hasher, data.constData(), data.size());

    uint8_t out[BLAKE3_OUT_LEN];
    blake3_hasher_finalize(&hasher, out, BLAKE3_OUT_LEN);

    return {reinterpret_cast<const char *>(out), BLAKE3_OUT_LEN};
}

auto blake3HashForFile(const QString &filename) -> std::expected<QByteArray, QString>
{
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly))
        return std::unexpected(
            QStringLiteral("Unable to open file %1 for hashing: %2").arg(filename, file.errorString()));

    const auto data = file.readAll();
    blake3_hasher hasher;
    blake3_hasher_init(&hasher);
    blake3_hasher_update(&hasher, data.constData(), data.size());

    uint8_t out[BLAKE3_OUT_LEN];
    blake3_hasher_finalize(&hasher, out, BLAKE3_OUT_LEN);

    return QByteArray(reinterpret_cast<const char *>(out), BLAKE3_OUT_LEN);
}

QString formatByteSize(qint64 bytes)
{
    return QLocale().formattedDataSize(bytes, 1, QLocale::DataSizeSIFormat);
}

QString formatApproxDuration(double seconds)
{
    if (seconds < 60)
        return QStringLiteral("less than a minute");
    if (seconds < 90 * 60)
        return QStringLiteral("about %1 minutes").arg(qRound(seconds / 60.0));
    if (seconds < 48 * 3600)
        return QStringLiteral("about %1 hours").arg(seconds / 3600.0, 0, 'f', 1);
    return QStringLiteral("about %1 days").arg(seconds / (24 * 3600.0), 0, 'f', 1);
}

} // namespace Syntalos

Updated on 2026-09-06 at 20:30:11 +0000