code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once

#include <boost/asio.hpp>
#include "model/device.h"
#include "model/device_id.h"
#include "utils/platform.h"
#include <filesystem>

#include <fmt/format.h>

template <> struct fmt::formatter<std::filesystem::path> {
    using Path = std::filesystem::path;

    constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { return ctx.end(); }

    template <typename FormatContext> auto format(const Path &path, FormatContext &ctx) const -> decltype(ctx.out()) {
        return fmt::format_to(ctx.out(), "{}", path.string());
    }
};

template <> struct fmt::formatter<boost::asio::ip::address> {
    using Address = boost::asio::ip::address;

    constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { return ctx.end(); }

    template <typename FormatContext>
    auto format(const Address &addr, FormatContext &ctx) const -> decltype(ctx.out()) {
        return fmt::format_to(ctx.out(), "{}", addr.to_string());
    }
};

template <typename T> struct fmt::formatter<boost::asio::ip::basic_endpoint<T>> {

    using EndPoint = boost::asio::ip::basic_endpoint<T>;

    constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { return ctx.end(); }

    template <typename FormatContext> auto format(const EndPoint &p, FormatContext &ctx) const -> decltype(ctx.out()) {
        return fmt::format_to(ctx.out(), "{}:{}", p.address(), p.port());
    }
};

template <> struct fmt::formatter<syncspirit::model::device_id_t> {
    using device_id_t = syncspirit::model::device_id_t;

    constexpr auto parse(format_parse_context &ctx) -> format_parse_context::iterator { return ctx.begin(); }

    auto format(const device_id_t &device_id, format_context &ctx) const -> format_context::iterator {
        return fmt::format_to(ctx.out(), "{}", device_id.get_short());
    }
};

template <> struct fmt::formatter<syncspirit::utils::uri_t> {
    using object_t = syncspirit::utils::uri_t;

    constexpr auto parse(format_parse_context &ctx) -> format_parse_context::iterator { return ctx.begin(); }

    auto format(const object_t &url, format_context &ctx) const -> format_context::iterator {
        return fmt::format_to(ctx.out(), "{}", std::string_view(url.buffer()));
    }
};

template <> struct fmt::formatter<syncspirit::utils::uri_ptr_t> {
    using object_t = syncspirit::utils::uri_ptr_t;

    constexpr auto parse(format_parse_context &ctx) -> format_parse_context::iterator { return ctx.begin(); }

    auto format(const object_t &url, format_context &ctx) const -> format_context::iterator {
        return fmt::format_to(ctx.out(), "{}", *url);
    }
};

template <> struct fmt::formatter<syncspirit::model::device_t> {
    using device_t = syncspirit::model::device_t;

    constexpr auto parse(format_parse_context &ctx) -> format_parse_context::iterator { return ctx.begin(); }

    auto format(const device_t &device, format_context &ctx) const -> format_context::iterator {
        return fmt::format_to(ctx.out(), "{}", device.device_id());
    }
};

template <> struct fmt::formatter<syncspirit::model::device_ptr_t> {
    using device_t = syncspirit::model::device_ptr_t;

    constexpr auto parse(format_parse_context &ctx) -> format_parse_context::iterator { return ctx.begin(); }

    auto format(const device_t &device, format_context &ctx) const -> format_context::iterator {
        return fmt::format_to(ctx.out(), "{}", *device);
    }
};


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#pragma once

#include <boost/nowide/fstream.hpp>

namespace syncspirit::utils {

using fstream_t = boost::nowide::fstream;
using ifstream_t = boost::nowide::ifstream;
using ofstream_t = boost::nowide::ofstream;

} // namespace syncspirit::utils

repo_name: basiliscos/syncspirit

license: mit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "location.h"
#include "error_code.h"
#include <cstdlib>
#include <system_error>
#include <boost/system/error_code.hpp>
#include <boost/nowide/convert.hpp>

#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#endif

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
#include <windows.h>
#include <shlobj.h>
#endif

namespace syncspirit::utils {

namespace sys = boost::system;

SYNCSPIRIT_API std::string expand_home(const std::string &path, const home_option_t &home) noexcept {
    if (home.has_value() && path.size() >= 2 && path[0] == '~' && path[1] == '/') {
        auto path_view = std::string_view(path).substr(2);
        return (home.assume_value() / path_view).generic_string();
    }
    return path;
}

outcome::result<bfs::path> get_home_dir() noexcept {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
    wchar_t appdata[MAX_PATH] = {0};
    if (SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, appdata) != S_OK) {
        return error_code_t::cant_determine_config_dir;
    }
    return bfs::path(appdata);
#elif defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
    auto *pw = getpwuid(getuid());
    if (!pw) {
        return sys::error_code{errno, sys::generic_category()};
    }
#if defined(__unix__)
    if (auto xdg_home = std::getenv("XDG_CONFIG_HOME")) {
        return bfs::path(xdg_home);
    } else {
        return bfs::path(pw->pw_dir);
    }
#else
    return bfs::path(pw->pw_dir);
#endif

#endif
}

outcome::result<bfs::path> get_default_config_dir() noexcept {
    auto home_opt = get_home_dir();
    if (home_opt.has_error()) {
        return home_opt.assume_error();
    }
    auto home = home_opt.assume_value();

#if defined(__unix__)
    home /= ".config";
#endif
    home /= "syncspirit";
    return home;
}

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once

#include <string>
#include <boost/outcome.hpp>
#include <filesystem>
#include "syncspirit-export.h"

namespace syncspirit {
namespace utils {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

using home_option_t = outcome::result<bfs::path>;

SYNCSPIRIT_API outcome::result<bfs::path> get_home_dir() noexcept;

SYNCSPIRIT_API outcome::result<bfs::path> get_default_config_dir() noexcept;

SYNCSPIRIT_API std::string expand_home(const std::string &path, const home_option_t &home) noexcept;

} // namespace utils
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "log-setup.h"

#include "error_code.h"
#include "io.h"
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <memory>
#include <unordered_map>
#include <string_view>
#include <vector>
#include <filesystem>

namespace syncspirit::utils {

namespace bfs = std::filesystem;
namespace sys = boost::system;

static const char *log_pattern = "[%Y-%m-%d %H:%M:%S.%e] [%^%L/%t%$] {%n} %v";

using sink_option_t = outcome::result<spdlog::sink_ptr>;

static sink_option_t make_sink(std::string_view name) noexcept {
    if (name == "stdout") {
        return std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    } else if (name == "stderr") {
        return std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
    } else if (name.size() > 5 && name.substr(0, 5) == "file:") {
        auto path = std::string(name.substr(5));
        return std::make_shared<spdlog::sinks::basic_file_sink_mt>(path);
    }
    return utils::make_error_code(error_code_t::unknown_sink);
}

outcome::result<void> init_loggers(const config::log_configs_t &configs) noexcept {
    using sink_map_t = std::unordered_map<std::string, spdlog::sink_ptr>;
    using logger_map_t = std::unordered_map<std::string, std::shared_ptr<spdlog::logger>>;

    // init sinks
    auto prev = spdlog::default_logger();
    if (prev->sinks().size() != 1) {
        return utils::make_error_code(error_code_t::misconfigured_default_logger);
    }
    auto prev_sink = prev->sinks().front();
    auto dist_sink = dynamic_cast<spdlog::sinks::dist_sink_mt *>(prev_sink.get());
    if (!dist_sink) {
        return utils::make_error_code(error_code_t::misconfigured_default_logger);
    }

    spdlog::drop_all();
    spdlog::set_default_logger(prev);

    sink_map_t sink_map;
    for (auto &cfg : configs) {
        for (auto &sink : cfg.sinks) {
            auto sink_option = make_sink(sink);
            if (!sink_option) {
                return sink_option.error();
            }
            sink_map[sink] = sink_option.value();
        }
    }

    logger_map_t logger_map;
    // init default
    std::vector<spdlog::sink_ptr> default_sinks;
    auto root_level = spdlog::level::debug;
    for (auto &cfg : configs) {
        auto &name = cfg.name;
        if (name != "default") {
            continue;
        }
        for (auto &sink_name : cfg.sinks) {
            auto &sink = sink_map.at(sink_name);
            dist_sink->add_sink(sink);
        }
        prev->set_level(cfg.level);
        if (cfg.level == spdlog::level::trace) {
            prev->flush_on(spdlog::level::trace);
        }
        root_level = cfg.level;
        logger_map[name] = prev;
    }

    // if (dist_sink->sinks().size() == 1) {
    //     return utils::make_error_code(error_code_t::misconfigured_default_logger);
    // }

    // init others
    for (auto &cfg : configs) {
        auto &name = cfg.name;
        if (name == "default") {
            continue;
        }

        std::vector<spdlog::sink_ptr> sinks;
        for (auto &sink_name : cfg.sinks) {
            sinks.push_back(sink_map.at(sink_name));
        }
        if (sinks.empty()) {
            sinks = default_sinks;
        }

        auto log_name = (name == "default") ? "" : name;
        auto logger = std::make_shared<spdlog::logger>(log_name, sinks.begin(), sinks.end());
        auto level = std::max(cfg.level, root_level);
        logger->set_level(level);
        logger_map[name] = logger;
    }

    // register
    for (auto &it : logger_map) {
        auto &name = it.first;
        auto &logger = it.second;
        if (name != "default") {
            spdlog::register_logger(logger);
        }
    }
    spdlog::set_pattern(log_pattern);
    return outcome::success();
}

static const char *bootstrap_sink = "syncspirit-bootstrap.log";

bootstrap_guard_t::bootstrap_guard_t(dist_sink_t dist_sink_, spdlog::sinks::sink *sink_)
    : dist_sink{dist_sink_}, sink{sink_} {}

bootstrap_guard_t::~bootstrap_guard_t() {
    auto logger = spdlog::default_logger_raw();
    auto sinks = logger->sinks();
    if (sinks.size() == 1) {
        auto dist_sink = dynamic_cast<spdlog::sinks::dist_sink_mt *>(sinks.at(0).get());
        if (dist_sink) {
            for (auto &s : dist_sink->sinks()) {
                if (s.get() == sink) {
                    dist_sink->flush();
                    sink->flush();
                    dist_sink->remove_sink(s);
                }
            }
        }
    }
}

auto bootstrap_guard_t::get_dist_sink() -> dist_sink_t { return dist_sink; }

dist_sink_t create_root_logger() noexcept {
    auto dist_sink = std::make_shared<spdlog::sinks::dist_sink_mt>();
    auto logger = std::make_shared<spdlog::logger>("", dist_sink);
    logger->set_level(spdlog::level::trace);
    spdlog::drop_all();
    spdlog::set_default_logger(logger);
    spdlog::set_pattern(log_pattern);
    spdlog::set_level(spdlog::level::trace);
    return dist_sink;
}

auto bootstrap(dist_sink_t &dist_sink, const bfs::path &dir) noexcept -> bootstrap_guard_ptr_t {
    using F = fstream_t;
    auto file_path = dir / bootstrap_sink;
    auto file = fstream_t(file_path, F::trunc | F::out | F::binary);
    auto file_sink = spdlog::sink_ptr();
    if (file) {
        file.close();
#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES)
        file_sink.reset(new spdlog::sinks::basic_file_sink_mt(file_path.wstring(), true));
#else
        file_sink.reset(new spdlog::sinks::basic_file_sink_mt(file_path.string(), true));
#endif
        dist_sink->add_sink(file_sink);
        spdlog::trace("file sink has been added initialized");
    } else {
        spdlog::trace("file sink '{}' has NOT been added", file_path.string());
    }
    return std::make_unique<bootstrap_guard_t>(dist_sink, file_sink.get());
}

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "log.h"
#include "config/log.h"
#include <boost/outcome.hpp>
#include <spdlog/sinks/dist_sink.h>
#include <filesystem>
#include "syncspirit-export.h"

namespace syncspirit::utils {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

using dist_sink_t = std::shared_ptr<spdlog::sinks::dist_sink_mt>;
using sink_t = spdlog::sink_ptr;

struct SYNCSPIRIT_API bootstrap_guard_t {
    bootstrap_guard_t(dist_sink_t dist_sink, spdlog::sinks::sink *);
    ~bootstrap_guard_t();
    dist_sink_t get_dist_sink();

  private:
    dist_sink_t dist_sink;
    spdlog::sinks::sink *sink;
};
using bootstrap_guard_ptr_t = std::unique_ptr<bootstrap_guard_t>;

SYNCSPIRIT_API outcome::result<void> init_loggers(const config::log_configs_t &configs) noexcept;

SYNCSPIRIT_API dist_sink_t create_root_logger() noexcept;
SYNCSPIRIT_API bootstrap_guard_ptr_t bootstrap(dist_sink_t &, const bfs::path &dir) noexcept;

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "log.h"

namespace syncspirit::utils {

auto get_log_level(const std::string &log_level) noexcept -> level_opt_t {
    using namespace spdlog::level;
    level_enum value = debug;
    if (log_level == "trace")
        return trace;
    if (log_level == "debug")
        return debug;
    if (log_level == "info")
        return info;
    if (log_level == "warn")
        return warn;
    if (log_level == "error")
        return err;
    if (log_level == "crit")
        return critical;
    if (log_level == "off")
        return off;
    return {};
}

std::string_view get_level_string(spdlog::level::level_enum level) noexcept {
    using L = spdlog::level::level_enum;
    switch (level) {
    case L::trace:
        return "trace";
    case L::debug:
        return "debug";
    case L::info:
        return "info";
    case L::warn:
        return "info";
    case L::err:
        return "error";
    case L::critical:
        return "critical";
    default:
        return "off";
    }
}

logger_t get_logger(std::string_view initial_name) noexcept {
    std::string name(initial_name);
    logger_t result = spdlog::get(name);
    if (result) {
        return result;
    }

    logger_t parent;
    while (!parent) {
        auto p = name.find_last_of(".");
        if (p != name.npos) {
            name = name.substr(0, p);
            parent = spdlog::get(name);
        } else {
            parent = spdlog::default_logger();
        }
    }

    auto &sinks = parent->sinks();
    auto log_name = std::string(initial_name);
    result = std::make_shared<spdlog::logger>(log_name, sinks.begin(), sinks.end());
    result->set_level(parent->level());
    spdlog::register_logger(result);
    return result;
}

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <memory>
#include <string_view>
#include <spdlog/spdlog.h>
#include <optional>
#include "syncspirit-export.h"

namespace syncspirit::utils {

using logger_t = std::shared_ptr<spdlog::logger>;
using level_opt_t = std::optional<spdlog::level::level_enum>;

SYNCSPIRIT_API logger_t get_logger(std::string_view name) noexcept;
SYNCSPIRIT_API level_opt_t get_log_level(const std::string &log_level) noexcept;
SYNCSPIRIT_API std::string_view get_level_string(spdlog::level::level_enum) noexcept;

#define LOG_GENERIC(LOGGER, LEVEL, ...)                                                                                \
    if (LEVEL >= LOGGER->level())                                                                                \
    LOGGER->log(LEVEL, __VA_ARGS__)

#define LOG_TRACE(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::trace, __VA_ARGS__)
#define LOG_DEBUG(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::debug, __VA_ARGS__)
#define LOG_INFO(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::info, __VA_ARGS__)
#define LOG_WARN(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::warn, __VA_ARGS__)
#define LOG_ERROR(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::err, __VA_ARGS__)
#define LOG_CRITICAL(LOGGER, ...) LOG_GENERIC(LOGGER, spdlog::level::critical, __VA_ARGS__)

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "network_interface.h"

#if defined(__linux__) || defined(__APPLE__)
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <unistd.h>
#endif

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#include <winsock2.h>
#include <iphlpapi.h>
#endif

namespace syncspirit::utils {

#if defined(__linux__) || defined(__APPLE__)
static uri_container_t _local_interfaces(logger_t &log, std::uint16_t port) noexcept {
    uri_container_t r{};

    ifaddrs *ifaddr;
    if (getifaddrs(&ifaddr) == -1) {
        LOG_WARN(log, "getifaddrs failed: ", strerror(errno));
        return r;
    }
    using guard_t = std::unique_ptr<ifaddrs, decltype(&freeifaddrs)>;
    auto guard = guard_t(ifaddr, &freeifaddrs);

    char host[NI_MAXHOST];
    auto lo = inet_addr("127.0.0.1");
    for (auto ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
        if (ifa->ifa_addr == NULL) {
            continue;
        }
        if (ifa->ifa_addr->sa_family != AF_INET) {
            continue;
        }

        auto op = getnameinfo(ifa->ifa_addr, sizeof(sockaddr_in), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
        if (op != 0) {
            LOG_WARN(log, "getnameinfo() failed: {}", gai_strerror(op));
            continue;
        }
        if (((sockaddr_in *)ifa->ifa_addr)->sin_addr.s_addr == lo) {
            continue;
        }

        auto full = fmt::format("tcp://{}:{}/", host, port);
        auto uri = utils::parse(full);
        r.emplace_back(std::move(uri));
    }

    return r;
}
#endif

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
std::string GetLastErrorAsString() {
    // Get the error message ID, if any.
    DWORD errorMessageID = ::GetLastError();
    if (errorMessageID == 0) {
        return std::string(); // No error message has been recorded
    }

    LPSTR messageBuffer = nullptr;

    // Ask Win32 to give us the string version of that message ID.
    // The parameters we pass in, tell Win32 to create the buffer that holds the message for us (because we don't yet
    // know how long the message string will be).
    size_t size =
        FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                       NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);

    // Copy the error message into a std::string.
    std::string message(messageBuffer, size);

    // Free the Win32's string's buffer.
    LocalFree(messageBuffer);

    return message;
}

static uri_container_t _local_interfaces(logger_t &log, std::uint16_t port) noexcept {
    uri_container_t r{};

    char buff[16 * 1024];
    auto adapter_addresses = reinterpret_cast<IP_ADAPTER_ADDRESSES *>(buff);
    DWORD adapter_addresses_buffer_size = sizeof(buff);
    DWORD code = ::GetAdaptersAddresses(AF_UNSPEC,
                                        GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER |
                                            GAA_FLAG_SKIP_FRIENDLY_NAME,
                                        NULL, adapter_addresses, &adapter_addresses_buffer_size);

    if (ERROR_SUCCESS != code) {
        LOG_WARN(log, "GetAdaptersAddresses failed: ", GetLastErrorAsString());
        return r;
    }

    // Iterate through all of the adapters
    for (auto adapter = adapter_addresses; NULL != adapter; adapter = adapter->Next) {
        // Skip loopback adapters
        if (IF_TYPE_SOFTWARE_LOOPBACK == adapter->IfType) {
            continue;
        }

        // Parse all IPv4 and IPv6 addresses
        for (IP_ADAPTER_UNICAST_ADDRESS *address = adapter->FirstUnicastAddress; NULL != address;
             address = address->Next) {
            auto family = address->Address.lpSockaddr->sa_family;
            if (AF_INET == family) {
                // IPv4
                SOCKADDR_IN *ipv4 = reinterpret_cast<SOCKADDR_IN *>(address->Address.lpSockaddr);

                auto host = inet_ntoa(ipv4->sin_addr);
                auto full = fmt::format("tcp://{}:{}/", host, port);
                auto uri = utils::parse(full);
                r.emplace_back(std::move(uri));
            }
        }
    }
    return r;
}
#endif

uri_container_t local_interfaces(const tcp::endpoint &fallback, logger_t &log) noexcept {
    auto port = fallback.port();
    auto r = _local_interfaces(log, port);
    if (r.empty()) {
        auto uri_str = fmt::format("tcp://{0}:{1}/", fallback.address().to_string(), port);
        LOG_DEBUG(log, "falling back into usage {} interface", uri_str);
        auto uri = utils::parse(uri_str);
        r.emplace_back(std::move(uri));
    }
    return r;
}

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include <boost/asio.hpp>
#include "uri.h"
#include "log.h"
#include "syncspirit-export.h"

namespace syncspirit::utils {
using tcp = boost::asio::ip::tcp;

SYNCSPIRIT_API uri_container_t local_interfaces(const tcp::endpoint &fallback, logger_t &log) noexcept;
} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "platform.h"
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#include <stdexcept>
#endif
using namespace syncspirit::utils;

void platform_t::startup() {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
    auto wVersionRequested = MAKEWORD(2, 2);
    WSADATA wsaData;

    auto err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
        std::string msg = "WSAStartup failed with error: ";
        msg += std::to_string(err);
        throw std::runtime_error(msg);
    }
#endif
}

void platform_t::shutdown() noexcept {}

bool platform_t::symlinks_supported() noexcept {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
    return false;
#else
    return true;
#endif
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "syncspirit-export.h"

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#endif

namespace syncspirit::utils {

struct SYNCSPIRIT_API platform_t {
    static void startup();
    static void shutdown() noexcept;
    static bool symlinks_supported() noexcept;
};

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 Ivan Baidakou

#pragma once

#include <cstring>
#include <string>
#include <string_view>

namespace syncspirit::utils {

struct string_comparator_t {
    using is_transparent = void;

    template <typename T1, typename T2> bool operator()(const T1 &k1, const T2 &k2) const { return k1 < k2; }
};

struct string_hash_t {
    using is_transparent = void;

    std::size_t operator()(const char *value) const { return (*this)(std::string_view(value)); }
    std::size_t operator()(std::string_view value) const { return std::hash<std::string_view>()(value); }
    std::size_t operator()(const std::string &value) const { return std::hash<std::string>()(value); }
};

struct string_eq_t {
    using is_transparent = void;

    template <typename T, typename U> auto operator()(const T &lhs, U &rhs) const { return lhs == rhs; }
};

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 Ivan Baidakou

#include "time.h"

namespace syncspirit::utils {

std::int64_t as_seconds(const pt::ptime &t) noexcept {
    pt::ptime epoch(boost::gregorian::date(1970, 1, 1));
    auto time_diff = t - epoch;
    auto value = time_diff.ticks() / time_diff.ticks_per_second();
    return value;
}

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 Ivan Baidakou

#pragma once

#include <boost/date_time/posix_time/posix_time.hpp>
#include <cstdint>
#include "syncspirit-export.h"

namespace syncspirit::utils {

namespace pt = boost::posix_time;

SYNCSPIRIT_API std::int64_t as_seconds(const pt::ptime &t) noexcept;

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "tls.h"
#include "error_code.h"
#include "io.h"
#include <random>
#include <boost/system/error_code.hpp>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <openssl/err.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/bio.h>

#ifdef OSSL_DEPRECATEDIN_3_0
#include <openssl/encoder.h>
#endif

namespace sys = boost::system;

namespace syncspirit::utils {

bool cert_data_t::operator==(const cert_data_t &other) const noexcept {
    auto view_mine = bytes_view_t(*this);
    auto view_other = bytes_view_t(other);
    return view_mine == view_other;
}

x509_t::~x509_t() {
    if (cert) {
        X509_free(cert);
    }
}

template <typename T, typename G> guard_t<T> make_guard(T *ptr, G &&fn) {
    return guard_t<T>{ptr, [fn = std::move(fn)](T *it) { fn(it); }};
}

static bool add_extension(X509V3_CTX &ctx, X509 *cert, int NID_EXT, const char *value) {
    auto ex = X509V3_EXT_conf_nid(nullptr, &ctx, NID_EXT, value);
    if (!ex) {
        return false;
    }
    auto ex_guard = make_guard(ex, [](auto *ptr) { X509_EXTENSION_free(ptr); });
    if (1 != X509_add_ext(cert, ex, -1)) {
        return false;
    }
    return true;
}

template <typename Key, typename Fn> static outcome::result<utils::bytes_t> as_der_impl(Key *cert, Fn &&fn) noexcept {
    BIO *bio = BIO_new(BIO_s_mem());
    auto bio_guard = make_guard(bio, [](auto *ptr) { BIO_free(ptr); });
    if (fn(bio, cert) < 0) {
        return error_code_t::tls_cert_save_failure;
    }
    char *cert_buff;
    auto cert_sz = BIO_get_mem_data(bio, &cert_buff);
    if (cert_sz < 0) {
        return error_code_t::tls_cert_save_failure;
    }
    auto cert_container = utils::bytes_t(static_cast<std::size_t>(cert_sz));
    std::memcpy(cert_container.data(), cert_buff, cert_container.size());
    return cert_container;
}

static outcome::result<utils::bytes_t> as_der(X509 *cert) noexcept {
    return as_der_impl(cert, [](BIO *bio, auto *cert) { return i2d_X509_bio(bio, cert); });
}

static outcome::result<utils::bytes_t> as_der(EVP_PKEY *key) noexcept {
    auto pkc8 = EVP_PKEY2PKCS8(key);
    auto pkc8_guard = make_guard(pkc8, [](auto *ptr) { PKCS8_PRIV_KEY_INFO_free(ptr); });
    return as_der_impl(pkc8, [](BIO *bio, auto *key) { return i2d_PKCS8_PRIV_KEY_INFO_bio(bio, key); });
}

outcome::result<utils::bytes_t> as_serialized_der(X509 *cert) noexcept { return as_der(cert); }

outcome::result<key_pair_t> generate_pair(const char *issuer_name) noexcept {
    auto ev_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr);
    if (ev_ctx == nullptr) {
        return error_code_t::tls_context_init_failure;
    }
    auto ev_ctx_guard = make_guard(ev_ctx, [](auto *ptr) { EVP_PKEY_CTX_free(ptr); });

    if (1 != EVP_PKEY_paramgen_init(ev_ctx)) {
        return error_code_t::tls_param_init_failure;
    }

    if (1 != EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ev_ctx, NID_secp384r1)) {
        return error_code_t::tls_ec_curve_failure;
    }

    EVP_PKEY *params = nullptr;
    if (!EVP_PKEY_paramgen(ev_ctx, &params)) {
        return error_code_t::tls_param_gen_failure;
    }
    auto params_guard = make_guard(params, [](auto *ptr) { EVP_PKEY_free(ptr); });

    EVP_PKEY_CTX *key_ctx = EVP_PKEY_CTX_new(params, nullptr);
    auto key_ctx_guard = make_guard(key_ctx, [](auto *ptr) { EVP_PKEY_CTX_free(ptr); });
    /* Generate the key */
    if (1 != EVP_PKEY_keygen_init(key_ctx)) {
        return error_code_t::tls_key_gen_init_failure;
    }

    EVP_PKEY *pkey = nullptr;
    if (1 != EVP_PKEY_keygen(key_ctx, &pkey)) {
        return error_code_t::tls_key_gen_failure;
    }
    auto pkey_guard = make_guard(pkey, [](auto *ptr) { EVP_PKEY_free(ptr); });

    EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_secp384r1);
    EC_GROUP_set_asn1_flag(group, OPENSSL_EC_NAMED_CURVE);
    if (group == nullptr) {
        return error_code_t::tls_ec_group_failure;
    }
    auto group_guard = make_guard(group, [](auto *ptr) { EC_GROUP_free(ptr); });

    X509 *cert = X509_new();
    // cert->ex_kusage = X509v3_KU_KEY_ENCIPHERMENT | X509v3_KU_DIGITAL_SIGNATURE;
    auto cert_guard = make_guard(cert, [](auto *ptr) { X509_free(ptr); });

    std::random_device rd;
    std::mt19937 generator(rd());
    constexpr const auto max_sn = std::numeric_limits<std::uint64_t>::max() >> 1;
    std::uniform_int_distribution<std::uint64_t> distr(1, max_sn);
    int version = 2;
    long serial = static_cast<long>(distr(generator));
    long start_epoch = 0; /* now */
    long end_epoch = 2524568399;

    if (1 != X509_set_version(cert, version)) {
        return error_code_t::tls_cert_set_failure;
    }

    if (-1 == ASN1_INTEGER_set(X509_get_serialNumber(cert), serial)) {
        return error_code_t::tls_cert_set_failure;
    }

    X509_gmtime_adj(X509_get_notBefore(cert), start_epoch);
    X509_gmtime_adj(X509_get_notAfter(cert), end_epoch);

    if (1 != X509_set_pubkey(cert, pkey)) {
        return error_code_t::tls_cert_set_failure;
    }

    auto name = X509_get_subject_name(cert);
    if (1 != X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<const unsigned char *>(issuer_name),
                                        -1, -1, 0)) {
        return error_code_t::tls_cert_set_failure;
    }

    if (1 != X509_set_issuer_name(cert, name)) {
        return error_code_t::tls_cert_set_failure;
    }

    X509V3_CTX ctx;
    ctx.db = nullptr; /* no DB is used */
    X509V3_set_ctx(&ctx, cert, cert, nullptr, nullptr, 0);

    if (!add_extension(ctx, cert, NID_basic_constraints, "critical, CA:false")) {
        return error_code_t::tls_cert_ext_failure;
    }

    if (!add_extension(ctx, cert, NID_key_usage, "critical, Digital Signature, Key Encipherment")) {
        return error_code_t::tls_cert_ext_failure;
    }

    if (!add_extension(ctx, cert, NID_ext_key_usage, "TLS Web Server Authentication, TLS Web Client Authentication")) {
        return error_code_t::tls_cert_ext_failure;
    }

    auto bytes = X509_sign(cert, pkey, EVP_sha256());
    if (!bytes) {
        return error_code_t::tls_cert_sign_failure;
    }

    auto cert_container = as_der(cert);
    if (!cert_container) {
        return cert_container.error();
    }

    auto key_container = as_der(pkey);
    if (!key_container) {
        return key_container.error();
    }

    return key_pair_t{std::move(cert_guard), std::move(pkey_guard), cert_data_t{std::move(cert_container.value())},
                      cert_data_t{std::move(key_container.value())}};
}

static bool write_mem_to(const char *path, BIO *mem) {
    auto file = ofstream_t(path, ifstream_t::out | ifstream_t::binary);
    char *ptr;
    auto size = BIO_get_mem_data(mem, &ptr);
    if (size < 0) {
        return false;
    }
    if (size == 0) {
        return true;
    }
    file.write(ptr, size);
    return (bool)file;
}

outcome::result<void> key_pair_t::save(const char *cert_path, const char *priv_key_path) const noexcept {
    do {
        BIO *bio = BIO_new(BIO_s_mem());
        auto bio_guard = make_guard(bio, [](auto ptr) { BIO_free(ptr); });
        if (1 != PEM_write_bio_X509(bio, cert.get())) {
            return error_code_t::tls_cert_save_failure;
        }
        if (!write_mem_to(cert_path, bio)) {
            return error_code_t::tls_cert_save_failure;
        }
    } while (0);

    do {
        BIO *bio = BIO_new(BIO_s_mem());
        auto bio_guard = make_guard(bio, [](auto ptr) { BIO_free(ptr); });
        if (1 != PEM_write_bio_PrivateKey(bio, private_key.get(), nullptr, nullptr, 0, nullptr, nullptr)) {
            return error_code_t::tls_key_save_failure;
        }
        if (!write_mem_to(priv_key_path, bio)) {
            return error_code_t::tls_key_save_failure;
        }
    } while (0);

    return outcome::success();
}

static outcome::result<guard_t<BIO>> read_to_mem_bio(const char *cert_path) {
    auto file = ifstream_t(cert_path, ifstream_t::in | ifstream_t::binary);
    if (!file) {
        return sys::error_code{errno, sys::system_category()};
    }

    auto begin = file.tellg();
    if (!file.seekg(0, ifstream_t::end)) {
        return sys::error_code{errno, sys::generic_category()};
    }
    auto end = file.tellg();
    if (end < 0) {
        return sys::error_code{errno, sys::generic_category()};
    }
    auto cert_sz = end - begin;
    if (!file.seekg(ifstream_t::beg)) {
        return sys::error_code{errno, sys::generic_category()};
    }
    auto data = std::vector<char>(cert_sz);
    auto ptr = data.data();
    file.read(ptr, cert_sz);

    auto cert_bio = BIO_new_mem_buf(ptr, static_cast<int>(cert_sz));
    return make_guard(cert_bio, [data = std::move(data)](auto *ptr) { BIO_free(ptr); });
}

outcome::result<key_pair_t> load_pair(const char *cert_path, const char *priv_key_path) {
    /* read certificate in memory, then load it va openssl */
    auto cert_mem_result = read_to_mem_bio(cert_path);
    if (!cert_mem_result) {
        return cert_mem_result.assume_error();
    }
    auto cert_bio = cert_mem_result.assume_value().get();
    X509 *cert = X509_new();
    auto cert_guard = make_guard(cert, [](auto *ptr) { X509_free(ptr); });
    if (!PEM_read_bio_X509(cert_bio, &cert, nullptr, nullptr)) {
        return error_code_t::tls_cert_load_failure;
    }

    /* read private key */
    auto pk_mem_result = read_to_mem_bio(priv_key_path);
    if (!pk_mem_result) {
        return pk_mem_result.assume_error();
    }
    auto pk_bio = pk_mem_result.assume_value().get();
    auto pkey = PEM_read_bio_PrivateKey(pk_bio, nullptr, nullptr, nullptr);
    if (!pkey) {
        return error_code_t::tls_key_load_failure;
    }
    auto pkey_guard = make_guard(pkey, [](auto *ptr) { EVP_PKEY_free(ptr); });

    auto cert_container = as_der(cert);
    if (!cert_container) {
        return cert_container.error();
    }

    auto key_container = as_der(pkey);
    if (!key_container) {
        return key_container.error();
    }

    return key_pair_t{std::move(cert_guard), std::move(pkey_guard), cert_data_t{std::move(cert_container.value())},
                      cert_data_t{std::move(key_container.value())}};
}

outcome::result<bytes_t> sha256_digest(utils::bytes_view_t data) noexcept {
    unsigned char buff[SHA256_DIGEST_LENGTH];
    auto ctx = EVP_MD_CTX_create();
    auto ctx_guard = make_guard(ctx, [](auto *ptr) { EVP_MD_CTX_free(ptr); });

    auto digester = EVP_get_digestbyname("sha256");
    if (!digester) {
        return error_code_t::tls_sha256_init_failure;
    }
    if (1 != EVP_DigestInit_ex(ctx, digester, NULL)) {
        return error_code_t::tls_sha256_failure;
    }
    if (1 != EVP_DigestUpdate(ctx, data.data(), data.size())) {
        return error_code_t::tls_sha256_failure;
    }
    EVP_DigestFinal_ex(ctx, buff, NULL);
    return {buff, buff + SHA256_DIGEST_LENGTH};
}

outcome::result<std::string> get_common_name(X509 *cert) noexcept {
    auto name = X509_get_subject_name(cert);

    char buff[256] = {};
    int r = X509_NAME_get_text_by_NID(name, NID_commonName, buff, 256);
    if (r == -1) {
        return error_code_t::tls_cn_missing;
    }
    return std::string(buff, r);
}

void digest(const unsigned char *src, size_t length, unsigned char *storage) noexcept { SHA256(src, length, storage); }

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <memory>
#include <string>
#include <boost/outcome.hpp>
#include <openssl/x509v3.h>
#include <openssl/evp.h>
#include "syncspirit-export.h"
#include "bytes.h"

namespace syncspirit {
namespace utils {

namespace outcome = boost::outcome_v2;

template <typename T> using guard_t = std::unique_ptr<T, std::function<void(T *)>>;

/* in DER-format */
struct cert_data_view_t : bytes_view_t {
    using bytes_view_t::bytes_view_t;
};
struct SYNCSPIRIT_API cert_data_t : bytes_t {
    inline explicit cert_data_t(bytes_t bytes) noexcept : bytes_t(std::move(bytes)) {}
    using bytes_t::bytes_t;
    bool operator==(const cert_data_t &other) const noexcept;
};

struct SYNCSPIRIT_API key_pair_t {
    using X509_sp = guard_t<X509>;
    using EVP_PKEY_sp = guard_t<EVP_PKEY>;

    X509_sp cert;
    EVP_PKEY_sp private_key;
    cert_data_t cert_data;
    cert_data_t key_data;

    outcome::result<void> save(const char *cert, const char *priv_key) const noexcept;
};

struct SYNCSPIRIT_API x509_t {
    x509_t() noexcept : cert{nullptr} {}
    x509_t(X509 *cert_) noexcept : cert{cert_} {}
    ~x509_t();

    inline operator X509 *() noexcept { return cert; }

    X509 *cert;
};

SYNCSPIRIT_API outcome::result<key_pair_t> generate_pair(const char *issuer_name) noexcept;

SYNCSPIRIT_API outcome::result<key_pair_t> load_pair(const char *cert, const char *priv_key);

SYNCSPIRIT_API outcome::result<bytes_t> sha256_digest(utils::bytes_view_t data) noexcept;

SYNCSPIRIT_API outcome::result<bytes_t> as_serialized_der(X509 *cert) noexcept;

SYNCSPIRIT_API outcome::result<std::string> get_common_name(X509 *cert) noexcept;

SYNCSPIRIT_API void digest(const unsigned char *src, size_t length, unsigned char *storage) noexcept;

} // namespace utils
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "uri.h"

namespace syncspirit::utils {

uri_t::uri_t(boost::urls::url_view view) : parent_t(view) {
    if (!has_port()) {
        if (scheme() == "http") {
            set_port_number(80);
        } else if (scheme() == "https") {
            set_port_number(443);
        }
    }
}

uri_ptr_t uri_t::clone() const { return new uri_t(*this); }

uri_ptr_t parse(std::string_view str) noexcept {
    auto result = boost::urls::parse_uri(str);
    if (result) {
        return new uri_t(result.value());
    }
    return {};
}

SYNCSPIRIT_API bool is_parsable(std::string_view string) noexcept { return boost::urls::parse_uri(string).has_value(); }

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "syncspirit-export.h"
#include <boost/utility/string_view.hpp>
#include <boost/url.hpp>
#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

namespace syncspirit::utils {

struct uri_t;
using uri_ptr_t = boost::intrusive_ptr<uri_t>;

struct SYNCSPIRIT_API uri_t : boost::intrusive_ref_counter<uri_t, boost::thread_unsafe_counter>, boost::urls::url {
    using parent_t = boost::urls::url;
    uri_t(boost::urls::url_view view);

    uri_ptr_t clone() const;
};

using uri_container_t = std::vector<uri_ptr_t>;

SYNCSPIRIT_API uri_ptr_t parse(std::string_view string) noexcept;
SYNCSPIRIT_API bool is_parsable(std::string_view string) noexcept;

} // namespace syncspirit::utils


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "test-utils.h"
#include "utils/uri.h"

using namespace syncspirit::utils;

TEST_CASE("parse IGD control url", "[support]") {
    auto uri = parse("http://192.168.100.1:49652/upnp/control/WANIPConn1");
    REQUIRE((bool)uri);
    CHECK(uri->port_number() == 49652);
    CHECK(uri->port() == "49652");
    CHECK(uri->host() == "192.168.100.1");
    CHECK(uri->scheme() == "http");
    CHECK(uri->encoded_path() == "/upnp/control/WANIPConn1");

    std::string relay = "relay://188.68.32.45:22067/"
                        "?id=O4LHPKG-O6BQ36W-MUOVKTI-MKAVHSC-Y7EC3U4-DHNLEDE-MZBJWQN-UIX6QAL\u0026pingInterval="
                        "1m0s\u0026networkTimeout=2m0s\u0026sessionLimitBps=0\u0026globalLimitBps=0\u0026statusAddr=:"
                        "22070\u0026providedBy=🐾 TilCreator (tils.pw)";
    REQUIRE((bool)parse(relay) == false);

    std::string relay2 = "relay://188.68.32.45:22067/"
                         "?id=O4LHPKG-O6BQ36W-MUOVKTI-MKAVHSC-Y7EC3U4-DHNLEDE-MZBJWQN-UIX6QAL&pingInterval=1m0s&"
                         "networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=22070";
    REQUIRE((bool)parse(relay2) == true);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2023 Ivan Baidakou

#include "test-utils.h"
#include "proto/upnp_support.h"
#include <filesystem>
#include <iostream>

namespace sys = boost::system;
namespace bfs = std::filesystem;

using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("parse IGD description", "[support]") {
    auto xml = read_file(locate_path("data/49652gatedesc.xml"));
    auto wan_service = parse_igd(xml.c_str(), xml.size());
    REQUIRE(wan_service);
    REQUIRE(wan_service.value().control_path == "/upnp/control/WANIPConn1");
    REQUIRE(wan_service.value().description_path == "/gateconnSCPD.xml");
}

TEST_CASE("parse external IP", "[support]") {
    auto xml = read_file(locate_path("data/external-ip.xml"));
    auto ip = parse_external_ip(xml.c_str(), xml.size());
    REQUIRE(ip);
    REQUIRE(ip.value() == "81.31.113.9");
}

TEST_CASE("parse successful port mapping", "[support]") {
    auto xml = read_file(locate_path("data/port-mapping-success.xml"));
    auto r = parse_mapping(xml.c_str(), xml.size());
    REQUIRE(r);
    REQUIRE(r.value() == true);
}

TEST_CASE("parse failed port mapping", "[support]") {
    auto xml = read_file(locate_path("data/soap-failure.xml"));
    auto r = parse_mapping(xml.c_str(), xml.size());
    REQUIRE(r);
    REQUIRE(r.value() == false);
}

TEST_CASE("parse failed port unmapping", "[support]") {
    auto xml = read_file(locate_path("data/port-unmapping-failure.xml"));
    auto r = parse_unmapping(xml.c_str(), xml.size());
    REQUIRE(r);
    REQUIRE(r.value() == false);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "utils/base32.h"
#include "utils/tls.h"
#include <openssl/pem.h>
#include <filesystem>
#include <cstdio>

using namespace syncspirit::utils;
using namespace syncspirit::test;

namespace bfs = std::filesystem;

TEST_CASE("generate cert/key pair, save & load", "[support][tls]") {
    auto pair = generate_pair("sample");
    REQUIRE((bool)pair);
    REQUIRE((bool)pair.value().cert);
    REQUIRE((bool)pair.value().private_key);
    REQUIRE(pair.value().cert_data.size() > 0);

    auto &value = pair.value();

    PEM_write_PrivateKey(stdout, value.private_key.get(), nullptr, nullptr, 0, nullptr, nullptr);
    PEM_write_X509(stdout, value.cert.get());
    X509_print_fp(stdout, value.cert.get());

    auto cert_file = unique_path();
    auto cert_file_path = cert_file.string();
    auto cert_file_guard = path_guard_t(cert_file);

    auto key_file = unique_path();
    auto key_file_path = key_file.string();
    auto key_file_guard = path_guard_t(key_file);
    auto save_result = value.save(cert_file_path.c_str(), key_file_path.c_str());
    REQUIRE((bool)save_result);
    printf("cert has been saved as %s\n", cert_file_path.c_str());

    auto load_result = load_pair(cert_file_path.c_str(), key_file_path.c_str());
    REQUIRE((bool)load_result);
    REQUIRE(load_result.value().cert_data.size() == pair.value().cert_data.size());

    bool bytes_equal = load_result.value().cert_data == pair.value().cert_data;
    REQUIRE(bytes_equal);

    auto cn = get_common_name(value.cert.get());
    REQUIRE(cn);
    REQUIRE(cn.value() == "sample");
}

TEST_CASE("sha256 for certificate", "[support][tls]") {
    auto cert = read_file(locate_path("data/cert.der"));
    auto cert_bytes = bytes_view_t((unsigned char *)cert.data(), cert.size());
    auto sha_result = sha256_digest(cert_bytes);
    REQUIRE((bool)sha_result);
    auto &sha = sha_result.value();
    REQUIRE(1 == 1);
    std::string expected = "b1b48b580b78b47c975a138b4aaa2988fc621795c95a2868e24d93b327e8858c";
    std::string got_str;

    for (std::size_t i = 0; i < sha.size(); i++) {
        got_str += fmt::format("{:02x}", (unsigned char)sha[i]);
    }
    REQUIRE(got_str == expected);

    auto enc = base32::encode(sha);
    REQUIRE(enc == "WG2IWWALPC2HZF22COFUVKRJRD6GEF4VZFNCQ2HCJWJ3GJ7IQWGA");
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "utils/base32.h"

using namespace syncspirit::test;
using namespace syncspirit::utils;

namespace fs = std::filesystem;
using b32 = syncspirit::utils::base32;

TEST_CASE("base32 encode/decode", "[support]") {

    SECTION("encode") {
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"f", 1)) == "MY");
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"fo", 2)) == "MZXQ");
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"foo", 3)) == "MZXW6");
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"foo-", 4)) == "MZXW6LI");
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"foo-b", 5)) == "MZXW6LLC");
        REQUIRE(b32::encode(bytes_view_t((const unsigned char *)"foo-bar", 7)) == "MZXW6LLCMFZA");
    }

    SECTION("decode") {
        REQUIRE(b32::decode("MY").value() == bytes_view_t((const unsigned char *)"f", 1));
        REQUIRE(b32::decode("MZXQ").value() == bytes_view_t((const unsigned char *)"fo", 2));
        REQUIRE(b32::decode("MZXW6").value() == bytes_view_t((const unsigned char *)"foo", 3));
        REQUIRE(b32::decode("MZXW6LI").value() == bytes_view_t((const unsigned char *)"foo-", 4));
        REQUIRE(b32::decode("MZXW6LLC").value() == bytes_view_t((const unsigned char *)"foo-b", 5));
        REQUIRE(b32::decode("MZXW6LLCMFZA").value() == bytes_view_t((const unsigned char *)"foo-bar", 7));
    }

    SECTION("encode-decode lorum") {
        std::string_view in = "lorem ipsum dolor sit amet";
        auto bytes_in = bytes_view_t((const unsigned char *)in.data(), in.size());
        auto text_out = b32::encode(bytes_in);
        REQUIRE(text_out == "NRXXEZLNEBUXA43VNUQGI33MN5ZCA43JOQQGC3LFOQ");
        REQUIRE(b32::decode(text_out).value() == bytes_in);
    }

    SECTION("binary string") {
        unsigned char in[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x00};
        auto bytes_in = bytes_view_t(in, sizeof(in));
        auto e = b32::encode(bytes_in);
        auto d = b32::decode(e);
        REQUIRE((bool)d);
        REQUIRE(d.value() == bytes_in);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "test-utils.h"
#include "proto/discovery_support.h"
#include "utils/error_code.h"
#include <filesystem>

namespace sys = boost::system;
namespace fs = std::filesystem;

using namespace syncspirit::proto;
using namespace syncspirit::utils;
using namespace syncspirit::test;

TEST_CASE("parse valid announce sample 1", "[support]") {
    std::string body =
        R""(
                {"seen":"2020-10-13T18:41:37.02287354Z",
                "addresses":["quic://192.168.100.15:22000",
                "quic://81.52.93.44:22000",
                "quic://81.52.93.44:22000",
                "relay://188.68.32.45:22067/?id=O4LHPKG-O6BQ36W-MUOVKTI-MKAVHSC-Y7EC3U4-DHNLEDE-MZBJWQN-UIX6QAL&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=22070",
                "tcp://192.168.100.15:22000",
                "tcp://81.52.93.44:22000",
                "tcp://81.52.93.44:22000",
                "tcp://81.52.93.44:55386",
                "tcp://81.52.93.44:55386"]}
            )"";

    http::response<http::string_body> res;
    res.result(200);
    res.body() = body;
    auto r = parse_contact(res);
    REQUIRE((bool)r);

    auto &o = r.value();
    CHECK(o.size() == 9);
    CHECK(o[0]->buffer() == "quic://192.168.100.15:22000");
}

TEST_CASE("parse valid announce sample 2", "[support]") {
    std::string body =
        R""(
{
  "seen": "2023-10-31T09:38:31Z",
  "addresses": [
	"relay://93.31.21.95:443/?id=QUJWY5Q-YUKKWZF-OWOJ66C-NWAPXYC-LWU4IPC-TO6UBKB-67R5JV5-BILNNA7"
  ]
}
			)"";

    http::response<http::string_body> res;
    res.result(200);
    res.body() = body;
    auto r = parse_contact(res);
    REQUIRE((bool)r);

    auto &o = r.value();
    CHECK(o.size() == 1);
    CHECK(o[0]->buffer() ==
          "relay://93.31.21.95:443/?id=QUJWY5Q-YUKKWZF-OWOJ66C-NWAPXYC-LWU4IPC-TO6UBKB-67R5JV5-BILNNA7");
}

TEST_CASE("malformed url", "[support]") {

    http::response<http::string_body> res;
    res.result(200);

    SECTION("malformed url") {
        res.body() = R""( {"seen":"2020-10-13T18:41:37.02287354Z", "addresses":["q uic://192.168.100.15:22000" ]})"";
        auto r = parse_contact(res);
        CHECK(r.error() == make_error_code(error_code_t::malformed_url));
    }

    SECTION("malformed json") {
        res.body() = R""( {"seen":"2020-10-13T18:41:37.02287354Z" "addresses":["qic://192.168.100.15:22000" ]})"";
        auto r = parse_contact(res);
        CHECK(r.error() == make_error_code(error_code_t::malformed_json));
    }

    SECTION("incorrect json") {
        res.body() = R""( {"seen":"2020-10-13T18:41:37.02287354Z", "Addresses":["quic://192.168.100.15:22000" ]})"";
        auto r = parse_contact(res);
        CHECK(r.error() == make_error_code(error_code_t::incorrect_json));
    }

    SECTION("malformed date") {
        res.body() = R""( {"seen":"2020-99-13T18:41:37.02287354Z", "addresses":["qic://192.168.100.15:22000" ]})"";
        auto r = parse_contact(res);
        CHECK(r.error() == make_error_code(error_code_t::malformed_date));
    }

    SECTION("wrong code") {
        res.result(500);
        auto r = parse_contact(res);
        CHECK(r.error() == make_error_code(error_code_t::unexpected_response_code));
    }

    SECTION("not found") {
        res.result(404);
        auto r = parse_contact(res);
        REQUIRE((bool)r);
        CHECK(r.value().empty());
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "config/utils.h"
#include "utils/uri.h"
#include "utils/location.h"
#include <filesystem>
#include <sstream>

namespace syncspirit::config {

bool operator==(const bep_config_t &lhs, const bep_config_t &rhs) noexcept {
    return lhs.rx_buff_size == rhs.rx_buff_size && lhs.tx_buff_limit == rhs.tx_buff_limit &&
           lhs.connect_timeout == rhs.connect_timeout && lhs.request_timeout == rhs.request_timeout &&
           lhs.tx_timeout == rhs.tx_timeout && lhs.rx_timeout == rhs.rx_timeout &&
           lhs.blocks_max_requested == rhs.blocks_max_requested &&
           lhs.blocks_simultaneous_write == rhs.blocks_simultaneous_write;
}

bool operator==(const dialer_config_t &lhs, const dialer_config_t &rhs) noexcept {
    return lhs.enabled == rhs.enabled && lhs.redial_timeout == rhs.redial_timeout &&
           lhs.skip_discovers == rhs.skip_discovers;
}

bool operator==(const fs_config_t &lhs, const fs_config_t &rhs) noexcept {
    return lhs.temporally_timeout == rhs.temporally_timeout && lhs.mru_size == rhs.mru_size &&
           lhs.bytes_scan_iteration_limit == rhs.bytes_scan_iteration_limit &&
           lhs.files_scan_iteration_limit == rhs.files_scan_iteration_limit;
}

bool operator==(const db_config_t &lhs, const db_config_t &rhs) noexcept {
    return lhs.upper_limit == rhs.upper_limit && lhs.uncommitted_threshold == rhs.uncommitted_threshold &&
           lhs.max_blocks_per_diff == rhs.max_blocks_per_diff && lhs.max_files_per_diff == rhs.max_files_per_diff;
}

bool operator==(const global_announce_config_t &lhs, const global_announce_config_t &rhs) noexcept {
    return lhs.enabled == rhs.enabled && lhs.debug == rhs.debug && *lhs.announce_url == *rhs.announce_url &&
           lhs.device_id == rhs.device_id && lhs.cert_file == rhs.cert_file && lhs.key_file == rhs.key_file &&
           lhs.rx_buff_size == rhs.rx_buff_size && lhs.timeout == rhs.timeout &&
           lhs.reannounce_after == rhs.reannounce_after;
}

bool operator==(const local_announce_config_t &lhs, const local_announce_config_t &rhs) noexcept {
    return lhs.enabled == rhs.enabled && lhs.port == rhs.port && lhs.frequency == rhs.frequency;
}

bool operator==(const log_config_t &lhs, const log_config_t &rhs) noexcept {
    return lhs.name == rhs.name && lhs.level == rhs.level && lhs.sinks == rhs.sinks;
}

bool operator==(const upnp_config_t &lhs, const upnp_config_t &rhs) noexcept {
    return lhs.enabled == rhs.enabled && lhs.max_wait == rhs.max_wait && lhs.external_port == rhs.external_port &&
           lhs.rx_buff_size == rhs.rx_buff_size && lhs.debug == rhs.debug;
}

bool operator==(const relay_config_t &lhs, const relay_config_t &rhs) noexcept {
    return lhs.enabled == rhs.enabled && lhs.debug == rhs.debug && *lhs.discovery_url == *rhs.discovery_url &&
           lhs.rx_buff_size == rhs.rx_buff_size;
}

bool operator==(const main_t &lhs, const main_t &rhs) noexcept {
    return lhs.local_announce_config == rhs.local_announce_config && lhs.upnp_config == rhs.upnp_config &&
           lhs.global_announce_config == rhs.global_announce_config && lhs.bep_config == rhs.bep_config &&
           lhs.db_config == rhs.db_config && lhs.timeout == rhs.timeout && lhs.device_name == rhs.device_name &&
           lhs.config_path == rhs.config_path && lhs.log_configs == rhs.log_configs &&
           lhs.hasher_threads == rhs.hasher_threads;
}

} // namespace syncspirit::config

namespace sys = boost::system;
namespace fs = std::filesystem;
namespace st = syncspirit::test;

using namespace syncspirit;

TEST_CASE("expand_home", "[config]") {
    SECTION("valid home") {
        auto home = utils::home_option_t(fs::path("/user/home/.config/syncspirit_test"));
        REQUIRE(utils::expand_home("some/path", home) == "some/path");
        REQUIRE(utils::expand_home("~/some/path", home) == "/user/home/.config/syncspirit_test/some/path");
    }

    SECTION("invalid home") {
        auto ec = sys::error_code{1, sys::system_category()};
        auto home = utils::home_option_t(ec);
        REQUIRE(utils::expand_home("some/path", home) == "some/path");
        REQUIRE(utils::expand_home("~/some/path", home) == "~/some/path");
    }
}

TEST_CASE("default config is OK", "[config]") {
    auto dir = st::unique_path();
    fs::create_directory(dir);
    auto dir_guard = st::path_guard_t(dir);
    auto cfg_path = dir / "syncspirit.toml";
    auto cfg_opt = config::generate_config(cfg_path);
    REQUIRE(cfg_opt);
    auto &cfg = cfg_opt.value();
    std::stringstream out;
    SECTION("serialize default") {
        auto r = config::serialize(cfg, out);
        CHECK(r);
        INFO(out.str());
        CHECK(out.str().find("~") == std::string::npos);
        auto cfg_opt = config::get_config(out, cfg_path);
        CHECK(cfg_opt);

        auto cfg2 = cfg_opt.value();
        CHECK(cfg == cfg2);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "utils/log.h"
#include "utils/log-setup.h"
#include <spdlog/sinks/dist_sink.h>
#include <spdlog/sinks/null_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>

namespace st = syncspirit::test;
namespace bfs = std::filesystem;

using namespace syncspirit;

using L = spdlog::level::level_enum;

static bool _init = []() {
    auto dist_sink = utils::create_root_logger();
    auto console_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
    dist_sink->add_sink(console_sink);
    dist_sink->add_sink(std::make_shared<spdlog::sinks::null_sink_mt>());
    return true;
}();

TEST_CASE("default logger", "[log]") {
    config::log_configs_t cfg{{"default", L::trace, {"stdout"}}};
    REQUIRE(utils::init_loggers(cfg));
    auto l = utils::get_logger("default");
    CHECK(l);
    CHECK(l->level() == L::trace);
}

TEST_CASE("hierarchy", "[log]") {
    config::log_configs_t cfg{{"default", L::trace, {"stdout"}}, {"a", L::info, {}}, {"a.b.c", L::warn, {}}};
    REQUIRE(utils::init_loggers(cfg));
    SECTION("custom") {
        auto l = utils::get_logger("a");
        REQUIRE(l);
        CHECK(l->level() == L::info);
    }
    SECTION("submatch") {
        auto l = utils::get_logger("a.b");
        REQUIRE(l);
        CHECK(l->level() == L::info);
    }
    SECTION("full match") {
        auto l = utils::get_logger("a.b.c");
        REQUIRE(l);
        CHECK(l->level() == L::warn);
    }
    SECTION("mismatch") {
        auto l = utils::get_logger("xxx");
        REQUIRE(l);
        CHECK(l->level() == L::trace);
    }
}

TEST_CASE("file sink", "[log]") {
    auto dir = bfs::absolute(bfs::current_path() / st::unique_path());
    auto path_guard = st::path_guard_t{dir};
    bfs::create_directory(dir);
    auto log_file = dir / "log.txt";
    auto log_file_str = log_file.string();

    auto sink_config = fmt::format("file:{}", log_file_str);
    config::log_configs_t cfg{{"default", L::trace, {sink_config}}};
    REQUIRE(utils::init_loggers(cfg));
    auto l = utils::get_logger("default");
    l->info("lorem ipsum dolor");
    l->flush();

    spdlog::drop_all(); // to cleanup on win32
    auto data = st::read_file(log_file);
    CHECK(log_file_str != "");
    CHECK(!data.empty());
    CHECK(data.find("lorem ipsum dolor") != std::string::npos);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023-2024 Ivan Baidakou

#include "test-utils.h"
#include "proto/relay_support.h"

using namespace syncspirit::proto::relay;
using namespace syncspirit::utils;

TEST_CASE("relay proto", "[relay]") {
    bytes_t buff;

    SECTION("successful cases") {
        SECTION("ping") {
            auto sz = serialize(ping_t{}, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<ping_t>(&msg->message);
            REQUIRE(target);
        }

        SECTION("pong") {
            auto sz = serialize(pong_t{}, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<pong_t>(&msg->message);
            REQUIRE(target);
        }

        SECTION("join_relay_request") {
            auto sz = serialize(join_relay_request_t{}, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<join_relay_request_t>(&msg->message);
            REQUIRE(target);
        }

        SECTION("join_session_request") {
            auto key = std::string_view("lorem impsum dolor");
            auto ptr = (unsigned char *)key.data();
            auto source = join_session_request_t(bytes_t(ptr, ptr + key.size()));
            auto sz = serialize(source, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<join_session_request_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->key == source.key);
        }

        SECTION("response") {
            auto source = response_t{404, "not found"};
            auto sz = serialize(source, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<response_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->code == source.code);
            CHECK(target->details == source.details);
        }

        SECTION("connect_request") {
            auto device_id = std::string_view("lorem impsum dolor");
            auto ptr = (unsigned char *)device_id.data();
            auto source = connect_request_t(bytes_t(ptr, ptr + device_id.size()));
            auto sz = serialize(source, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<connect_request_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->device_id == source.device_id);
        }

        SECTION("session_invitation") {
            auto device_id = std::string_view("lorem");
            auto ptr = (unsigned char *)device_id.data();
            auto device_id_bytes = bytes_t(ptr, ptr + device_id.size());
            auto key = std::string_view("impsum dolor");
            ptr = (unsigned char *)key.data();
            auto key_bytes = bytes_t(ptr, ptr + key.size());
            auto ip = boost::asio::ip::address_v4::from_string("127.0.0.1");
            auto source = session_invitation_t(device_id_bytes, key_bytes, ip, 1234, true);
            auto sz = serialize(source, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<session_invitation_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->from == source.from);
            CHECK(target->key == source.key);
            CHECK(target->address.value() == ip);
            CHECK(target->server_socket == source.server_socket);
        }

        SECTION("session_invitation (host of zeroes)") {
            auto device_id = std::string_view("lorem");
            auto ptr = (unsigned char *)device_id.data();
            auto device_id_bytes = bytes_t(ptr, ptr + device_id.size());
            auto key = std::string_view("impsum dolor");
            ptr = (unsigned char *)key.data();
            auto key_bytes = bytes_t(ptr, ptr + key.size());
            auto ip = boost::asio::ip::address_v4(0);
            auto source = session_invitation_t(device_id_bytes, key_bytes, ip, 1234, true);
            auto sz = serialize(source, buff);
            REQUIRE(sz);
            auto r = parse(buff);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == sz);
            auto target = std::get_if<session_invitation_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->from == source.from);
            CHECK(target->key == source.key);
            CHECK(!target->address.has_value());
            CHECK(target->server_socket == source.server_socket);
        }

        SECTION("response sample") {
            const unsigned char data[] = {0x9e, 0x79, 0xbc, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
                                          0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
                                          0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x00};
            auto r = parse({data, sizeof(data)});
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == 28);
            auto target = std::get_if<response_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->code == 0);
            auto details = std::string_view("success");
            CHECK(target->details == details);
        }

        SECTION("response sample-2") {
            const unsigned char data[] = {0x9e, 0x79, 0xbc, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
                                          0x14, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x6e, 0x6f,
                                          0x74, 0x20, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x00, 0x00, 0x00};
            auto r = parse({data, sizeof(data)});
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == 32);
            auto target = std::get_if<response_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->code == 1);
            auto details = std::string_view("not found");
            CHECK(target->details == details);
        }

        SECTION("session invitation sample") {
            const unsigned char data[] = {0x9e, 0x79, 0xbc, 0x40, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x4a, 0x00,
                                          0x00, 0x00, 0x20, 0xf8, 0x31, 0xf5, 0x75, 0xea, 0x61, 0x8a, 0x2f, 0x15, 0xef,
                                          0x67, 0x68, 0x36, 0x4a, 0x62, 0x89, 0xfc, 0x76, 0xb6, 0x73, 0xc8, 0x5a, 0x2a,
                                          0xbe, 0x60, 0x8f, 0x4a, 0xff, 0x27, 0xba, 0x39, 0x02, 0x00, 0x00, 0x00, 0x12,
                                          0x6c, 0x6f, 0x72, 0x65, 0x6d, 0x2d, 0x69, 0x6d, 0x73, 0x70, 0x75, 0x6d, 0x2d,
                                          0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39,
                                          0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
            auto bytes = bytes_view_t(data, sizeof(data));
            auto r = parse(bytes);
            auto msg = std::get_if<wrapped_message_t>(&r);
            REQUIRE(msg);
            CHECK(msg->length == bytes.size());
            auto target = std::get_if<session_invitation_t>(&msg->message);
            REQUIRE(target);
            CHECK(target->from.size() == 32);
            auto ptr = (const char *)(target->key.data());
            CHECK(std::string_view(ptr, target->key.size()) == "lorem-imspum-dolor");
            CHECK(!target->address.has_value());
            CHECK(target->port == 12345);
            CHECK(target->server_socket);
        }
    }

    SECTION("incomplete") {
        auto r = parse(buff);
        auto msg = std::get_if<incomplete_t>(&r);
        REQUIRE(msg);
        serialize(ping_t{}, buff);
        auto view = bytes_view_t(buff);
        r = parse(view.subspan(1));
        msg = std::get_if<incomplete_t>(&r);
        REQUIRE(msg);
    }

    SECTION("protocol errors") {
        SECTION("wrong magic") {
            auto sz = serialize(ping_t{}, buff);
            REQUIRE(sz);
            buff[0] = -1;
            auto r = parse(buff);
            CHECK(std::get_if<protocol_error_t>(&r));
        }
        SECTION("wrong size") {
            auto sz = serialize(ping_t{}, buff);
            REQUIRE(sz);
            buff[4] = -1;
            auto r = parse(buff);
            CHECK(std::get_if<protocol_error_t>(&r));
        }
        SECTION("wrong type") {
            auto sz = serialize(ping_t{}, buff);
            REQUIRE(sz);
            buff[9] = -1;
            auto r = parse(buff);
            CHECK(std::get_if<protocol_error_t>(&r));
        }
    }
}

TEST_CASE("endpoing parsing", "[relay]") {
    std::string body = R""(
{
  "relays": [
    {
      "url": "relay://130.61.176.206:22067/?id=OAKAXEX-7HE764M-5EWVN7U-SZCQU4D-ZPXF2TY-SNTL2LL-Y5RVGVM-U7WBRA3&pingInterval=1m30s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=ina",
      "location": {
        "latitude": 50.1049,
        "longitude": 8.6295,
        "city": "Frankfurt am Main",
        "country": "DE",
        "continent": "EU"
      },
      "stats": {
        "startTime": "2022-04-16T09:12:21.941097618Z",
        "uptimeSeconds": 1312802,
        "numPendingSessionKeys": 0,
        "numActiveSessions": 46,
        "numConnections": 494,
        "numProxies": 88,
        "bytesProxied": 493207937616,
        "goVersion": "go1.16.3",
        "goOS": "linux",
        "goArch": "arm64",
        "goMaxProcs": 4,
        "goNumRoutine": 1136,
        "kbps10s1m5m15m30m60m": [
          241,
          284,
          309,
          332,
          355,
          312
        ],
        "options": {
          "network-timeout": 120,
          "ping-interval": 60,
          "message-timeout": 60,
          "per-session-rate": 0,
          "global-rate": 0,
          "pools": [
            "https://relays.syncthing.net/endpoint"
          ],
          "provided-by": "ina"
        }
      },
      "statsRetrieved": "2022-05-01T13:52:24.524417759Z"
    }
]
}
    )"";
    auto r = parse_endpoint(body);
    REQUIRE(r);
    REQUIRE(r.value().size() == 1);
    auto relay = r.value()[0];
    CHECK(relay->uri->buffer() ==
          "relay://130.61.176.206:22067/"
          "?id=OAKAXEX-7HE764M-5EWVN7U-SZCQU4D-ZPXF2TY-SNTL2LL-Y5RVGVM-U7WBRA3&pingInterval=1m30s&"
          "networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=ina");
    CHECK(relay->device_id.get_short() == "OAKAXEX");
    auto &l = relay->location;
    CHECK(abs(l.latitude - 50.1049) < 0.0001);
    CHECK(abs(l.longitude - 8.6295) < 0.0001);
    CHECK(l.city == "Frankfurt am Main");
    CHECK(l.country == "DE");
    CHECK(l.continent == "EU");
    CHECK(relay->ping_interval == pt::seconds{90});

    auto device = parse_device(relay->uri);
    REQUIRE(device);
    CHECK(device.value() == relay->device_id);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 Ivan Baidakou

#include "test-utils.h"
#include "fs/utils.h"

using namespace syncspirit::fs;

using D = block_division_t;

namespace syncspirit::fs {

bool operator==(const D &lhs, const D &rhs) noexcept { return lhs.count == rhs.count && lhs.size == rhs.size; }

} // namespace syncspirit::fs

TEST_CASE("get_block_size", "[fs]") {
    constexpr std::int64_t kb = 1024;
    constexpr std::int64_t mb = kb * kb;
    constexpr std::int64_t gb = kb * kb * kb;

    SECTION("no prev_size") {
        CHECK(get_block_size(1, 0) == D{1, 1});
        CHECK(get_block_size(2, 0) == D{1, 2});
        CHECK(get_block_size(128 * kb, 0) == D{1, 128 * kb});
        CHECK(get_block_size(128 * kb + 1, 0) == D{2, 128 * kb});

        SECTION("gb") {
            CHECK(gb == 1073741824ull);
            auto d1 = get_block_size(16 * gb, 0);
            auto d2 = D{1024, 16 * mb};
            auto sz = 16 * gb;
            CHECK(d1.size == d2.size);
            CHECK(d1.count == d2.count);
            CHECK(get_block_size(sz, 0) == d2);
        }
    }

    SECTION("with prev_size") {
        CHECK(get_block_size(1, 256 * kb) == D{1, 1});
        CHECK(get_block_size(1 * gb, 256 * kb) == D{4096, 256 * kb});
    };
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "utils/dns.h"

using namespace syncspirit;
using namespace syncspirit::utils;
namespace ip = boost::asio::ip;

TEST_CASE("get_block_size", "[support]") {
    auto str = "192.168.1.100,192.168.1.101:53,[1:2:3::4]:53,[fe80::1]:53%eth0";
    auto endpoints = parse_dns_servers(str);

    CHECK(endpoints.size() >= 2u);

    auto &e0 = endpoints[0];
    CHECK(e0.ip == ip::make_address_v4("192.168.1.100"));
    CHECK(e0.port == 53);

    auto &e1 = endpoints[1];
    CHECK(e1.ip == ip::make_address_v4("192.168.1.101"));
    CHECK(e1.port == 53);

    if (test::has_ipv6()) {
        CHECK(endpoints.size() == 4u);
        auto &e2 = endpoints[2];
        CHECK(e2.ip == ip::make_address_v6("1:2:3::4"));
        CHECK(e2.port == 53);

        auto &e3 = endpoints[3];
        CHECK(e3.ip == ip::make_address_v6("fe80::1"));
        CHECK(e3.port == 53);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/device.h"
#include "model/misc/lru_cache.hpp"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

namespace bfs = std::filesystem;

TEST_CASE("generic map", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto map = devices_map_t();
    map.put(my_device);
    REQUIRE(map.by_sha256(my_id.get_sha256()));
    REQUIRE(map.by_key(my_device->get_key()));

    map.remove(my_device);
    REQUIRE(!map.by_sha256(my_id.get_sha256()));
    REQUIRE(!map.by_key(my_device->get_key()));
    REQUIRE(map == map);
}

namespace syncspirit::model::details {

template <> inline std::string_view get_lru_key<std::string>(const std::string &key) { return key; }

} // namespace syncspirit::model::details

TEST_CASE("lru cache", "[model]") {
    SECTION("string item") {
        mru_list_t<std::string> list(3);
        list.put("a");
        list.put("b");
        list.put("c");

        CHECK(list.get("a") == "a");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "c");
        CHECK(list.get("d") == "");

        list.put("d");
        CHECK(list.get("a") == "");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "c");
        CHECK(list.get("d") == "d");

        list.get("b");
        list.put("e");
        CHECK(list.get("a") == "");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "");
        CHECK(list.get("d") == "d");
        CHECK(list.get("e") == "e");

        list.put("e");
        CHECK(list.get("a") == "");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "");
        CHECK(list.get("d") == "d");
        CHECK(list.get("e") == "e");

        list.remove("d");
        CHECK(list.get("a") == "");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "");
        CHECK(list.get("d") == "");
        CHECK(list.get("e") == "e");

        list.put("c");
        CHECK(list.get("a") == "");
        CHECK(list.get("b") == "b");
        CHECK(list.get("c") == "c");
        CHECK(list.get("d") == "");
        CHECK(list.get("e") == "e");
    }
}

struct item_t {
    utils::bytes_t key;
    int value;
};

using item_map_t = syncspirit::model::generic_map_t<item_t, 1>;

namespace syncspirit::model {

template <> inline utils::bytes_view_t get_index<0>(const item_t &item) noexcept { return item.key; }

} // namespace syncspirit::model

TEST_CASE("generic map ops") {
    item_map_t map;
    auto k = as_owned_bytes("k");
    auto kv = utils::bytes_view_t(k);
    auto i1 = item_t{k, 1};
    auto i2 = item_t{k, 2};

    map.put(i1);
    CHECK(map.get(k).value == 1);
    CHECK(map.get(kv).value == 1);

    map.put(i2);
    CHECK(map.get(k).value == 2);
    CHECK(map.get(kv).value == 2);
    REQUIRE(map.size() == 1);

    map.remove(i2);
    REQUIRE(map.size() == 0);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/device.h"
#include "model/cluster.h"
#include "model/misc/orphaned_blocks.h"
#include "diff-builder.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

namespace bfs = std::filesystem;

TEST_CASE("orphaned blocks, all removed for single file", "[model]") {
    test::init_logging();

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);

    auto builder = diff_builder_t(*cluster);

    REQUIRE(builder.upsert_folder("fid", "/some/path").apply());

    auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto b2_hash = utils::sha256_digest(as_bytes("56789")).value();
    auto b3_hash = utils::sha256_digest(as_bytes("00000")).value();

    SECTION("1 file with 2 different blocsk erased") {
        proto::FileInfo pr_file;
        proto::set_name(pr_file, "a.txt");
        proto::set_block_size(pr_file, 5ul);
        proto::set_size(pr_file, 10ul);

        auto b1 = proto::BlockInfo();
        proto::set_hash(b1, b1_hash);
        proto::set_offset(b1, 5u);
        proto::set_size(b1, 5);
        proto::add_blocks(pr_file, b1);

        auto b2 = proto::BlockInfo();
        proto::set_hash(b2, b2_hash);
        proto::set_offset(b2, 5u);
        proto::set_size(b2, 5);
        proto::add_blocks(pr_file, b2);

        auto bi1 = model::block_info_t::create(b1).value();
        auto bi2 = model::block_info_t::create(b2).value();

        REQUIRE(builder.local_update("fid", pr_file).apply());
        auto &folder_infos = cluster->get_folders().by_id("fid")->get_folder_infos();
        auto &file_infos = folder_infos.by_device(*my_device)->get_file_infos();
        auto file = file_infos.by_name("a.txt");
        REQUIRE(file);

        auto orphans = model::orphaned_blocks_t();
        orphans.record(*file);

        auto blocks = orphans.deduce();
        REQUIRE(blocks.size() == 2);
        CHECK(blocks.contains(bi1->get_key()));
        CHECK(blocks.contains(bi2->get_key()));
    }

    SECTION("1 file with 2 same blocks erased") {
        proto::FileInfo pr_file;
        proto::set_name(pr_file, "a.txt");
        proto::set_block_size(pr_file, 5ul);
        proto::set_size(pr_file, 10ul);

        auto b1 = proto::BlockInfo();
        proto::set_hash(b1, b1_hash);
        proto::set_offset(b1, 5u);
        proto::set_size(b1, 5);
        proto::add_blocks(pr_file, b1);

        auto b2 = proto::BlockInfo();
        proto::set_hash(b2, b1_hash);
        proto::set_offset(b2, 5u);
        proto::set_size(b2, 5);
        proto::add_blocks(pr_file, b2);

        auto bi = model::block_info_t::create(b1).value();

        REQUIRE(builder.local_update("fid", pr_file).apply());
        auto &folder_infos = cluster->get_folders().by_id("fid")->get_folder_infos();
        auto &file_infos = folder_infos.by_device(*my_device)->get_file_infos();
        auto file = file_infos.by_name("a.txt");
        REQUIRE(file);

        auto orphans = model::orphaned_blocks_t();
        orphans.record(*file);

        auto blocks = orphans.deduce();
        REQUIRE(blocks.size() == 1);
        CHECK(blocks.contains(bi->get_key()));
    }

    SECTION("2 file with 1 shared erased") {
        proto::FileInfo pr_file_1;
        proto::set_name(pr_file_1, "a.txt");
        proto::set_block_size(pr_file_1, 5ul);
        proto::set_size(pr_file_1, 10ul);

        auto b_1_1 = [&]() {
            auto b = proto::BlockInfo();
            proto::set_hash(b, b1_hash);
            proto::set_offset(b, 0);
            proto::set_size(b, 5);
            proto::add_blocks(pr_file_1, b);
            return b;
        }();

        auto b_1_2 = [&]() {
            auto b = proto::BlockInfo();
            proto::set_hash(b, b2_hash);
            proto::set_offset(b, 5);
            proto::set_size(b, 5);
            proto::add_blocks(pr_file_1, b);
            return b;
        }();

        proto::FileInfo pr_file_2;
        proto::set_name(pr_file_2, "b.txt");
        proto::set_block_size(pr_file_2, 5ul);
        proto::set_size(pr_file_2, 10ul);

        auto b_2_1 = [&]() {
            auto b = proto::BlockInfo();
            proto::set_hash(b, b1_hash);
            proto::set_offset(b, 0);
            proto::set_size(b, 5);
            proto::add_blocks(pr_file_2, b);
            return b;
        }();

        auto b_2_2 = [&]() {
            auto b = proto::BlockInfo();
            proto::set_hash(b, b3_hash);
            proto::set_offset(b, 5);
            proto::set_size(b, 5);
            proto::add_blocks(pr_file_2, b);
            return b;
        }();

        auto bi_1 = model::block_info_t::create(b_1_1).value();
        auto bi_2 = model::block_info_t::create(b_1_2).value();
        auto bi_3 = model::block_info_t::create(b_2_2).value();

        REQUIRE(builder.local_update("fid", pr_file_1).apply());
        REQUIRE(builder.local_update("fid", pr_file_2).apply());

        auto &folder_infos = cluster->get_folders().by_id("fid")->get_folder_infos();
        auto &file_infos = folder_infos.by_device(*my_device)->get_file_infos();
        auto file_1 = file_infos.by_name("a.txt");
        auto file_2 = file_infos.by_name("b.txt");
        REQUIRE(file_1);
        REQUIRE(file_2);

        auto orphans = model::orphaned_blocks_t();
        orphans.record(*file_1);

        auto blocks = orphans.deduce();
        REQUIRE(blocks.size() == 1);
        CHECK(blocks.contains(bi_2->get_key()));

        orphans.record(*file_2);
        blocks = orphans.deduce();
        REQUIRE(blocks.size() == 3);
        CHECK(blocks.contains(bi_1->get_key()));
        CHECK(blocks.contains(bi_2->get_key()));
        CHECK(blocks.contains(bi_3->get_key()));
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Ivan Baidakou

#include "test-utils.h"
#include "model/version.h"

using namespace syncspirit;
using namespace syncspirit::model;

TEST_CASE("version ", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto proto_v = proto::Vector();
    auto c0 = proto::Counter(1, 2);
    auto c1 = proto::Counter(my_device->device_id().get_uint(), 10);
    proto::add_counters(proto_v, c0);
    proto::add_counters(proto_v, c1);

    auto v1 = version_ptr_t(new version_t(proto_v));
    auto v1_copy = version_ptr_t(new version_t(proto_v));
    v1->update(*my_device);

    CHECK(v1->contains(*v1_copy));
    CHECK(v1->contains(*v1));
    CHECK(!v1_copy->contains(*v1));
    CHECK(v1_copy->contains(*v1_copy));
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2023 Ivan Baidakou

#include "test-utils.h"
#include "model/device_id.h"
#include "proto/luhn32.h"
#include "utils/tls.h"

using namespace syncspirit::test;
using namespace syncspirit::utils;
using namespace syncspirit::model;
using namespace syncspirit::proto;

namespace bfs = std::filesystem;
using l32 = luhn32;

TEST_CASE("luhn32", "[model]") {
    REQUIRE(l32::calculate("WG2IWWALPC2HZ") == 'H');
    REQUIRE(l32::calculate("KHQNO2S5QSILR") == 'K');
    REQUIRE(l32::validate("KHQNO2S5QSILRK"));
    REQUIRE(!l32::validate("KHQNO2S5QSILR"));
    REQUIRE(l32::validate("WG2IWWALPC2HZH"));
    REQUIRE(!l32::validate("WG2IWWALPC2HZ"));
    REQUIRE(l32::validate("MFZWI3DBONSGYC"));
    REQUIRE(!l32::validate("MFZWI3DBONSGY"));
}

TEST_CASE("device_id", "[model]") {
    auto cert_path = locate_path("data/sample-cert.pem");
    auto key_path = locate_path("data/sample-key.pem");
    auto load_result = load_pair(cert_path.string().c_str(), key_path.string().c_str());
    REQUIRE(load_result);
    auto &pair = load_result.value();
    auto opt_device_id = device_id_t::from_cert(pair.cert_data);
    REQUIRE(opt_device_id);

    auto &device_id = opt_device_id.value();
    auto expected = std::string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD");
    REQUIRE(device_id.get_value().size() == expected.size());
    REQUIRE(device_id.get_value() == expected);
    CHECK(device_id.get_short() == "KHQNO2S");

    auto opt = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD");
    REQUIRE((bool)opt);
    CHECK(opt.value() == device_id);

    opt = device_id_t::from_string("KHQNO2S-5QSILRE-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD");
    REQUIRE(!opt);

    opt = device_id_t::from_sha256(device_id.get_sha256());
    REQUIRE((bool)opt);
    CHECK(opt.value() == device_id);

    auto short_id = device_id.get_uint();
    CHECK(short_id != 0);

    CHECK(opt.value() != local_device_id);
}

TEST_CASE("device_id, short", "[model]") {
    auto device = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto d2 = device_id_t::from_sha256(device.get_sha256());
    auto short_str = device_id_t::make_short(device.get_uint());
    CHECK(short_str == device.get_short());
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include <catch2/catch_all.hpp>
#include "test-utils.h"
#include "model/cluster.h"
#include "model/file_info.h"
#include "model/misc/sequencer.h"

using namespace syncspirit;
using namespace syncspirit::model;

using Catch::Matchers::Matches;

TEST_CASE("file-info", "[model]") {
    auto sequencer = make_sequencer(4);
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    db::Folder db_folder;
    db::set_id(db_folder, "1234-5678");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    db::FolderInfo db_folder_info;
    db::set_index_id(db_folder_info, 2);
    db::set_max_sequence(db_folder_info, 3);
    auto folder_info = folder_info_t::create(sequencer->next_uuid(), db_folder_info, my_device, folder).value();

    proto::FileInfo pr_fi;
    auto name = "a/b.txt";
    proto::set_name(pr_fi, name);
    proto::set_size(pr_fi, 55ul);
    proto::set_block_size(pr_fi, 5ul);
    proto::set_sequence(pr_fi, 6);
    proto::set_modified_s(pr_fi, 1734680712);
    proto::add_counters(proto::get_version(pr_fi), proto::Counter(my_device->device_id().get_uint(), 0));

    auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, folder_info).value();
    auto map = file_infos_map_t{};

    map.put(fi);
    CHECK(map.by_name(name) == fi);
    CHECK(map.by_sequence(proto::get_sequence(pr_fi)) == fi);

    auto [begin, end] = map.range(0, 10);
    CHECK(std::distance(begin, end) == 1);
    CHECK(begin->item == fi);

    map.remove(fi);
    fi->set_sequence(10);

    map.put(fi);
    CHECK(map.by_name(proto::get_name(pr_fi)) == fi);
    CHECK(map.by_sequence(10) == fi);
    CHECK(!map.by_sequence(proto::get_sequence(pr_fi)));

    auto conflict_name = fi->make_conflicting_name();
    REQUIRE_THAT(conflict_name, Matches("a.b.sync-conflict-202412(\\d){2}-(\\d){6}-KHQNO2S.txt"));
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include <catch2/catch_all.hpp>
#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"
#include "model/cluster.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::test;

using Catch::Matchers::Matches;

TEST_CASE("cluster", "[model]") {

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_id_2 =
        device_id_t::from_string("EAMTZPW-Q4QYERN-D57DHFS-AUP2OMG-PAHOR3R-ZWLKGAA-WQC5SVW-UJ5NXQA").value();

    auto peer_device_1 = device_t::create(peer_id_1, "peer-device-1").value();
    auto peer_device_2 = device_t::create(peer_id_2, "peer-device-2").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device_1);
    devices.put(peer_device_2);

    auto builder = diff_builder_t(*cluster);
    auto folder_1_id = "1234";

    auto sha256_1 = peer_id_1.get_sha256();
    auto sha256_2 = peer_id_2.get_sha256();

    auto r = builder.upsert_folder(folder_1_id, "/my/path-1")
                 .then()
                 .share_folder(sha256_1, folder_1_id, {})
                 .share_folder(sha256_2, folder_1_id, {})
                 .apply();
    REQUIRE(r);

    SECTION("cluster config for d1") {
        auto cc = cluster->generate(*peer_device_1);
        REQUIRE(proto::get_folders_size(cc) == 1);
        auto &f = proto::get_folders(cc, 0);

        CHECK(proto::get_id(f) == folder_1_id);
        REQUIRE(proto::get_devices_size(f) == 3);
        CHECK(proto::get_read_only(f) == false);

        auto dS = (const proto::Device *){nullptr};
        auto d1 = (const proto::Device *){nullptr};
        auto d2 = (const proto::Device *){nullptr};

        for (size_t i = 0; i < 3; ++i) {
            auto &d = proto::get_devices(f, i);
            if (proto::get_id(d) == sha256_1) {
                d1 = &d;
            } else if (proto::get_id(d) == sha256_2) {
                d2 = &d;
            } else if (proto::get_id(d) == my_id.get_sha256()) {
                dS = &d;
            }
        }

        REQUIRE(d1);
        REQUIRE(d2);
        REQUIRE(dS);

        CHECK(proto::get_id(*d1) == sha256_1);
        CHECK(proto::get_id(*d2) == sha256_2);
    }
    SECTION("cluster config for d1, when folder is read-only") {
        auto folder = cluster->get_folders().by_id(folder_1_id);
        auto &folder_type = ((model::folder_data_t *)folder.get())->access<test::to::folder_type>();
        folder_type = db::FolderType::send;

        auto cc = cluster->generate(*peer_device_1);
        REQUIRE(proto::get_folders_size(cc) == 1);
        auto &f = proto::get_folders(cc, 0);

        CHECK(proto::get_id(f) == folder_1_id);
        REQUIRE(proto::get_devices_size(f) == 3);
        CHECK(proto::get_read_only(f));
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2023 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/cluster.h"
#include "model/misc/error_code.h"
#include "model/diff/cluster_diff.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

struct fail_diff_t : diff::cluster_diff_t {
    outcome::result<void> apply_impl(cluster_t &, diff::apply_controller_t &) const noexcept override {
        auto ec = make_error_code(error_code_t::source_device_not_exists);
        return outcome::failure(ec);
    }
};

TEST_CASE("generic cluster diff", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto diff = diff::cluster_diff_ptr_t(new fail_diff_t());
    CHECK(!cluster->is_tainted());
    CHECK(!diff->apply(*cluster, get_apply_controller()));
    CHECK(cluster->is_tainted());
}