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

#pragma once

#include "messages.h"
#include "utils/log.h"
#include "config/relay.h"
#include "proto/relay_support.h"
#include "model/cluster.h"
#include <boost/asio.hpp>
#include <boost/optional.hpp>
#include <boost/smart_ptr/local_shared_ptr.hpp>
#include <rotor/asio/supervisor_asio.h>
#include <optional>
#include <deque>

namespace syncspirit::net {

struct relay_actor_config_t : r::actor_config_t {
    model::cluster_ptr_t cluster;
    config::relay_config_t config;
};

template <typename Actor> struct relay_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&cluster(const model::cluster_ptr_t &value) && noexcept {
        parent_t::config.cluster = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&relay_config(const config::relay_config_t &value) && noexcept {
        parent_t::config.config = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API relay_actor_t : public r::actor_base_t {
    using config_t = relay_actor_config_t;
    template <typename Actor> using config_builder_t = relay_actor_config_builder_t<Actor>;

    relay_actor_t(config_t &config) noexcept;

    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void on_start() noexcept override;
    void shutdown_start() noexcept override;

  private:
    using http_rx_buff_t = payload::http_request_t::rx_buff_ptr_t;
    using request_option_t = std::optional<r::request_id_t>;
    using relays_t = proto::relay::relay_infos_t;
    using tx_item_t = boost::local_shared_ptr<utils::bytes_t>;
    using tx_queue_t = std::deque<tx_item_t>;

    enum rx_state_t : std::uint32_t {
        response = 1 << 0,
        ping = 1 << 1,
        pong = 1 << 2,
        invitation = 1 << 3,
    };

    void request_relay_list() noexcept;
    void connect_to_relay() noexcept;
    void push_master(utils::bytes_t) noexcept;
    void write_master() noexcept;
    void read_master() noexcept;
    void respawn_ping_timer() noexcept;
    void respawn_tx_timer() noexcept;
    void respawn_rx_timer() noexcept;
    void send_ping() noexcept;

    void on_list(message::http_response_t &res) noexcept;
    void on_connect(message::connect_response_t &res) noexcept;
    void on_write(std::size_t sz) noexcept;
    void on_read(std::size_t bytes) noexcept;
    void on_io_error(const sys::error_code &ec, r::plugin::resource_id_t resource) noexcept;
    bool on(proto::relay::message_t &) noexcept;
    bool on(proto::relay::response_t &) noexcept;
    bool on(proto::relay::session_invitation_t &) noexcept;
    void on_ping_timer(r::request_id_t, bool cancelled) noexcept;
    void on_tx_timer(r::request_id_t, bool cancelled) noexcept;
    void on_rx_timer(r::request_id_t, bool cancelled) noexcept;

    model::cluster_ptr_t cluster;
    config::relay_config_t config;
    utils::logger_t log;

    r::address_ptr_t http_client;
    r::address_ptr_t coordinator;
    r::address_ptr_t peer_supervisor;
    http_rx_buff_t http_rx_buff;
    request_option_t http_request;
    relays_t relays;
    int relay_index = -1;
    transport::stream_sp_t master;
    tcp::endpoint master_endpoint;
    fmt::memory_buffer rx_buff;
    std::size_t rx_idx = 0;
    std::uint32_t rx_state = 0;
    tx_queue_t tx_queue;
    std::optional<r::request_id_t> ping_timer;
    std::optional<r::request_id_t> tx_timer;
    std::optional<r::request_id_t> rx_timer;
};

} // namespace syncspirit::net


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

#include "resolver_actor.h"
#include "model/cluster.h"
#include "utils/error_code.h"
#include "utils/format.hpp"
#include "names.h"
#include <ares.h>

using namespace syncspirit::net;
using namespace syncspirit::utils;

namespace {

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

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); }};
}

namespace resource {
r::plugin::resource_id_t timer = 0;
r::plugin::resource_id_t send = 1;
r::plugin::resource_id_t recv = 2;
} // namespace resource
} // namespace

resolver_actor_t::resolver_actor_t(resolver_actor_t::config_t &config)
    : r::actor_base_t{config}, io_timeout{config.resolve_timeout}, hosts_path{config.hosts_path},
      server_addresses{std::move(config.server_addresses)},
      strand{static_cast<ra::supervisor_asio_t *>(config.supervisor)->get_strand()}, channel{nullptr} {

    rx_buff.resize(1500);
    tx_buff = nullptr;
}

void resolver_actor_t::do_initialize(r::system_context_t *ctx) noexcept {
    r::actor_base_t::do_initialize(ctx);

    ares_options opts;
    int opts_mask = 0;
    memset(&opts, 0, sizeof(opts));

    if (hosts_path.size()) {
        opts.hosts_path = const_cast<char *>(hosts_path.data());
        opts_mask |= ARES_OPT_HOSTS_FILE;
    }

    auto status = ares_library_init(ARES_LIB_INIT_ALL);
    if (status != ARES_SUCCESS) {
        auto details = ares_strerror(status);
        LOG_ERROR(log, "cannot init ares library, code: {}, details: {}", static_cast<int>(status), details);
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        return do_shutdown(make_error(ec));
    }

    status = ares_init_options(&channel, &opts, opts_mask);
    if (status != ARES_SUCCESS) {
        auto details = ares_strerror(status);
        LOG_ERROR(log, "cannot do ares_init, code: {}, details: {}", static_cast<int>(status), details);
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        return do_shutdown(make_error(ec));
    }

    auto dns_servers = std::string(server_addresses);
    if (dns_servers.empty()) {
        auto servers = ares_get_servers_csv(channel);
        if (!servers) {
            LOG_ERROR(log, "cannot get dns servers");
            auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
            return do_shutdown(make_error(ec));
        }

        auto servers_guard = make_guard(servers, [](auto str) { ares_free_string(str); });
        LOG_TRACE(log, "got dns servers: {}", servers);
        dns_servers = servers;
    }

    auto dns_addresses = utils::parse_dns_servers(dns_servers);
    if (dns_addresses.empty()) {
        LOG_ERROR(log, "no valid dns servers found");
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        return do_shutdown(make_error(ec));
    }

    auto dns_address = dns_addresses[0];
    for (auto &addr : dns_addresses) {
        if (addr.ip.is_v4()) {
            dns_address = addr;
            break;
        }
    }
    LOG_DEBUG(log, "selected dns server: {}:{}", dns_address.ip, dns_address.port);

    sys::error_code ec;
    auto s = udp_socket_t{strand.context()};
    s.open(boost::asio::ip::udp::v4(), ec);
    if (ec) {
        LOG_WARN(log, "init, can't open socket: {}", ec.message());
        return do_shutdown(make_error(ec));
    }

    auto endpoint = asio::ip::udp::endpoint(dns_address.ip, dns_address.port);
    s.connect(endpoint, ec);
    if (ec) {
        LOG_WARN(log, "init, can't connect to {}: {}", endpoint, ec.message());
        return do_shutdown(make_error(ec));
    }

    sock.reset(new udp_socket_t(std::move(s)));
}

void resolver_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity(names::resolver, false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) { p.register_name(names::resolver, get_address()); });
    plugin.with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
        p.subscribe_actor(&resolver_actor_t::on_request);
        p.subscribe_actor(&resolver_actor_t::on_cancel);
    });
}

void resolver_actor_t::on_start() noexcept {
    LOG_TRACE(log, "{}, on_start", identity);
    r::actor_base_t::on_start();
}

void resolver_actor_t::shutdown_finish() noexcept {
    r::actor_base_t::shutdown_finish();
    ares_destroy(channel);
    ares_library_cleanup();
}

void resolver_actor_t::on_request(message::resolve_request_t &req) noexcept {
    queue.emplace_back(&req);
    process();
}

void resolver_actor_t::on_cancel(message::resolve_cancel_t &message) noexcept {
    if (queue.empty())
        return;
    auto &request_id = message.payload.id;
    auto &source = message.payload.source;
    auto matches = [&](auto &it) {
        auto &payload = it->payload;
        return payload.id == request_id && payload.origin == source;
    };
    for (auto it = queue.begin(); it != queue.end(); ++it) {
        if (matches(*it)) {
            if (current_query->payload.id == request_id && current_query->payload.origin == source) {
                if (timer_id) {
                    cancel_timer();
                } else {
                    auto ec = r::make_error_code(r::error_code_t::cancelled);
                    reply_with_error(**it, make_error(ec));
                    queue.erase(it);
                }
            } else {
                cancel_timer();
            }
            return;
        }
    }
}

void resolver_actor_t::mass_reply(const utils::dns_query_t &query, const resolve_results_t &results) noexcept {
    reply(query, [&](auto &message) { reply_to(message, results); });
    current_query.reset();
    cache[query] = results;
}

void resolver_actor_t::mass_reply(const utils::dns_query_t &quey, const std::error_code &ec) noexcept {
    reply(quey, [&](auto &message) { reply_with_error(message, make_error(ec)); });
}

void resolver_actor_t::process() noexcept {
    if (resources->has(resource::recv) || resources->has(resource::send)) {
        return;
    }
    if (queue.empty())
        return;
    auto queue_it = queue.begin();
    auto &payload = (*queue_it)->payload.request_payload;
    auto &query = *payload;
    auto cache_it = cache.find(query);
    if (cache_it != cache.end()) {
        mass_reply(query, cache_it->second);
        LOG_TRACE(log, "cache hit for '{}'", query.host);
        return process();
    }
    if (queue.empty())
        return;
    resolve_start(*queue_it);
}

bool resolver_actor_t::resolve_locally(const utils::dns_query_t &query) noexcept {
    auto host = query.host.c_str();
    hostent *host_ent;
    auto result = ares_gethostbyname_file(channel, host, AF_INET, &host_ent);
    if (result != ARES_SUCCESS) {
        LOG_TRACE(log, "host '{}' is not found in hosts file", host);
    } else {
        auto guard = make_guard(host_ent, [](auto *ptr) { ares_free_hostent(ptr); });
        auto results = payload::address_response_t::resolve_results_t();
        auto p_addr = host_ent->h_addr_list;
        while (p_addr && *p_addr) {
            char buff[INET6_ADDRSTRLEN + 1] = {0};
            auto family = host_ent->h_length == 4 ? AF_INET : AF_INET6;
            ares_inet_ntop(family, *p_addr, buff, INET6_ADDRSTRLEN);
            auto addr_string = std::string_view(buff);
            LOG_DEBUG(log, "{} => {}, resolved via hosts file", host, addr_string);

            auto ec = sys::error_code{};
            auto ip = asio::ip::make_address(buff, ec);
            if (ec) {
                LOG_WARN(log, "invalid ip address {}: ", buff, ec.message());
                continue;
            }
            results.emplace_back(std::move(ip));
            ++p_addr;
        }

        if (results.size()) {
            mass_reply(query, std::move(results));
            return true;
        }
    }
    return false;
}

bool resolver_actor_t::resolve_as_ip(const utils::dns_query_t &query) noexcept {
    sys::error_code ec;
    auto host = std::string_view(query.host);
    if (host.size() && host[0] == '[') {
        auto idx = host.find_last_of(']');
        if (idx != host.npos) {
            host = host.substr(1, idx - 1);
        }
    }
    auto ip = asio::ip::make_address(host, ec);
    if (!ec) {
        auto results = payload::address_response_t::resolve_results_t();
        LOG_DEBUG(log, "{} resolved as ip address", query.host);
        results.emplace_back(std::move(ip));
        mass_reply(query, std::move(results));
        return true;
    }

    return false;
}

void resolver_actor_t::resolve_start(request_ptr_t &req) noexcept {
    if (resources->has_any())
        return;
    if (queue.empty())
        return;

    auto &query = *req->payload.request_payload;
    if (resolve_as_ip(query)) {
        return process();
    }
    if (resolve_locally(query)) {
        return process();
    }

    auto host = query.host.c_str();
    ares_dns_record_t *record_raw;
    auto result = ares_dns_record_create(&record_raw, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, ARES_RCODE_NOERROR);
    if (result != ARES_SUCCESS) {
        LOG_WARN(log, "cannot create dns record: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        reply_with_error(*queue.front(), make_error(ec));
        return queue.pop_front();
    }
    auto record = make_guard(record_raw, [](ares_dns_record_t *ptr) { ares_dns_record_destroy(ptr); });

    result = ares_dns_record_query_add(record.get(), host, ARES_REC_TYPE_A, ARES_CLASS_IN);
    if (result != ARES_SUCCESS) {
        LOG_WARN(log, "cannot record dns query: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        reply_with_error(*queue.front(), make_error(ec));
        return queue.pop_front();
    }

    assert(this->tx_buff == nullptr);
    size_t buff_sz;
    result = ares_dns_write(record.get(), &this->tx_buff, &buff_sz);
    if (result != ARES_SUCCESS) {
        LOG_WARN(log, "cannot serialized dns query: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        reply_with_error(*queue.front(), make_error(ec));
        return queue.pop_front();
    }

    auto fwd_read = ra::forwarder_t(*this, &resolver_actor_t::on_read, &resolver_actor_t::on_read_error);
    auto rx_buff = asio::buffer(this->rx_buff.data(), this->rx_buff.size());
    sock->async_receive(rx_buff, std::move(fwd_read));
    resources->acquire(resource::recv);

    auto fwd_write = ra::forwarder_t(*this, &resolver_actor_t::on_write, &resolver_actor_t::on_write_error);
    auto tx_buff = asio::buffer(this->tx_buff, buff_sz);
    sock->async_send(tx_buff, std::move(fwd_write));
    resources->acquire(resource::send);

    timer_id = start_timer(io_timeout, *this, &resolver_actor_t::on_timer);
    resources->acquire(resource::timer);
    current_query = req;
}

void resolver_actor_t::on_timer(r::request_id_t, bool cancelled) noexcept {
    resources->release(resource::timer);
    auto cancel_socket = [this]() {
        if (sock) {
            sys::error_code ec;
            sock->cancel(ec);
            if (ec) {
                LOG_WARN(log, "cannot cancel socket: {}", ec.message());
            }
        }
    };
    if (cancelled) {
        if (resources->has(resource::recv) || resources->has(resource::send)) {
            cancel_socket();
            // reply_with_error(*current_query, make_error(ec));
            // queue.pop_front();
        }
    } else {
        if (!queue.empty() && current_query) {
            // could be actually some other ec...
            auto ec = r::make_error_code(r::error_code_t::request_timeout);
            auto &payload = current_query->payload.request_payload;
            LOG_DEBUG(log, "timeout while resolving '{}'", payload->host);
            mass_reply(*payload, ec);
            cancel_socket();
        }
    }
    timer_id.reset();
    process();
}

void resolver_actor_t::cancel_timer() noexcept {
    if (timer_id) {
        r::actor_base_t::cancel_timer(*timer_id);
    }
}

void resolver_actor_t::on_write(size_t) noexcept {
    resources->release(resource::send);
    ares_free_string(tx_buff);
    tx_buff = nullptr;
}

void resolver_actor_t::on_write_error(const sys::error_code &ec) noexcept {
    resources->release(resource::send);
    if (ec != asio::error::operation_aborted) {
        LOG_WARN(log, "on_write_error, error = {}", ec.message());
    }
    if (current_query) {
        auto &payload = current_query->payload.request_payload;
        mass_reply(*payload, ec);
    }
    process();
}

void resolver_actor_t::on_read_error(const sys::error_code &ec) noexcept {
    resources->release(resource::recv);
    if (ec != asio::error::operation_aborted) {
        LOG_WARN(log, "on_read_error, error = {}", ec.message());
    }
    if (current_query) {
        auto &payload = current_query->payload.request_payload;
        mass_reply(*payload, ec);
    }
    process();
}

void resolver_actor_t::on_read(size_t bytes) noexcept {
    resources->release(resource::recv);

    ares_dns_record_t *record_raw;
    auto data = reinterpret_cast<const unsigned char *>(rx_buff.data());
    auto result = ares_dns_parse(data, bytes, ARES_DNS_PARSE_AN_BASE_RAW, &record_raw);
    if (result != ARES_SUCCESS) {
        LOG_WARN(log, "cannot parse dns reply: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        mass_reply(*current_query->payload.request_payload, ec);
        return process();
    }
    auto record = make_guard(record_raw, [](ares_dns_record_t *ptr) { ares_dns_record_destroy(ptr); });
    auto query_count = ares_dns_record_query_cnt(record_raw);
    if (query_count != 1) {
        LOG_WARN(log, "dns reply query count mismatch {}, expected: 1", query_count);
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        mass_reply(*current_query->payload.request_payload, ec);
        return process();
    }
    const char *queried_name;
    ares_dns_rec_type_t queried_type;
    ares_dns_class_t queried_class;
    result = ares_dns_record_query_get(record_raw, 0, &queried_name, &queried_type, &queried_class);
    if (result != ARES_SUCCESS) {
        LOG_WARN(log, "cannot get initial query from dns reply: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        mass_reply(*current_query->payload.request_payload, ec);
        return process();
    }
    auto &requested_host = current_query->payload.request_payload->host;
    if (queried_name != requested_host) {
        LOG_WARN(log, "dns reply for '{}', it is asked for: '{}'", queried_name, requested_host);
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        mass_reply(*current_query->payload.request_payload, ec);
        return process();
    }

    auto answers_count = ares_dns_record_rr_cnt(record_raw, ARES_SECTION_ANSWER);
    if (answers_count < 1) {
        LOG_WARN(log, "dns reply contains no answers: {}", static_cast<int>(result));
        auto ec = utils::make_error_code(utils::error_code_t::cares_failure);
        mass_reply(*current_query->payload.request_payload, ec);
        return process();
    }

    char addr_buff[INET6_ADDRSTRLEN + 1] = {0};
    auto port = current_query->payload.request_payload->port;
    auto results = payload::address_response_t::resolve_results_t();
    for (size_t i = 0; i < answers_count; ++i) {
        auto resource_record = ares_dns_record_rr_get_const(record_raw, ARES_SECTION_ANSWER, i);
        size_t keys_cnt;
        auto name = ares_dns_rr_get_name(resource_record);
        const ares_dns_rr_key_t *keys = ares_dns_rr_get_keys(ares_dns_rr_get_type(resource_record), &keys_cnt);
        for (size_t k = 0; k < keys_cnt; k++) {
            auto key = keys[k];
            auto type = ares_dns_rr_key_datatype(key);
            if (type == ARES_DATATYPE_INADDR) {
                auto addr = ares_dns_rr_get_addr(resource_record, key);
                ares_inet_ntop(AF_INET, addr, addr_buff, sizeof(addr_buff));
                auto ip = asio::ip::make_address(addr_buff);
                auto ep = tcp::endpoint(ip, port);

                LOG_DEBUG(log, "{} => {}, resolved", name, ep);
            } else if (type == ARES_DATATYPE_BIN) {
                size_t length;
                auto data = ares_dns_rr_get_bin(resource_record, key, &length);
                if (length == 4) {
                    ares_inet_ntop(AF_INET, data, addr_buff, sizeof(addr_buff));
                    auto ip = asio::ip::make_address(addr_buff);
                    LOG_DEBUG(log, "resolved: {} => {}", name, ip);
                    results.emplace_back(ip);
                }
            }
        }
    }

    mass_reply(*current_query->payload.request_payload, results);
    cancel_timer();
    process();
}

repo_name: basiliscos/syncspirit

license: mit


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

#pragma once

#include "messages.h"
#include "utils/log.h"
#include "utils/dns.h"
#include <boost/asio.hpp>
#include <rotor.hpp>
// #include <ares.h>

struct ares_channeldata;
typedef struct ares_channeldata ares_channel_t;

namespace syncspirit {
namespace net {

struct resolver_actor_config_t : public r::actor_config_t {
    using r::actor_config_t::actor_config_t;
    r::pt::time_duration resolve_timeout;
    std::string_view hosts_path;
    std::string server_addresses;
};

template <typename Actor> struct resolver_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&resolve_timeout(const pt::time_duration &value) && noexcept {
        parent_t::config.resolve_timeout = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&hosts_path(std::string_view value) && noexcept {
        parent_t::config.hosts_path = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&server_addresses(std::string value) && noexcept {
        parent_t::config.server_addresses = std::move(value);
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API resolver_actor_t : public r::actor_base_t {

    using config_t = resolver_actor_config_t;
    template <typename Actor> using config_builder_t = resolver_actor_config_builder_t<Actor>;

    explicit resolver_actor_t(config_t &config);
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void do_initialize(r::system_context_t *ctx) noexcept override;
    void on_start() noexcept override;
    void shutdown_finish() noexcept override;

  private:
    using socket_ptr_t = std::unique_ptr<udp_socket_t>;
    using request_ptr_t = r::intrusive_ptr_t<message::resolve_request_t>;
    using resolve_results_t = payload::address_response_t::resolve_results_t;
    using Queue = std::list<request_ptr_t>;
    using Cache = std::unordered_map<utils::dns_query_t, resolve_results_t>;

    void on_request(message::resolve_request_t &req) noexcept;
    void on_cancel(message::resolve_cancel_t &message) noexcept;
    void mass_reply(const utils::dns_query_t &query, const resolve_results_t &results) noexcept;
    void mass_reply(const utils::dns_query_t &query, const std::error_code &ec) noexcept;
    void process() noexcept;
    void resolve_start(request_ptr_t &req) noexcept;
    void on_timer(r::request_id_t, bool cancelled) noexcept;
    void cancel_timer() noexcept;
    bool resolve_as_ip(const utils::dns_query_t &query) noexcept;
    bool resolve_locally(const utils::dns_query_t &query) noexcept;
    void on_read(size_t bytes) noexcept;
    void on_read_error(const sys::error_code &ec) noexcept;
    void on_write(size_t bytes) noexcept;
    void on_write_error(const sys::error_code &ec) noexcept;

    template <typename ReplyFn> void reply(const utils::dns_query_t &query, ReplyFn &&fn) noexcept {
        auto it = queue.begin();
        while (it != queue.end()) {
            auto &message_ptr = *it;
            auto &payload = *message_ptr->payload.request_payload;
            if (query == payload) {
                fn(*message_ptr);
                it = queue.erase(it);
            } else {
                ++it;
            }
        }
    }

    utils::logger_t log;
    pt::time_duration io_timeout;
    std::string_view hosts_path;
    std::string server_addresses;
    asio::io_context::strand &strand;
    std::optional<r::request_id_t> timer_id;
    socket_ptr_t sock;
    ares_channel_t *channel;
    Queue queue;
    Cache cache;
    request_ptr_t current_query;
    fmt::memory_buffer rx_buff;
    unsigned char *tx_buff;
};

} // namespace net
} // namespace syncspirit


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

#include "sink_actor.h"
#include "names.h"

using namespace syncspirit::net;

void sink_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity(names::sink, false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.register_name(names::sink, get_address());
        p.discover_name(names::coordinator, coordinator, false).link(false).callback([&](auto phase, auto &ee) {
            if (!ee && phase == r::plugin::registry_plugin_t::phase_t::linking) {
                auto p = get_plugin(r::plugin::starter_plugin_t::class_identity);
                auto plugin = static_cast<r::plugin::starter_plugin_t *>(p);
                plugin->subscribe_actor(&sink_actor_t::on_model_sink);
            }
        });
    });
}

void sink_actor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    r::actor_base_t::on_start();
}

void sink_actor_t::on_model_sink(model::message::model_update_t &message) noexcept {
    LOG_TRACE(log, "on_model_sink");
    auto custom = const_cast<void *>(message.payload.custom);
    auto diff_ptr = reinterpret_cast<model::diff::cluster_diff_t *>(custom);
    if (diff_ptr) {
        auto diff = model::diff::cluster_diff_ptr_t(diff_ptr, false);
        send<model::payload::model_update_t>(coordinator, std::move(diff));
    }
}


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

#pragma once

#include <rotor.hpp>
#include "model/messages.h"

namespace syncspirit {
namespace net {

namespace r = rotor;

struct SYNCSPIRIT_API sink_actor_t : public r::actor_base_t {
    using r::actor_base_t::actor_base_t;
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void on_start() noexcept override;

  private:
    void on_model_sink(model::message::model_update_t &message) noexcept;

    utils::logger_t log;
    r::address_ptr_t coordinator;
};

} // namespace net
} // namespace syncspirit


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

#include "ssdp_actor.h"
#include "upnp_actor.h"
#include "proto/upnp_support.h"
#include "utils/format.hpp"
#include "names.h"

using namespace syncspirit::net;
using namespace syncspirit::utils;
using namespace syncspirit::proto;

static const constexpr std::size_t RX_BUFF_SIZE = 1500;

namespace {
namespace resource {
r::plugin::resource_id_t send = 0;
r::plugin::resource_id_t recv = 1;
r::plugin::resource_id_t timer = 2;
} // namespace resource
} // namespace

ssdp_actor_t::ssdp_actor_t(ssdp_actor_config_t &cfg)
    : r::actor_base_t::actor_base_t(cfg), cluster{cfg.cluster},
      strand{static_cast<ra::supervisor_asio_t *>(cfg.supervisor)->get_strand()}, upnp_config{cfg.upnp_config},
      upnp_endpoint(v4::from_string(upnp_addr), upnp_port) {
    rx_buff.resize(RX_BUFF_SIZE);
}

void ssdp_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity("net.ssdp", false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>(
        [&](auto &p) { p.discover_name(names::coordinator, coordinator, true).link(false); });
}

void ssdp_actor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    sock = std::make_unique<udp_socket_t>(strand.context());

    auto endpoint = udp::endpoint(udp::v4(), 1234);
    auto ec = sys::error_code{};
    sock->open(endpoint.protocol(), ec);
    if (ec) {
        LOG_WARN(log, "init, can't open socket :: {}", ec.message());
        return do_shutdown(make_error(ec));
    }

    sock->set_option(udp_socket_t::broadcast(true), ec);
    if (ec) {
        LOG_WARN(log, "init, can't set broadcast option :: {}", ec.message());
        return do_shutdown(make_error(ec));
    }

    sock->bind(endpoint, ec);
    if (ec) {
        LOG_WARN(log, "init, can't bind socket {}: {}", endpoint, ec.message());
        return do_shutdown(make_error(ec));
    }

    /* broadcast discovery */
    auto max_wait = upnp_config.max_wait;
    auto request_result = make_discovery_request(tx_buff, max_wait);
    if (!request_result) {
        auto &ec = request_result.error();
        LOG_ERROR(log, "cannot serialize discovery request: {}", ec.message());
        return do_shutdown(make_error(ec));
    }

    LOG_TRACE(log, "sending multicast request to {}:{} ({} bytes)", upnp_addr, upnp_port, tx_buff.size());

    auto fwd_receive = ra::forwarder_t(*this, &ssdp_actor_t::on_discovery_received, &ssdp_actor_t::on_udp_recv_error);
    auto buff_rx = asio::buffer(rx_buff.data(), RX_BUFF_SIZE);
    sock->async_receive(buff_rx, std::move(fwd_receive));
    resources->acquire(resource::recv);

    auto fwd_discovery = ra::forwarder_t(*this, &ssdp_actor_t::on_discovery_sent, &ssdp_actor_t::on_udp_send_error);
    auto buff_tx = asio::buffer(tx_buff.data(), tx_buff.size());
    sock->async_send_to(buff_tx, upnp_endpoint, std::move(fwd_discovery));
    resources->acquire(resource::send);

    auto timeout = pt::seconds(max_wait);
    timer_request = start_timer(timeout, *this, &ssdp_actor_t::on_timer);
    resources->acquire(resource::timer);
}

void ssdp_actor_t::shutdown_start() noexcept {
    LOG_TRACE(log, "shutdown_start");
    sys::error_code ec;
    if (resources->has(resource::send) || resources->has(resource::recv)) {
        sock->cancel(ec);
        if (ec) {
            LOG_ERROR(log, "udp socket cancellation : {}", ec.message());
        }
    }
    timer_cancel();
    r::actor_base_t::shutdown_start();
}

void ssdp_actor_t::on_discovery_sent(std::size_t bytes) noexcept {
    LOG_TRACE(log, "on_discovery_sent ({} bytes)", bytes);
    resources->release(resource::send);

    auto endpoint = sock->local_endpoint();
    LOG_TRACE(log, "will wait discovery reply via {}:{}", endpoint.address().to_string(), endpoint.port());
}

void ssdp_actor_t::on_discovery_received(std::size_t bytes) noexcept {
    LOG_TRACE(log, "on_discovery_received ({} bytes)", bytes);
    resources->release(resource::recv);

    if (!bytes || !resources->has(resource::timer)) {
        auto ec = r::make_error_code(r::shutdown_code_t::normal);
        return do_shutdown(make_error(ec));
    }

    const char *buff = reinterpret_cast<const char *>(rx_buff.data());
    auto discovery_result = parse(buff, bytes);
    if (!discovery_result) {
        auto &ec = discovery_result.error();
        LOG_WARN(log, "can't get discovery result: {}", ec.message());
        return do_shutdown(make_error(ec));
    }

    auto &&value = discovery_result.value();
    auto &igd_location = value.location;

    launch_upnp(igd_location);
    timer_cancel();
    do_shutdown();
}

void ssdp_actor_t::on_udp_send_error(const sys::error_code &ec) noexcept {
    resources->release(resource::send);
    if (ec != asio::error::operation_aborted) {
        LOG_WARN(log, "on_udp_send_error :: {}", ec.message());
        do_shutdown(make_error(ec));
    }
    timer_cancel();
}

void ssdp_actor_t::on_udp_recv_error(const sys::error_code &ec) noexcept {
    resources->release(resource::recv);
    if (ec != asio::error::operation_aborted) {
        LOG_WARN(log, "on_udp_recv_error :: {}", ec.message());
        do_shutdown(make_error(ec));
    }
    timer_cancel();
}

void ssdp_actor_t::on_timer(r::request_id_t, bool cancelled) noexcept {
    resources->release(resource::timer);
    if (!cancelled) {
        LOG_DEBUG(log, "on_timer_trigger");
    }
    auto ec = r::make_error_code(r::shutdown_code_t::normal);
    return do_shutdown(make_error(ec));
}

void ssdp_actor_t::timer_cancel() noexcept {
    if (resources->has(resource::timer)) {
        cancel_timer(*timer_request);
    }
}

void ssdp_actor_t::launch_upnp(const uri_ptr_t &igd_uri) noexcept {
    LOG_DEBUG(log, "launching upnp");
    auto timeout = shutdown_timeout * 9 / 10;

    auto &sup = get_supervisor();
    sup.create_actor<upnp_actor_t>()
        .cluster(cluster)
        .timeout(timeout)
        .descr_url(igd_uri)
        .rx_buff_size(upnp_config.rx_buff_size)
        .external_port(upnp_config.external_port)
        .debug(upnp_config.debug)
        .finish()
        ->get_address();
}


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

#pragma once

#include "messages.h"
#include "model/cluster.h"
#include "config/upnp.h"
#include "utils/log.h"
#include <boost/asio.hpp>
#include <rotor/asio/supervisor_asio.h>
#include <optional>

namespace syncspirit {
namespace net {

struct ssdp_actor_config_t : r::actor_config_t {
    config::upnp_config_t upnp_config;
    model::cluster_ptr_t cluster;
};

template <typename Actor> struct ssdp_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&cluster(const model::cluster_ptr_t &value) && noexcept {
        parent_t::config.cluster = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&upnp_config(const config::upnp_config_t &value) && noexcept {
        parent_t::config.upnp_config = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API ssdp_actor_t : public r::actor_base_t {
    using config_t = ssdp_actor_config_t;
    template <typename Actor> using config_builder_t = ssdp_actor_config_builder_t<Actor>;

    explicit ssdp_actor_t(ssdp_actor_config_t &cfg);

    void on_start() noexcept override;
    void shutdown_start() noexcept override;
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;

  private:
    void on_discovery_sent(std::size_t bytes) noexcept;
    void on_udp_send_error(const sys::error_code &ec) noexcept;
    void on_udp_recv_error(const sys::error_code &ec) noexcept;
    void on_discovery_received(std::size_t bytes) noexcept;
    void on_timer(r::request_id_t, bool cancelled) noexcept;
    void timer_cancel() noexcept;
    void launch_upnp(const utils::uri_ptr_t &igd_uri) noexcept;

    model::cluster_ptr_t cluster;
    utils::logger_t log;
    asio::io_context::strand &strand;
    std::optional<r::request_id_t> timer_request;
    std::unique_ptr<udp_socket_t> sock;
    config::upnp_config_t upnp_config;
    r::address_ptr_t coordinator;
    udp::endpoint upnp_endpoint;

    utils::bytes_t tx_buff;
    utils::bytes_t rx_buff;
};

} // namespace net
} // namespace syncspirit


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

#include "upnp_actor.h"
#include "proto/upnp_support.h"
#include "utils/error_code.h"
#include "utils/format.hpp"
#include "names.h"
#include "model/messages.h"
#include "model/diff/contact/update_contact.h"

using namespace syncspirit::net;
using namespace syncspirit::utils;
using namespace syncspirit::proto;

namespace {
namespace resource {
r::plugin::resource_id_t external_port = 0;
r::plugin::resource_id_t http_req = 1;
} // namespace resource
} // namespace

upnp_actor_t::upnp_actor_t(config_t &cfg)
    : r::actor_base_t{cfg}, cluster{cfg.cluster}, main_url{cfg.descr_url}, rx_buff_size{cfg.rx_buff_size},
      external_port(cfg.external_port), debug{cfg.debug} {}

void upnp_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity("net.upnp", false);
        log = utils::get_logger(identity);
        addr_description = p.create_address();
        addr_external_ip = p.create_address();
        addr_mapping = p.create_address();
        addr_unmapping = p.create_address();
        addr_validate = p.create_address();
    });
    plugin.with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
        p.subscribe_actor(&upnp_actor_t::on_igd_description, addr_description);
        p.subscribe_actor(&upnp_actor_t::on_external_ip, addr_external_ip);
        p.subscribe_actor(&upnp_actor_t::on_mapping_port, addr_mapping);
        p.subscribe_actor(&upnp_actor_t::on_unmapping_port, addr_unmapping);
        p.subscribe_actor(&upnp_actor_t::on_validate, addr_validate);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.discover_name(names::http10, http_client, true).link(true);
        p.discover_name(names::coordinator, coordinator).link();
    });
    plugin.with_casted<r::plugin::link_client_plugin_t>([&](auto &p) {
        p.on_unlink([&](auto &req) {
            if (resources->has(resource::external_port)) {
                unlink_request = &req;
                return true;
            } else {
                return false;
            }
        });
    });
}

void upnp_actor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    r::actor_base_t::on_start();
    rx_buff = std::make_shared<payload::http_request_t::rx_buff_t>();

    utils::bytes_t tx_buff;
    auto res = make_description_request(tx_buff, main_url);
    if (!res) {
        auto &ec = res.error();
        LOG_TRACE(log, "error making description request :: {}", ec.message());
        return do_shutdown(make_error(ec));
    }
    make_request(addr_description, main_url, std::move(tx_buff), true);
}

void upnp_actor_t::make_request(const r::address_ptr_t &addr, utils::uri_ptr_t &uri, utils::bytes_t &&tx_buff,
                                bool get_local_address) noexcept {
    resources->acquire(resource::http_req);
    auto timeout = shutdown_timeout * 8 / 9;
    http_request = request_via<payload::http_request_t>(http_client, addr, uri, std::move(tx_buff), rx_buff,
                                                        rx_buff_size, get_local_address, debug)
                       .send(timeout);
}

void upnp_actor_t::request_finish() noexcept {
    resources->release(resource::http_req);
    http_request.reset();
}

void upnp_actor_t::on_igd_description(message::http_response_t &msg) noexcept {
    LOG_TRACE(log, "on_igd_description, state = {}", (int)state);
    request_finish();

    auto &ee = msg.payload.ee;
    if (ee) {
        auto inner = utils::make_error_code(utils::error_code_t::igd_description_failed);
        LOG_WARN(log, "get IGD description: {}", ee->message());
        return do_shutdown(make_error(inner, ee));
    }
    if (state > r::state_t::OPERATIONAL) {
        return;
    }

    local_address = msg.payload.res->local_addr.value();
    auto &body = msg.payload.res->response.body();
    if (debug) {
        LOG_DEBUG(log, "igd description reply: {}\n", std::string_view(body.data(), body.size()));
    }
    auto igd_result = parse_igd(body.data(), body.size());
    if (!igd_result) {
        auto &ec = igd_result.error();
        LOG_WARN(log, "can't get IGD result: {}", ec.message());
        std::string xml(body);
        LOG_DEBUG(log, "xml:\n{0}\n", xml);
        return do_shutdown(make_error(ec));
    }

    rx_buff->consume(msg.payload.res->bytes);
    auto &igd = igd_result.value();
    auto host = std::string(main_url->host());
    auto port = std::string(main_url->port());
    std::string control_url = fmt::format("http://{0}:{1}{2}", host, port, igd.control_path);
    std::string descr_url = fmt::format("http://{0}:{1}{2}", host, port, igd.description_path);
    LOG_DEBUG(log, "IGD control url: {}, description url: {}", control_url, descr_url);

    auto url = utils::parse(control_url.c_str());
    if (!url) {
        LOG_ERROR(log, "can't parse IGD url {}", control_url);
        auto ec = utils::make_error_code(utils::error_code_t::unparsable_control_url);
        return do_shutdown(make_error(ec));
    }
    igd_control_url = url;

    utils::bytes_t tx_buff;
    auto res = make_external_ip_request(tx_buff, igd_control_url);
    if (!res) {
        auto &ec = res.error();
        LOG_TRACE(log, "error making external ip address request :: {}", ec.message());
        return do_shutdown(make_error(ec));
    }
    make_request(addr_external_ip, igd_control_url, std::move(tx_buff));
}

void upnp_actor_t::on_external_ip(message::http_response_t &msg) noexcept {
    LOG_TRACE(log, "on_external_ip");
    request_finish();

    auto &ee = msg.payload.ee;
    if (ee) {
        LOG_WARN(log, "get external IP address: {}", ee->message());
        auto inner = utils::make_error_code(utils::error_code_t::external_ip_failed);
        return do_shutdown(make_error(inner, ee));
    }
    if (state > r::state_t::OPERATIONAL) {
        return;
    }

    auto &body = msg.payload.res->response.body();
    if (debug) {
        LOG_DEBUG(log, "external ip reply: {}\n", std::string_view(body.data(), body.size()));
    }
    auto ip_addr_result = parse_external_ip(body.data(), body.size());
    if (!ip_addr_result) {
        auto &ec = ip_addr_result.error();
        LOG_WARN(log, "can't get external IP address: {}", ec.message());
        std::string xml(body);
        LOG_DEBUG(log, "xml:\n{0}\n", xml);
        return do_shutdown(make_error(ec));
    }
    auto &ip_addr = ip_addr_result.value();
    LOG_DEBUG(log, "external IP addr: {}", ip_addr);
    rx_buff->consume(msg.payload.res->bytes);

    sys::error_code io_ec;
    external_addr = asio::ip::address::from_string(ip_addr, io_ec);
    if (ee) {
        LOG_WARN(log, "can't external IP address '{}' is incorrect: {}", ip_addr, io_ec.message());
        return do_shutdown(make_error(io_ec));
    }

    auto local_port = 0;
    for (auto &uri : cluster->get_device()->get_uris()) {
        if (uri->has_port()) {
            local_port = uri->port_number();
            break;
        }
    }
    LOG_DEBUG(log, "going to map {}:{} => {}:{}", ip_addr, external_port, local_address, local_port);

    utils::bytes_t tx_buff;
    auto res = make_mapping_request(tx_buff, igd_control_url, external_port, local_address.to_string(), local_port);
    if (!res) {
        auto &ec = res.error();
        LOG_TRACE(log, "error making port mapping request :: {}", ec.message());
        return do_shutdown(make_error(ec));
    }
    make_request(addr_mapping, igd_control_url, std::move(tx_buff));
}

void upnp_actor_t::on_mapping_port(message::http_response_t &msg) noexcept {
    LOG_TRACE(log, "on_mapping_port");
    request_finish();

    bool ok = false;
    auto &ee = msg.payload.ee;
    if (ee) {
        LOG_WARN(log, "unsuccessful port mapping: {}", ee->message());
        auto inner = utils::make_error_code(utils::error_code_t::portmapping_failed);
        return do_shutdown(make_error(inner, ee));
    } else if (state > r::state_t::OPERATIONAL) {
        return;
    }

    auto &body = msg.payload.res->response.body();
    if (debug) {
        LOG_DEBUG(log, "mapping port reply: {}\n", std::string_view(body.data(), body.size()));
    }
    auto result = parse_mapping(body.data(), body.size());
    if (!result) {
        LOG_WARN(log, "can't parse port mapping reply : {}", result.error().message());
        std::string xml(body);
        LOG_DEBUG(log, "xml:\n{0}\n", xml);
    } else {
        rx_buff->consume(msg.payload.res->bytes);
        if (!result.value()) {
            LOG_WARN(log, "unsuccessful port mapping");
            LOG_DEBUG(log, "mapping port reply: {}\n", std::string_view(body.data(), body.size()));
        } else {
            LOG_DEBUG(log, "port mapping succeeded");
            ok = true;
        }
    }

    if (ok) {
        utils::bytes_t tx_buff;
        auto res = make_mapping_validation_request(tx_buff, igd_control_url, external_port);
        if (!res) {
            auto &ec = res.error();
            LOG_TRACE(log, "error making port mapping validation request :: {}", ec.message());
            return do_shutdown(make_error(ec));
        }
        make_request(addr_validate, igd_control_url, std::move(tx_buff));
    }
}

void upnp_actor_t::on_unmapping_port(message::http_response_t &msg) noexcept {
    LOG_TRACE(log, "on_unmapping_port");
    request_finish();
    resources->release(resource::external_port);

    auto &ee = msg.payload.ee;
    if (ee) {
        LOG_WARN(log, "upnp_actor:: unsuccessful port mapping: {}", ee->message());
        return;
    }
    auto &body = msg.payload.res->response.body();
    auto content = std::string_view(body.data(), body.size());
    if (debug) {
        LOG_DEBUG(log, "unmapping port reply: {}\n", content);
    }
    auto result = parse_unmapping(body.data(), body.size());
    if (!result) {
        LOG_WARN(log, "can't parse port unmapping reply : {}", result.error().message());
        LOG_DEBUG(log, "xml:\n{0}\n", content);
    } else if (!result.value()) {
        LOG_WARN(log, "port unmapping failed");
        LOG_DEBUG(log, "xml:\n{0}\n", content);
    } else {
        LOG_DEBUG(log, "successfully unmapped external port {}", external_port);
    }
    if (unlink_request) {
        auto p = get_plugin(r::plugin::link_client_plugin_t::class_identity);
        auto plugin = static_cast<r::plugin::link_client_plugin_t *>(p);
        plugin->forget_link(*unlink_request);
        unlink_request.reset();
    }
}

void upnp_actor_t::on_validate(message::http_response_t &msg) noexcept {
    LOG_TRACE(log, "on_validate");
    request_finish();

    auto &ee = msg.payload.ee;
    if (ee) {
        LOG_WARN(log, "upnp_actor:: unsuccessful port mapping: {}", ee->message());
        return;
    }
    auto &body = msg.payload.res->response.body();
    auto content = std::string_view(body.data(), body.size());
    if (debug) {
        LOG_DEBUG(log, "validation port reply: {}\n", content);
    }
    bool ok = false;
    auto result = parse_mapping_validation(body.data(), body.size());
    if (!result) {
        LOG_WARN(log, "can't parse port mapping validation reply : {}", result.error().message());
        LOG_DEBUG(log, "xml:\n{0}\n", content);
    } else if (!result.value()) {
        LOG_WARN(log, "port mapping validation failed");
        LOG_DEBUG(log, "xml:\n{0}\n", content);
    } else {
        LOG_DEBUG(log, "successfully validate external port {} mapping", external_port);
        ok = true;
    }

    if (ok) {
        resources->acquire(resource::external_port);
        using namespace model::diff;
        auto diff = model::diff::cluster_diff_ptr_t{};
        auto external_uri = utils::parse(fmt::format("tcp://{}:{}", external_addr, external_port));
        auto &self = *cluster->get_device();
        auto uris = self.get_uris();
        uris.emplace_back(std::move(external_uri));

        diff = new contact::update_contact_t(*cluster, self.device_id(), std::move(uris));
        send<model::payload::model_update_t>(coordinator, std::move(diff), this);
    }
}

void upnp_actor_t::shutdown_start() noexcept {
    LOG_TRACE(log, "shutdown_start");
    r::actor_base_t::shutdown_start();

    if (resources->has(resource::http_req)) {
        send<message::http_cancel_t::payload_t>(http_client, *http_request, get_address());
    }

    if (resources->has(resource::external_port)) {
        LOG_TRACE(log, "going to unmap external port {}", external_port);
        utils::bytes_t tx_buff;
        auto res = make_unmapping_request(tx_buff, igd_control_url, external_port);
        if (!res) {
            LOG_WARN(log, "error making port mapping request :: {}", res.error().message());
            resources->release(resource::external_port);
            return;
        }
        make_request(addr_unmapping, igd_control_url, std::move(tx_buff));
    }
}


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

#pragma once

#include "messages.h"
#include "model/cluster.h"
#include "utils/log.h"
#include <boost/asio.hpp>
#include <rotor/asio/supervisor_asio.h>
#include <optional>

namespace syncspirit {
namespace net {

struct upnp_actor_config_t : r::actor_config_t {
    utils::uri_ptr_t descr_url;
    std::uint32_t rx_buff_size;
    std::uint16_t external_port;
    model::cluster_ptr_t cluster;
    bool debug;
};

template <typename Actor> struct upnp_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&descr_url(const utils::uri_ptr_t &value) && noexcept {
        parent_t::config.descr_url = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&external_port(std::uint16_t value) && noexcept {
        parent_t::config.external_port = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&rx_buff_size(std::uint32_t value) && noexcept {
        parent_t::config.rx_buff_size = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&cluster(const model::cluster_ptr_t &value) && noexcept {
        parent_t::config.cluster = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&debug(bool value) && noexcept {
        parent_t::config.debug = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API upnp_actor_t : public r::actor_base_t {
    using config_t = upnp_actor_config_t;
    template <typename Actor> using config_builder_t = upnp_actor_config_builder_t<Actor>;

    explicit upnp_actor_t(config_t &cfg);

    void on_start() noexcept override;
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void shutdown_start() noexcept override;

  private:
    using rx_buff_t = payload::http_request_t::rx_buff_ptr_t;
    using unlink_request_t = r::intrusive_ptr_t<r::message::unlink_request_t>;
    using request_option_t = std::optional<r::request_id_t>;

    void on_igd_description(message::http_response_t &res) noexcept;
    void on_external_ip(message::http_response_t &res) noexcept;
    void on_mapping_port(message::http_response_t &res) noexcept;
    void on_unmapping_port(message::http_response_t &res) noexcept;
    void on_validate(message::http_response_t &res) noexcept;
    void make_request(const r::address_ptr_t &addr, utils::uri_ptr_t &uri, utils::bytes_t &&tx_buff,
                      bool get_local_address = false) noexcept;
    void request_finish() noexcept;

    model::cluster_ptr_t cluster;
    utils::logger_t log;
    asio::ip::address local_address;
    utils::uri_ptr_t main_url;
    utils::uri_ptr_t igd_control_url;
    r::address_ptr_t http_client;
    r::address_ptr_t coordinator;
    r::address_ptr_t addr_description; /* for routing */
    r::address_ptr_t addr_external_ip; /* for routing */
    r::address_ptr_t addr_mapping;     /* for routing */
    r::address_ptr_t addr_unmapping;   /* for routing */
    r::address_ptr_t addr_validate;    /* for routing */
    rx_buff_t rx_buff;
    std::uint32_t rx_buff_size;
    std::uint16_t external_port;
    bool debug;
    std::uint16_t local_port;
    asio::ip::address external_addr;
    unlink_request_t unlink_request;
    request_option_t http_request;
};

} // namespace net
} // namespace syncspirit


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

#include "cluster_file_presence.h"
#include "file_entity.h"
#include "presence.h"
#include "model/file_info.h"
#include "model/folder_info.h"
#include "model/misc/resolver.h"

using namespace syncspirit;
using namespace syncspirit::presentation;

using F = presence_t::features_t;

cluster_file_presence_t::cluster_file_presence_t(std::uint32_t default_features_, file_entity_t &entity,
                                                 model::file_info_t &file_info_) noexcept
    : file_presence_t(&entity, file_info_.get_folder_info()->get_device()), file_info{file_info_},
      default_features{default_features_} {
    link(&file_info);
    refresh_features();
    statistics = own_statistics = refresh_own_stats();
}

void cluster_file_presence_t::refresh_features() noexcept {
    features = default_features | F::cluster;
    features |= (file_info.is_dir() ? F::directory : F::file);
    if (file_info.is_deleted()) {
        features |= F::deleted;
    }
    if (file_info.is_link()) {
        features |= F::symblink;
    }
}

auto cluster_file_presence_t::get_file_info() const noexcept -> const model::file_info_t & { return file_info; }

const presence_t *cluster_file_presence_t::determine_best(const presence_t *other) const {
    if (!(other->get_features() & F::cluster)) {
        return this;
    }
    auto o = static_cast<const cluster_file_presence_t *>(other);
    auto r = model::compare(file_info, o->file_info);
    return r >= 0 ? this : o;
}

presence_stats_t cluster_file_presence_t::refresh_own_stats() noexcept {
    std::int32_t local = ((features & F::local) && file_info.is_locally_available()) ? 1 : 0;
    return {file_info.get_size(), 1, 0, local};
}

void cluster_file_presence_t::on_update() noexcept {
    refresh_features();
    auto presence_diff = refresh_own_stats() - own_statistics;
    auto entity_stats = entity->get_stats();
    auto device = file_info.get_folder_info()->get_device();
    auto prev_best = entity->best;
    auto new_best = entity->recalc_best();
    auto best_changed = prev_best != new_best && new_best->device == device;
    auto best_updated = !best_changed && device == new_best->device;
    entity->push_stats(presence_diff, device, best_updated);
    own_statistics += presence_diff;
    if (best_changed) {
        assert(new_best == this);
        auto entity_diff = get_stats(true) - presence_stats_t{entity_stats, 0};
        entity->push_stats(entity_diff, {}, true);
    }
    file_presence_t::on_update();
}


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

#pragma once

#include "file_presence.h"
#include "syncspirit-export.h"
#include "model/file_info.h"

namespace syncspirit::presentation {

struct file_entity_t;

struct SYNCSPIRIT_API cluster_file_presence_t : file_presence_t {
    cluster_file_presence_t(std::uint32_t default_features, file_entity_t &entity,
                            model::file_info_t &file_info) noexcept;

    const model::file_info_t &get_file_info() const noexcept;
    const presence_t *determine_best(const presence_t *) const override;

  protected:
    void on_update() noexcept override;
    void refresh_features() noexcept;
    presence_stats_t refresh_own_stats() noexcept;

    model::file_info_t &file_info;
    std::uint32_t default_features = 0;
};

} // namespace syncspirit::presentation


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

#include "entity.h"
#include "presence.h"
#include <cassert>
#include <utility>

using namespace syncspirit;
using namespace syncspirit::presentation;

using F = presence_t::features_t;

entity_t::entity_t(path_t path_, entity_t *parent_) noexcept
    : parent{parent_}, path(std::move(path_)), best{nullptr}, entities_monitor{nullptr} {}

entity_t::~entity_t() {
    clear_children();
    while (!presences.empty()) {
        presences.front()->clear_presense();
    }
}

void entity_t::clear_children() noexcept {
    for (auto it = children.begin(); it != children.end();) {
        detach_child(**it);
        it = children.erase(it);
    }
}

void entity_t::set_parent(entity_t *value) noexcept {
    parent = value;
    for (auto presence : presences) {
        presence->set_parent(value);
    }
}

void entity_t::push_stats(const presence_stats_t &diff, const model::device_t *source, bool best) noexcept {
    auto current = this;
    while (current) {
        if (best) {
            assert(current->statistics.size >= 0);
            assert(current->statistics.entities >= 0);
            current->statistics += diff;
            assert(current->statistics.size >= 0);
            assert(current->statistics.entities >= 0);
            ++current->generation;
        }
        if (source) {
            bool found = false;
            for (auto p : current->presences) {
                if (p->device == source) {
                    found = true;
                    p->entity_generation--;
                    assert(p->statistics.size >= 0);
                    assert(p->statistics.entities >= 0);
                    assert(p->statistics.cluster_entries >= 0 || p->entity_generation != current->generation);
                    p->statistics += diff;
                    assert(p->statistics.size >= 0);
                    assert(p->statistics.entities >= 0);
                    assert(p->statistics.cluster_entries >= 0 || p->entity_generation != current->generation);
                    break;
                }
            }
            if (!found) { // detached presence
                return;
            }
        }
        current = current->parent;
    }
}

void entity_t::remove_presense(presence_t &item) noexcept {
    auto it = std::find(presences.begin(), presences.end(), &item);
    assert(it != presences.end());
    for (auto it = children.begin(); it != children.end();) {
        auto c = *it;
        bool found = false;
        for (auto p : c->presences) {
            if (p->device == item.device) {
                found = true;
                // critical to stop "child-from-parent" removal propagation
                it = children.erase(it);
                p->clear_presense();
                if (c->presences.empty()) {
                    detach_child(*c);
                } else {
                    children.emplace_hint(it, c);
                }
                break;
            }
        }
        if (!found) {
            ++it;
        }
    }

    bool need_restat = false;
    auto stats = statistics;
    auto own_stats = item.get_own_stats();
    if (&item == best) {
        stats -= own_stats;
        need_restat = true;
        ++own_stats.cluster_entries;
    }
    push_stats(-own_stats, item.device, false);
    presences.erase(it);
    if (need_restat) {
        if (auto best = recalc_best(); best) {
            stats += best->get_own_stats();
        }
    }
    if (stats != statistics) {
        auto diff = stats - statistics;
        push_stats({diff, 0}, {}, true);
    };

    if ((presences.size() == 1) && (presences.front()->get_features() & F::missing)) {
        remove_presense(*presences.front());
        if (parent) {
            auto &siblings = parent->children;
            auto it = siblings.find(this);
            if (it != siblings.end()) {
                parent->detach_child(*this);
                siblings.erase(it);
            }
        }
    }
}

auto entity_t::recalc_best() noexcept -> const presence_t * {
    auto best = (const presence_t *)(nullptr);
    if (parent) {
        best = {};
        for (auto p : presences) {
            if (!p->device) {
                continue;
            }
            if (!best) {
                best = p;
            } else {
                best = p->determine_best(best);
            }
        }
    }
    this->best = best;
    return best;
}

presence_t *entity_t::get_presence(const model::device_t *device) noexcept {
    presence_t *r = nullptr;
    for (auto p : presences) {
        if (p->device == device) {
            return p;
        } else if (!p->device) {
            r = p;
        }
    }
    return r;
}

void entity_t::add_child(entity_t &child) noexcept {
    model::intrusive_ptr_add_ref(&child);
    children.emplace(&child);
    child.set_parent(this);
}

void entity_t::detach_child(entity_t &child) noexcept {
    child.clear_children();
    child.set_augmentation({});

    for (auto r : presences) {
        r->children.clear();
    }
    // push_stats({-child.get_stats(), 0}, nullptr, true);
    auto &child_presences = child.presences;
    while (!child_presences.empty()) {
        child_presences.front()->clear_presense();
    }

    if (auto monitor = get_monitor(); monitor) {
        monitor->on_delete(child);
    }
    // child.parent = nullptr;
    model::intrusive_ptr_release(&child);
}

void entity_t::commit(const path_t &path, const model::device_t *device) noexcept {
    bool do_recurse = (device == nullptr);
    if (!do_recurse) {
        for (size_t i = 0; (i < presences.size()) && !do_recurse; ++i) {
            auto p = presences[i];
            if (p->device == device) {
                do_recurse = true;
            };
        }
    }
    if (!do_recurse) {
        return;
    }

    for (auto child : children) {
        child->commit(path, device);
        statistics += child->get_stats();
    }
    for (auto p : presences) {
        auto parent = p->parent;
        if (parent && path.contains(parent->entity->path)) {
            parent->statistics += p->statistics;
        }
    }

    auto best = recalc_best();
    if (best) {
        statistics += best->get_own_stats();
    }
    ++generation;
    if (auto monitor = get_monitor(); monitor) {
        monitor->on_update(*this);
    }
}

auto entity_t::monitor(entities_monitor_t *monitor_) noexcept -> monitor_guard_t {
    this->entities_monitor = monitor_;
    return monitor_guard_t(this);
}

auto entity_t::get_presences() const noexcept -> const presences_t & { return presences; }

const entity_stats_t &entity_t::get_stats() noexcept { return statistics; }

auto entity_t::get_path() const noexcept -> const path_t & { return path; }

auto entity_t::get_children() noexcept -> children_t & { return children; }

using nc_t = entity_t::name_comparator_t;

bool nc_t::operator()(const entity_t *lhs, const entity_t *rhs) const noexcept {
    return lhs->get_path().get_own_name() < rhs->get_path().get_own_name();
}

bool nc_t::operator()(const entity_t *lhs, const std::string_view rhs) const noexcept {
    return lhs->get_path().get_own_name() < rhs;
}

bool nc_t::operator()(const std::string_view lhs, const entity_t *rhs) const noexcept {
    return lhs < rhs->get_path().get_own_name();
}

using mg_t = entity_t::monitor_guard_t;

mg_t::monitor_guard_t(entity_t *entity_) noexcept : entity{entity_} {}

mg_t::monitor_guard_t(monitor_guard_t &&other) noexcept : entity{nullptr} { std::swap(entity, other.entity); }

mg_t::~monitor_guard_t() {
    if (entity) {
        entity->entities_monitor = {};
    }
}


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

#pragma once

#include "model/misc/proxy.h"
#include "model/device.h"
#include "path.h"
#include "statistics.h"
#include "syncspirit-export.h"

#include <string_view>
#include <set>
#include <type_traits>
#include <vector>

namespace syncspirit::presentation {

struct entity_t;
using entity_ptr_t = model::intrusive_ptr_t<entity_t>;

struct presence_t;
using presence_ptr_t = model::intrusive_ptr_t<presence_t>;
struct cluster_file_presence_t;

struct file_entity_t;
struct folder_entity_t;

struct SYNCSPIRIT_API entities_monitor_t {
    virtual void on_update(const entity_t &entity) noexcept = 0;
    virtual void on_delete(entity_t &entity) noexcept = 0;
};

struct SYNCSPIRIT_API entity_t : model::augmentable_t {
    struct SYNCSPIRIT_API monitor_guard_t {
        monitor_guard_t(entity_t *entity = nullptr) noexcept;
        monitor_guard_t(const monitor_guard_t &) = delete;
        monitor_guard_t(monitor_guard_t &&) noexcept;
        ~monitor_guard_t();

      private:
        entity_t *entity;
    };
    struct SYNCSPIRIT_API name_comparator_t {
        using is_transparent = std::true_type;
        bool operator()(const entity_t *lhs, const entity_t *rhs) const noexcept;
        bool operator()(const entity_t *lhs, const std::string_view rhs) const noexcept;
        bool operator()(const std::string_view lhs, const entity_t *rhs) const noexcept;
    };
    using children_t = std::set<entity_t *, name_comparator_t>;
    using child_presences_t = std::vector<presence_t *>;
    using presences_t = std::vector<presence_t *>;

    entity_t(path_t path, entity_t *parent = nullptr) noexcept;
    virtual ~entity_t();
    const path_t &get_path() const noexcept;

    presence_t *get_presence(const model::device_t *device) noexcept;

    children_t &get_children() noexcept;

    inline entity_t *get_parent() noexcept { return parent; }
    inline const entity_t *get_parent() const noexcept { return parent; }
    inline const presence_t *get_best() const noexcept { return best; }

    void add_child(entity_t &child) noexcept;
    void remove_presense(presence_t &) noexcept;
    const entity_stats_t &get_stats() noexcept;
    const presences_t &get_presences() const noexcept;
    [[nodiscard]] monitor_guard_t monitor(entities_monitor_t *) noexcept;

  protected:
    friend struct presence_t;
    friend struct file_entity_t;
    friend struct folder_entity_t;
    friend struct cluster_file_presence_t;

    void detach_child(entity_t &child) noexcept;
    void clear_children() noexcept;
    void set_parent(entity_t *parent) noexcept;
    void commit(const path_t &path, const model::device_t *device) noexcept;
    void push_stats(const presence_stats_t &diff, const model::device_t *source, bool best) noexcept;
    const presence_t *recalc_best() noexcept;

    inline entities_monitor_t *get_monitor() noexcept {
        auto e = this;
        while (e->parent) {
            e = e->parent;
        }
        return e->entities_monitor;
    }

    entity_t *parent;
    presences_t presences;
    path_t path;
    children_t children;
    entity_stats_t statistics;
    std::uint32_t generation = 0;
    const presence_t *best;
    entities_monitor_t *entities_monitor;
};

} // namespace syncspirit::presentation


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

#include "file_entity.h"
#include "local_file_presence.h"
#include "peer_file_presence.h"
#include "missing_file_presence.h"
#include "model/cluster.h"
#include "model/folder.h"
#include "model/folder_info.h"

#include <memory_resource>
#include <array>

using namespace syncspirit;
using namespace syncspirit::presentation;

file_entity_t::file_entity_t(model::file_info_t &sample_file, path_t path_) noexcept : entity_t(std::move(path_)) {
    using presence_files_t = std::pmr::vector<model::file_info_t *>;

    auto buffer = std::array<std::byte, 32 * sizeof(model::file_info_t *)>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    auto presence_files = presence_files_t(&pool);

    auto &fi_map = sample_file.get_folder_info()->get_folder()->get_folder_infos();
    for (auto &it_fi : fi_map) {
        auto &folder_info = it_fi.item;
        auto &files_map = folder_info->get_file_infos();
        auto file = files_map.by_name(path.get_full_name());
        if (file) {
            presence_files.emplace_back(file.get());
        }
    }

    missing_file = new missing_file_presence_t(*this);
    presences.emplace_back(missing_file.get());

    for (auto &it : presence_files) {
        on_insert(*it);
    }
}

auto file_entity_t::on_insert(model::file_info_t &file_info) noexcept
    -> std::pair<file_presence_t *, presence_stats_t> {
    auto fi = file_info.get_folder_info();
    auto device = fi->get_device();
    for (auto p : presences) {
        if (p->device == device) {
            return {static_cast<file_presence_t *>(p), {}};
        }
    }

    auto is_local = fi->get_folder()->get_cluster()->get_device() == device;
    auto presence = [&]() -> file_presence_t * {
        if (is_local) {
            return new local_file_presence_t(*this, file_info);
        } else {
            return new peer_file_presence_t(*this, file_info);
        }
    }();
    auto parent_presence = presence->set_parent(parent);
    if (parent_presence) {
        parent_presence->get_children().clear();
    }
    while (parent_presence) {
        --parent_presence->entity_generation;
        parent_presence = parent_presence->parent;
    }
    presences.emplace_back(presence);
    auto diff = presence->get_own_stats();
    for (auto &c : children) {
        for (auto p : c->presences) {
            if (p->device == device) {
                if (p->parent != presence) {
                    p->set_parent(presence);
                    auto child_stats = p->get_stats(false);
                    diff += child_stats;
                    presence->statistics += child_stats;
                }
            }
        }
    }
    return {presence, diff};
}


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

#pragma once

#include "model/file_info.h"
#include "entity.h"
#include "syncspirit-export.h"

namespace syncspirit::presentation {

struct file_presence_t;

struct SYNCSPIRIT_API file_entity_t : entity_t {
    file_entity_t(model::file_info_t &sample_file, path_t path) noexcept;
    void set_parent(entity_t *entity) noexcept;
    auto on_insert(model::file_info_t &file_info) noexcept -> std::pair<file_presence_t *, presence_stats_t>;

  private:
    presence_ptr_t missing_file;
};

} // namespace syncspirit::presentation


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

#pragma once

#include "presence.h"
#include "syncspirit-export.h"

namespace syncspirit::presentation {

struct SYNCSPIRIT_API file_presence_t : presence_t {
    using presence_t::presence_t;
};

} // namespace syncspirit::presentation


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

#include "folder_entity.h"
#include "folder_presence.h"
#include "file_presence.h"
#include "file_entity.h"
#include "model/folder.h"
#include "model/file_info.h"

#include <unordered_map>
#include <vector>

using namespace syncspirit;
using namespace syncspirit::presentation;

namespace bfs = std::filesystem;

using file_entity_ptr_t = model::intrusive_ptr_t<file_entity_t>;
using new_files_t = std::unordered_map<std::string_view, file_entity_ptr_t>;
using file_entities_t = std::unordered_map<std::string_view, file_entity_ptr_t>;

static void process(model::folder_info_t *folder_info, entity_t::children_t &files, new_files_t &new_files) {
    auto &files_map = folder_info->get_file_infos();
    new_files.reserve(files_map.size());
    for (auto &it_file : files_map) {
        auto &file = *it_file.item;
        auto name = file.get_name();
        if (new_files.count(name)) {
            continue;
        }
        auto path = path_t(name);
        auto child = file_entity_ptr_t(new file_entity_t(file, std::move(path)));
        new_files.emplace(name, std::move(child));
    }
}

static void process_files(new_files_t &&new_files, orphans_t &orphans, folder_entity_t *self) {
    using entities_vector_t = std::vector<file_entity_ptr_t>;
    auto entities = entities_vector_t();
    entities.reserve(new_files.size());
    for (auto &[_, file_entry] : new_files) {
        entities.emplace_back((std::move(file_entry)));
    }
    auto comparator = [](const file_entity_ptr_t &lhs, const file_entity_ptr_t &rhs) {
        return lhs->get_path().get_full_name() < rhs->get_path().get_full_name();
    };
    std::sort(entities.begin(), entities.end(), comparator);

    auto file_entities = file_entities_t();
    for (auto &entity : entities) {
        auto &path = entity->get_path();
        auto parent = (entity_t *)(nullptr);
        if (path.get_pieces_size() == 1) {
            parent = self;

        } else {
            auto parent_path = path.get_parent_name();
            auto it_parent = file_entities.find(parent_path);
            if (it_parent != file_entities.end()) {
                parent = it_parent->second.get();
            } else {
                orphans.push(entity);
            }
        }
        if (parent) {
            parent->add_child(*entity);
        }
        file_entities[path.get_full_name()] = entity;
    }
}

folder_entity_t::folder_entity_t(model::folder_ptr_t folder_) noexcept : entity_t({}), folder(*folder_.get()) {
    folder.set_augmentation(this);

    // make folder_infos as presence
    auto &folders_map = folder.get_folder_infos();
    presences.reserve(folders_map.size());
    for (auto &it : folders_map) {
        auto &folder_info = *it.item;
        auto p = new folder_presence_t(*this, folder_info);
        presences.emplace_back(p);
    }

    // make all-files as chilren, make hierarchy
    auto new_files = new_files_t();
    for (auto &it : folders_map) {
        process(it.item.get(), children, new_files);
    }
    process_files(std::move(new_files), orphans, this);
    commit(path, nullptr);
}

auto folder_entity_t::on_insert(model::folder_info_t &folder_info) noexcept -> folder_presence_t * {
    auto device = folder_info.get_device();
    for (auto p : presences) {
        if (p->get_device() == device) {
            return {};
        }
    }
    auto p = new folder_presence_t(*this, folder_info);
    presences.emplace_back(p);

    auto new_files = new_files_t();
    process(&folder_info, children, new_files);
    process_files(std::move(new_files), orphans, this);
    statistics = {};
    commit(path, device);
    return p;
}

entity_t *folder_entity_t::on_insert(model::file_info_t &file_info) noexcept {
    auto path = path_t(file_info.get_name());
    auto device = file_info.get_folder_info()->get_device();

    auto entity = static_cast<entity_t *>(this);
    size_t i = 0;
    for (auto piece : path) {
        auto &children = entity->children;
        auto it = children.find(piece);
        if (it != children.end()) {
            entity = *it;
        } else {
            break;
        }
        ++i;
    }

    auto child = file_entity_ptr_t();
    bool has_parent = i + 1 == path.get_pieces_size();
    auto diff = entity_stats_t{};
    if (has_parent) {
        child.reset(new file_entity_t(file_info, std::move(path)));
        entity->add_child(*child);
        orphans.reap_children(child);
        child->commit(child->get_path(), device);

        for (auto p : child->presences) {
            auto device = p->get_device();
            if (device) {
                auto best = child->best == p;
                auto diff = p->get_stats();
                entity->push_stats(diff, p->get_device(), false);
                p->get_children().clear();
            }
        }
        entity->push_stats({child->get_stats(), 0}, nullptr, true);
        recalc_best();
        return child.get();
    } else if (i == path.get_pieces_size()) {
        auto presence_diff = presence_stats_t{};
        for (auto p : presences) {
            if (p->get_device() == device) {
                presence_diff = -p->get_own_stats();
                p->get_children().clear();
                break;
            }
        }
        auto file_entity = static_cast<file_entity_t *>(entity);
        auto [file_presence, file_diff] = file_entity->on_insert(file_info);
        presence_diff += file_diff;
        if (auto parent = entity->parent; parent) {
            parent->push_stats(presence_diff, device, false);
        }
        auto prev_best = entity->best;
        auto new_best = entity->recalc_best();
        if (new_best != prev_best) {
            auto entity_diff = entity_stats_t{};
            if (prev_best) {
                entity_diff -= prev_best->get_own_stats();
            }
            assert(new_best == file_presence);
            entity_diff += new_best->get_own_stats();
            entity->push_stats({entity_diff, 0}, nullptr, true);
        }
        if (auto monitor = get_monitor(); monitor) {
            monitor->on_update(*entity);
        }
        return entity;
    } else {
        auto orphan = orphans.get_by_path(path.get_full_name());
        if (orphan) {
            static_cast<file_entity_t *>(orphan.get())->on_insert(file_info);
        } else {
            child.reset(new file_entity_t(file_info, std::move(path)));
            orphans.push(child);
        }
    }
    return {};
}

auto folder_entity_t::get_folder() noexcept -> model::folder_t & { return folder; }

void folder_entity_t::on_update() noexcept { notify_update(); }
void folder_entity_t::on_delete() noexcept {
    clear_children();
    if (auto monitor = get_monitor(); monitor) {
        monitor->on_delete(*this);
    }
}


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

#pragma once

#include "entity.h"
#include "orphans.h"
#include "model/file_info.h"

namespace syncspirit::presentation {

struct folder_presence_t;

struct SYNCSPIRIT_API folder_entity_t final : entity_t, model::augmentation_t {
    folder_entity_t(model::folder_ptr_t folder) noexcept;
    model::folder_t &get_folder() noexcept;
    folder_presence_t *on_insert(model::folder_info_t &folder_info) noexcept;
    entity_t *on_insert(model::file_info_t &file_info) noexcept;
    void on_update() noexcept override;
    void on_delete() noexcept override;

  private:
    model::folder_t &folder;
    orphans_t orphans;
};

using folder_entity_ptr_t = model::intrusive_ptr_t<folder_entity_t>;

} // namespace syncspirit::presentation


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

#include "folder_presence.h"
#include "folder_entity.h"
#include "model/cluster.h"

using namespace syncspirit::presentation;

folder_presence_t::folder_presence_t(folder_entity_t &entity_, model::folder_info_t &fi) noexcept
    : presence_t(&entity_, fi.get_device()), folder_info{fi} {
    link(&fi);
    features = features_t::folder;
    auto local = fi.get_device() == entity_.get_folder().get_cluster()->get_device();
    features |= (local ? features_t::local : features_t::peer);
}

auto folder_presence_t::get_folder_info() noexcept -> model::folder_info_t & { return folder_info; }


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

#pragma once

#include "presence.h"
#include "syncspirit-export.h"

namespace syncspirit::presentation {

struct folder_entity_t;

struct SYNCSPIRIT_API folder_presence_t : presence_t {
    folder_presence_t(folder_entity_t &entity, model::folder_info_t &folder_info) noexcept;

    model::folder_info_t &get_folder_info() noexcept;

  protected:
    model::folder_info_t &folder_info;
};

} // namespace syncspirit::presentation


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

#include "local_file_presence.h"

using namespace syncspirit;
using namespace syncspirit::presentation;

local_file_presence_t::local_file_presence_t(file_entity_t &entity, model::file_info_t &file_info_) noexcept
    : cluster_file_presence_t(features_t::local, entity, file_info_) {}


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

#pragma once

#include "cluster_file_presence.h"

namespace syncspirit::presentation {

struct SYNCSPIRIT_API local_file_presence_t : cluster_file_presence_t {
    local_file_presence_t(file_entity_t &entity, model::file_info_t &file_info) noexcept;
};

} // namespace syncspirit::presentation


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

#include "missing_file_presence.h"
#include "file_entity.h"

#include <memory_resource>

using namespace syncspirit;
using namespace syncspirit::presentation;

using augmentations_on_stack_t = std::pmr::set<model::augmentation_t *>;

missing_file_presence_t::missing_file_presence_t(file_entity_t &entity_) noexcept : file_presence_t({}, {}) {
    features = features_t::missing | features_t::file;
    entity = &entity_;
}

missing_file_presence_t::~missing_file_presence_t() {
    on_delete();
    entity = nullptr;
    statistics = {};
}

void missing_file_presence_t::add(model::augmentation_t *augmentation) noexcept { augmentations.insert(augmentation); }

void missing_file_presence_t::remove(model::augmentation_t *augmentation) noexcept {
    augmentations.erase(augmentation);
}

template <typename Fn> void on_each(const missing_file_presence_t::augmentations_t &source, Fn &&fn) {
    if (source.size()) {
        auto buffer = std::array<std::byte, 128>();
        auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
        auto allocator = std::pmr::polymorphic_allocator<std::string>(&pool);
        auto b = source.begin();
        auto e = source.end();
        auto copy = augmentations_on_stack_t(b, e, allocator);
        for (auto item : copy) {
            fn(item);
        }
    }
}

void missing_file_presence_t::on_update() noexcept {
    file_presence_t::on_update();
    on_each(augmentations, [](auto augmentation) { augmentation->on_update(); });
}

void missing_file_presence_t::on_delete() noexcept {
    on_each(augmentations, [](auto augmentation) { augmentation->on_delete(); });
    augmentations.clear();
}


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

#pragma once

#include "file_presence.h"
#include "syncspirit-export.h"
#include <set>

namespace syncspirit::presentation {

struct file_entity_t;

struct SYNCSPIRIT_API missing_file_presence_t final : file_presence_t {
    using augmentations_t = std::set<model::augmentation_t *>;

    missing_file_presence_t(file_entity_t &entity) noexcept;
    ~missing_file_presence_t();

    void add(model::augmentation_t *) noexcept;
    void remove(model::augmentation_t *) noexcept;

    void on_update() noexcept override;
    void on_delete() noexcept override;

  private:
    augmentations_t augmentations;
};

} // namespace syncspirit::presentation


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

#include "orphans.h"
#include "entity.h"
#include <deque>
#include <memory_resource>

namespace syncspirit::presentation {

namespace details {

std::string_view get_path(const entity_ptr_t &entity) noexcept { return entity->get_path().get_full_name(); }

std::string_view get_parent(const entity_ptr_t &entity) noexcept { return entity->get_path().get_parent_name(); }

} // namespace details

orphans_t::~orphans_t() {}

void orphans_t::push(entity_ptr_t entity) noexcept { emplace(std::move(entity)); }

void orphans_t::reap_children(entity_ptr_t parent) noexcept {
    auto buffer = std::array<std::byte, 256>();
    auto pool = std::pmr::monotonic_buffer_resource(buffer.data(), buffer.size());
    using queue_t = std::pmr::deque<entity_t *>;
    auto allocator = std::pmr::polymorphic_allocator<entity_t *>(&pool);

    auto &by_parent_name = get<1>();
    auto &by_name = get<0>();
    auto queue = queue_t(allocator);
    queue.emplace_back(parent.get());

    while (!queue.empty()) {
        auto item = queue.front();
        queue.pop_front();

        auto item_name = item->get_path().get_full_name();
        auto [b, e] = by_parent_name.equal_range(item_name);
        for (auto it = b; it != e; ++it) {
            auto &child = **it;
            queue.emplace_back(&child);

            auto it_name = by_name.find(child.get_path().get_full_name());
            // by_name.erase(it_name);

            item->add_child(child);
        }
        by_parent_name.erase(b, e);
    }
}

entity_ptr_t orphans_t::get_by_path(std::string_view path) noexcept {
    auto &by_name = get<0>();
    auto it = by_name.find(path);
    if (it != by_name.end()) {
        return *it;
    }
    return {};
}

} // namespace syncspirit::presentation


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

#pragma once

#include "model/misc/arc.hpp"

#include <boost/multi_index/global_fun.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index_container.hpp>

namespace syncspirit::presentation {

struct entity_t;
using entity_ptr_t = model::intrusive_ptr_t<entity_t>;

namespace details {
namespace mi = boost::multi_index;

std::string_view get_path(const entity_ptr_t &) noexcept;
std::string_view get_parent(const entity_ptr_t &) noexcept;

// clang-format off
using orphans_map_t = mi::multi_index_container<
    entity_ptr_t,
    mi::indexed_by<
        mi::ordered_unique<
            mi::global_fun<const entity_ptr_t&, std::string_view, get_path>
        >,
        mi::ordered_non_unique<
            mi::global_fun<const entity_ptr_t&, std::string_view, get_parent>
        >
    >
>;
// clang-format on

} // namespace details

struct orphans_t : private details::orphans_map_t {
    ~orphans_t();
    void push(entity_ptr_t) noexcept;
    void reap_children(entity_ptr_t) noexcept;
    entity_ptr_t get_by_path(std::string_view path) noexcept;
};

} // namespace syncspirit::presentation


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

#include "path.h"
#include <filesystem>
#include <boost/nowide/convert.hpp>
#include <cstring>
#include <cassert>

namespace bfs = std::filesystem;

using namespace boost::nowide;

namespace syncspirit::presentation {

path_t::path_t(std::string_view full_name) noexcept : name(full_name) {
    auto file_path = bfs::path(widen(full_name));
    auto tmp = std::vector<char>(full_name.size() * 4);
    auto it = file_path.begin();
    auto root_name = it->wstring();
    auto ptr = narrow(tmp.data(), tmp.size(), root_name.data());
    auto prev = std::strlen(tmp.data());
    for (++it; it != file_path.end(); ++it) {
        auto sub_name = it->wstring();
        auto ptr = narrow(tmp.data(), tmp.size(), sub_name.data());
        pieces.emplace_back(prev + 1);
        prev += std::strlen(tmp.data()) + 1;
    }
}

std::size_t path_t::get_pieces_size() const noexcept { return pieces.size() + (!name.empty() ? 1 : 0); }

std::string_view path_t::get_full_name() const noexcept { return name; }

std::string_view path_t::get_parent_name() const noexcept {
    if (pieces.size()) {
        auto last_offset = pieces.back();
        auto view = std::string_view(name);
        return view.substr(0, last_offset - 1);
    }
    return {};
}

std::string_view path_t::get_own_name() const noexcept {
    if (pieces.size()) {
        auto last_offset = pieces.back();
        auto view = std::string_view(name);
        return view.substr(last_offset);
    }
    return name;
}

auto path_t::begin() const noexcept -> iterator_t { return iterator_t(this); };

auto path_t::end() const noexcept -> iterator_t { return iterator_t(); }

bool path_t::contains(const path_t &other) const noexcept {
    auto view = std::string_view(other.name);
    return view.find(name) == 0;
}

using I = path_t::iterator_t;

I::iterator_t() noexcept : position{-1}, path{nullptr} {}

I::iterator_t(const path_t *path_) noexcept : position{0}, path{path_} {}

auto I::operator*() const noexcept -> reference {
    assert(path);
    assert(position >= 0);
    assert(position <= path->pieces.size());

    auto b = position ? path->pieces[position - 1] : 0;
    auto e = position < path->pieces.size() ? path->pieces[position] - 1 : std::string::npos;
    auto s = e - b;
    auto view = std::string_view(path->name);
    return view.substr(b, s);
}

I &I::operator++() noexcept {
    assert(path);
    auto sz = path->pieces.size();

    assert(position <= sz);
    ++position;
    if (position > sz) {
        path = nullptr;
        position = -1;
    }
    return *this;
}

bool I::operator==(iterator_t other) noexcept { return (path == other.path) && position == other.position; }

} // namespace syncspirit::presentation


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

#pragma once

#include "syncspirit-export.h"
#include <string>
#include <vector>
#include <cstdint>

namespace syncspirit::presentation {

struct SYNCSPIRIT_API path_t {

    struct SYNCSPIRIT_API iterator_t {
        using difference_type = std::ptrdiff_t;
        using element_type = std::string_view;
        using pointer = element_type *;
        using reference = element_type;

        iterator_t() noexcept;
        iterator_t(const path_t *path) noexcept;
        reference operator*() const noexcept;
        iterator_t &operator++() noexcept;
        bool operator==(iterator_t) noexcept;

        std::int32_t position;
        const path_t *path;
    };

    path_t() noexcept = default;
    path_t(std::string_view full_name) noexcept;
    path_t(path_t &&) noexcept = default;

    iterator_t begin() const noexcept;
    iterator_t end() const noexcept;

    std::size_t get_pieces_size() const noexcept;

    std::string_view get_full_name() const noexcept;
    std::string_view get_own_name() const noexcept;
    std::string_view get_parent_name() const noexcept;
    bool contains(const path_t &other) const noexcept;

  private:
    using pieces_t = std::vector<std::uint32_t>;
    std::string name;
    pieces_t pieces;
};

} // namespace syncspirit::presentation


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

#include "peer_file_presence.h"

using namespace syncspirit;
using namespace syncspirit::presentation;

peer_file_presence_t::peer_file_presence_t(file_entity_t &entity, model::file_info_t &file_info_) noexcept
    : cluster_file_presence_t(features_t::peer, entity, file_info_) {}


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

#pragma once

#include "cluster_file_presence.h"

namespace syncspirit::presentation {

struct SYNCSPIRIT_API peer_file_presence_t : cluster_file_presence_t {
    peer_file_presence_t(file_entity_t &entity, model::file_info_t &file_info) noexcept;
};

} // namespace syncspirit::presentation


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

#include "entity.h"
#include "presence.h"
#include "cluster_file_presence.h"

using namespace syncspirit;
using namespace syncspirit::presentation;

using F = presence_t::features_t;

presence_t::presence_t(entity_t *entity_, model::device_t *device_) noexcept
    : entity{entity_}, device{device_}, parent{nullptr} {
    if (entity) {
        model::intrusive_ptr_add_ref(entity);
    }
}

presence_t::~presence_t() { clear_presense(); }

presence_t *presence_t::get_parent() noexcept { return parent; }
entity_t *presence_t::get_entity() noexcept { return entity; }
auto presence_t::get_features() const noexcept -> std::uint32_t { return features; }
const presence_stats_t &presence_t::get_own_stats() const noexcept { return own_statistics; }

const presence_stats_t &presence_t::get_stats(bool sync) const noexcept {
    if (sync) {
        sync_with_entity();
    }
    return statistics;
}

presence_t *presence_t::set_parent(entity_t *value) noexcept {
    if (value && device) {
        auto p = value->get_presence(device);
        if (p && p->device == device) {
            parent = p;
        }
    }
    return parent;
}

void presence_t::set_parent(presence_t *value) noexcept {
    assert(entity->get_parent() == value->entity);
    parent = value;
}

void presence_t::clear_presense() noexcept {
    if (extension) {
        extension->on_delete();
        extension = {};
    }
    if (entity) {
        entity->remove_presense(*this);
        if (!(features & F::missing)) {
            auto ex_entity = entity;
            model::intrusive_ptr_release(ex_entity);
        }
        assert(statistics.size == 0);
        assert(statistics.entities == 0);
        entity = nullptr;
    }
}

void presence_t::link(augmentable_t *augmentable) noexcept { augmentable->set_augmentation(this); }

void presence_t::on_delete() noexcept { clear_presense(); }
void presence_t::on_update() noexcept {
    if (auto monitor = entity->get_monitor(); monitor) {
        monitor->on_update(*entity);
    }
}

const presence_t *presence_t::determine_best(const presence_t *other) const { return other; }

void presence_t::sync_with_entity() const noexcept {
    static constexpr auto mask = F::cluster | F::folder;
    if (entity_generation != entity->generation && (features & mask)) {
        entity_generation = entity->generation;
        statistics.cluster_entries = 0;

        for (auto &child_entity : entity->get_children()) {
            auto child_presence = const_cast<presence_t *>(child_entity->get_presence(device));
            if (child_presence->features & F::cluster) {
                auto child = static_cast<cluster_file_presence_t *>(child_presence);
                child->sync_with_entity();
                statistics.cluster_entries += child->statistics.cluster_entries;
            }
        }

        if (features & (F::file | F::directory)) {
            auto best_version = proto::Counter();
            for (auto p : entity->presences) {
                if ((p == entity->best) && (p->get_features() & F::cluster)) {
                    auto best = static_cast<cluster_file_presence_t *>(p);
                    best_version = best->get_file_info().get_version()->get_best();
                    break;
                }
            }
            assert(features & F::cluster);
            auto const_self = static_cast<const cluster_file_presence_t *>(this);
            auto self = const_cast<cluster_file_presence_t *>(const_self);
            if (self->get_file_info().get_version()->get_best() == best_version) {
                ++statistics.cluster_entries;
            }
        } else if (features & F::directory) {
            ++statistics.cluster_entries;
        }
    }
}

bool presence_t::compare(const presence_t *l, const presence_t *r) noexcept {
    auto lf = l->get_features();
    if (lf & F::missing) {
        l = l->entity->best;
        lf = l->get_features();
    }
    auto rf = r->get_features();
    if (rf & F::missing) {
        r = r->entity->best;
        rf = r->get_features();
    }

    auto ld = lf & F::directory;
    auto rd = rf & F::directory;
    if (ld && !rd) {
        return true;
    } else if (!ld && rd) {
        return false;
    }
    auto l_name = l->entity->get_path().get_own_name();
    auto r_name = r->entity->get_path().get_own_name();
    return l_name < r_name;
}

auto presence_t::get_children() noexcept -> children_t & {
    auto &e_children = entity->children;
    if (children.size() != e_children.size()) {
        children.clear();
        children.reserve(e_children.size());
        for (auto &c : e_children) {
            auto p = const_cast<presence_t *>(c->get_presence(device));
            children.emplace_back(p);
        }
        std::sort(children.begin(), children.end(), compare);
    }
    return children;
}


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

#pragma once

#include "model/misc/proxy.h"
#include "model/device.h"
#include "statistics.h"
#include "syncspirit-export.h"

namespace syncspirit::presentation {

struct entity_t;
using entity_ptr_t = model::intrusive_ptr_t<entity_t>;

struct presence_t;
using presence_ptr_t = model::intrusive_ptr_t<presence_t>;

struct file_entity_t;
struct cluster_file_presence_t;

struct SYNCSPIRIT_API presence_t : model::proxy_t {
    using children_t = std::vector<presence_t *>;

    // clang-format off
    enum features_t: std::uint32_t {
        folder          = 1 << 0,
        file            = 1 << 1,
        directory       = 1 << 2,
        missing         = 1 << 3,
        cluster         = 1 << 4,
        peer            = 1 << 5,
        local           = 1 << 6,
        deleted         = 1 << 7,
        ignored         = 1 << 8,
        symblink        = 1 << 9,
        conflict        = 1 << 10,
    };
    static constexpr std::uint32_t mask = 0xFFFFFFFF;
    // clang-format ON

    presence_t(entity_t *entity, model::device_t* device) noexcept ;
    ~presence_t();

    presence_t *get_parent() noexcept ;
    entity_t* get_entity() noexcept;
    void set_parent(presence_t *value) noexcept ;
    presence_t* set_parent(entity_t *entity) noexcept ;
    std::uint32_t get_features() const noexcept ;
    virtual const presence_stats_t& get_stats(bool sync = true) const noexcept ;
    const presence_stats_t& get_own_stats() const noexcept;
    inline const model::device_t* get_device() const noexcept { return device; }

    virtual const presence_t* determine_best(const presence_t*) const;

    children_t& get_children() noexcept;

    static bool compare(const presence_t *l, const presence_t *r) noexcept;

  protected:
    friend struct entity_t;
    friend struct cluster_file_presence_t;
    friend struct file_entity_t;

    void on_update() noexcept override;
    void on_delete() noexcept override;
    void clear_presense() noexcept;
    void link(augmentable_t* augmentable) noexcept;
    void sync_with_entity() const noexcept;

    entity_t *entity;
    presence_t *parent;
    model::device_t* device;
    children_t children;
    mutable std::uint32_t features = 0;
    mutable std::uint32_t entity_generation = 0;
    mutable presence_stats_t statistics;
    mutable presence_stats_t own_statistics;

};

} // namespace syncspirit::presentation