datactl/edlutils.cpp

datactl/edlutils.cpp

datactl/edlutils.cpp

Namespaces

Name
Syntalos
Syntalos::edl

Source code

/*
 * Copyright (C) 2025-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 "edlutils.h"

#include <gio/gio.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <random>
#include <string>
#include <vector>

namespace Syntalos::edl
{

std::string createRandomString(size_t len)
{
    static constexpr std::string_view chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    std::random_device rd;
    std::uniform_int_distribution<size_t> dist(0, chars.size() - 1);
    std::string result;
    result.reserve(len);
    for (size_t i = 0; i < len; ++i)
        result += chars[dist(rd)];
    return result;
}

toml::date_time toToml(const EdlDateTime &dt)
{
    // Extract the local_time and utc offset from the zoned_time
    const auto sysTime = dt.get_sys_time();
    const auto localTime = dt.get_local_time();

    // Compute UTC offset in minutes
    const auto offset = std::chrono::duration_cast<std::chrono::minutes>(
        localTime.time_since_epoch() - sysTime.time_since_epoch());
    const auto offsetMinutes = static_cast<int16_t>(offset.count());

    // Decompose local time into calendar fields
    const auto localDays = std::chrono::floor<std::chrono::days>(localTime);
    const auto tod = localTime - localDays;

    const std::chrono::year_month_day ymd{localDays};
    const std::chrono::hh_mm_ss<std::chrono::system_clock::duration> hms{tod};

    toml::date d;
    d.year = static_cast<int>(ymd.year());
    d.month = static_cast<uint8_t>(static_cast<unsigned>(ymd.month()));
    d.day = static_cast<uint8_t>(static_cast<unsigned>(ymd.day()));

    toml::time t;
    t.hour = static_cast<uint8_t>(hms.hours().count());
    t.minute = static_cast<uint8_t>(hms.minutes().count());
    t.second = static_cast<uint8_t>(hms.seconds().count());
    t.nanosecond = static_cast<uint32_t>(
        std::chrono::duration_cast<std::chrono::nanoseconds>(hms.subseconds()).count());

    toml::time_offset off;
    off.minutes = offsetMinutes;

    return toml::date_time{d, t, off};
}

static toml::array metaArrayToToml(const MetaArray &arr)
{
    toml::array result;
    for (const auto &elem : arr) {
        std::visit(
            [&result](const auto &v) {
                using T = std::decay_t<decltype(v)>;
                if constexpr (std::is_same_v<T, std::nullptr_t>) {
                    result.push_back(toml::value<std::string>{"null"});
                } else if constexpr (std::is_same_v<T, bool>) {
                    result.push_back(v);
                } else if constexpr (std::is_same_v<T, int64_t>) {
                    result.push_back(v);
                } else if constexpr (std::is_same_v<T, double>) {
                    result.push_back(v);
                } else if constexpr (std::is_same_v<T, std::string>) {
                    result.push_back(v);
                } else if constexpr (std::is_same_v<T, MetaSize>) {
                    toml::table sz;
                    sz.insert("width", v.width);
                    sz.insert("height", v.height);
                    result.push_back(std::move(sz));
                } else if constexpr (std::is_same_v<T, MetaArray>) {
                    result.push_back(metaArrayToToml(v));
                } else if constexpr (std::is_same_v<T, MetaStringMap>) {
                    // Nested map in array - recurse
                    toml::table inner;
                    for (const auto &[k2, v2] : v) {
                        std::visit(
                            [&inner, &k2](const auto &iv) {
                                using IT = std::decay_t<decltype(iv)>;
                                if constexpr (std::is_same_v<IT, std::nullptr_t>) {
                                    inner.insert(k2, std::string{"null"});
                                } else if constexpr (std::is_same_v<IT, bool>) {
                                    inner.insert(k2, iv);
                                } else if constexpr (std::is_same_v<IT, int64_t>) {
                                    inner.insert(k2, iv);
                                } else if constexpr (std::is_same_v<IT, double>) {
                                    inner.insert(k2, iv);
                                } else if constexpr (std::is_same_v<IT, std::string>) {
                                    inner.insert(k2, iv);
                                } else if constexpr (std::is_same_v<IT, MetaSize>) {
                                    toml::table sz;
                                    sz.insert("width", iv.width);
                                    sz.insert("height", iv.height);
                                    inner.insert(k2, std::move(sz));
                                } else if constexpr (std::is_same_v<IT, MetaArray>) {
                                    inner.insert(k2, metaArrayToToml(iv));
                                } else if constexpr (std::is_same_v<IT, MetaStringMap>) {
                                    // Deep nesting - handled recursively via toTomlTable
                                    inner.insert(k2, toTomlTable(iv));
                                }
                            },
                            static_cast<const MetaValue::Base &>(v2));
                    }
                    result.push_back(std::move(inner));
                }
            },
            static_cast<const MetaValue::Base &>(elem));
    }
    return result;
}

toml::table toTomlTable(const std::map<std::string, MetaValue> &attrs)
{
    toml::table tab;
    for (const auto &[key, val] : attrs) {
        std::visit(
            [&tab, &key](const auto &v) {
                using T = std::decay_t<decltype(v)>;
                if constexpr (std::is_same_v<T, std::nullptr_t>) {
                    // TOML has no null; skip or emit empty string
                } else if constexpr (std::is_same_v<T, bool>) {
                    tab.insert(key, v);
                } else if constexpr (std::is_same_v<T, int64_t>) {
                    tab.insert(key, v);
                } else if constexpr (std::is_same_v<T, double>) {
                    tab.insert(key, v);
                } else if constexpr (std::is_same_v<T, std::string>) {
                    if (!v.empty())
                        tab.insert(key, v);
                } else if constexpr (std::is_same_v<T, MetaSize>) {
                    toml::table sz;
                    sz.insert("width", v.width);
                    sz.insert("height", v.height);
                    tab.insert(key, std::move(sz));
                } else if constexpr (std::is_same_v<T, MetaArray>) {
                    if (!v.empty())
                        tab.insert(key, metaArrayToToml(v));
                } else if constexpr (std::is_same_v<T, MetaStringMap>) {
                    if (!v.empty())
                        tab.insert(key, toTomlTable(v));
                }
            },
            static_cast<const MetaValue::Base &>(val));
    }
    return tab;
}

void naturalNumListSort(std::vector<std::string> &list)
{
    std::sort(list.begin(), list.end(), [](const std::string &a, const std::string &b) {
        return strverscmp(a.c_str(), b.c_str()) < 0;
    });
}

std::string guessContentType(const fs::path &filePath, bool onlyCertain)
{
    gboolean resultUncertain = FALSE;
    g_autofree gchar *guess = g_content_type_guess(filePath.c_str(), nullptr, 0, &resultUncertain);
    if (resultUncertain && onlyCertain)
        return {};

    return (guess == nullptr ? std::string() : std::string(guess));
}

[[nodiscard]] static constexpr char asciiToLower(char c) noexcept
{
    return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + ('a' - 'A')) : c;
}

[[nodiscard]] static constexpr bool isAsciiWhitespace(char c) noexcept
{
    return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
}

static void replaceAll(std::string &s, std::string_view from, std::string_view to)
{
    std::size_t pos = 0;
    while ((pos = s.find(from, pos)) != std::string::npos) {
        s.replace(pos, from.size(), to);
        pos += to.size();
    }
}

[[nodiscard]] static std::size_t utf8BoundedLength(std::string_view s, std::size_t limit) noexcept
{
    if (limit >= s.size())
        return s.size();
    // Back up over UTF-8 continuation bytes (10xxxxxx) so we cut on a codepoint boundary.
    std::size_t n = limit;
    while (n > 0 && (static_cast<unsigned char>(s[n]) & 0xC0) == 0x80)
        --n;
    return n;
}

[[nodiscard]] std::string makeCompactName(std::string_view input, const CompactNameOptions &opts)
{
    std::string result;
    result.reserve(input.size());

    bool pendingSeparator = false;
    for (const char c : input) {
        // Optionally drop anything outside the 7-bit ASCII range
        if (opts.asciiOnly && static_cast<unsigned char>(c) > 0x7F)
            continue;

        // Drop characters that are structural in URLs (query/fragment/parameter markers),
        // as well as other characters we never want in filenames (globs, quotes, pipes, ...).
        static constexpr std::string_view droppedChars = "#?&*`\"'<>|";
        if (droppedChars.contains(c))
            continue;

        if (isAsciiWhitespace(c)) {
            // Collapse whitespace runs; emission is deferred so we never lead with a
            // separator and a trailing run is dropped for free.
            pendingSeparator = true;
            continue;
        }

        if (pendingSeparator) {
            pendingSeparator = false;
            if (opts.wordSeparator != '\0' && !result.empty())
                result.push_back(opts.wordSeparator);
        }

        // Map path/drive separators to underscores, and also replace dots
        // (to make file-extension handling more robust for naive
        // post-processing tools). Keep all other characters as-is.
        if (c == '/' || c == '\\' || c == ':')
            result.push_back('_');
        else if (c == '.')
            result.push_back('-');
        else
            result.push_back(c);
    }

    // Clean up separator/underscore artifacts, mirroring the historical replace chain.
    replaceAll(result, "_-", "-");
    replaceAll(result, "-_", "-");

    if (opts.lowercase) {
        for (char &c : result)
            c = asciiToLower(c);
    }

    // Truncate, preferring the last word boundary that still fits. maxLength is a byte
    // count; a hard cut is backed up to a UTF-8 codepoint boundary to stay valid UTF-8.
    if (result.size() > opts.maxLength) {
        const auto cut = result.find_last_of("-_", opts.maxLength);
        result.resize(cut == std::string::npos ? utf8BoundedLength(result, opts.maxLength) : cut);
        while (result.ends_with('-') || result.ends_with('_'))
            result.pop_back();
    }

    if (result.empty()) {
        if (opts.fallback.empty())
            return {};
        // Normalize the fallback through the same rules (without its own fallback, to
        // avoid recursion); if it still collapses to nothing, hard-truncate it.
        CompactNameOptions fallbackOpts = opts;
        fallbackOpts.fallback = {};
        auto fallbackResult = makeCompactName(opts.fallback, fallbackOpts);
        if (fallbackResult.empty())
            fallbackResult = std::string{opts.fallback.substr(0, utf8BoundedLength(opts.fallback, opts.maxLength))};
        return fallbackResult;
    }

    return result;
}

} // namespace Syntalos::edl

Updated on 2026-06-22 at 03:54:47 +0000