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

#include "test-utils.h"
#include "access.h"
#include "test_supervisor.h"
#include "diff-builder.h"

#include "model/cluster.h"
#include "hasher/hasher_proxy_actor.h"
#include "hasher/hasher_actor.h"
#include "fs/scan_actor.h"
#include "fs/utils.h"
#include "net/names.h"

using namespace syncspirit;
using namespace syncspirit::test;
using namespace syncspirit::model;
using namespace syncspirit::net;
using namespace syncspirit::fs;
using namespace syncspirit::hasher;

using fs_time_t = std::filesystem::file_time_type;

struct fixture_t {
    using target_ptr_t = r::intrusive_ptr_t<fs::scan_actor_t>;
    using builder_ptr_t = std::unique_ptr<diff_builder_t>;

    fixture_t() noexcept : root_path{unique_path()}, path_guard{root_path} {
        test::init_logging();
        bfs::create_directory(root_path);
    }

    void run() noexcept {
        auto my_id =
            device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
        my_device = device_t::create(my_id, "my-device").value();
        auto peer_id =
            device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
        peer_device = device_t::create(peer_id, "peer-device").value();

        cluster = new cluster_t(my_device, 1);

        cluster->get_devices().put(my_device);
        cluster->get_devices().put(peer_device);

        r::system_context_t ctx;
        sup = ctx.create_supervisor<supervisor_t>().make_presentation(true).timeout(timeout).create_registry().finish();
        sup->cluster = cluster;

        auto folder_id = "1234-5678";

        sup->start();
        sup->do_process();
        builder = std::make_unique<diff_builder_t>(*cluster);
        builder->upsert_folder(folder_id, root_path.string())
            .apply(*sup)
            .share_folder(peer_id.get_sha256(), folder_id)
            .apply(*sup);

        folder = cluster->get_folders().by_id(folder_id);
        folder_info = folder->get_folder_infos().by_device(*my_device);
        files = &folder_info->get_file_infos();
        folder_info_peer = folder->get_folder_infos().by_device(*peer_device);
        files_peer = &folder_info_peer->get_file_infos();

        CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::OPERATIONAL);

        sup->create_actor<hasher_actor_t>().index(1).timeout(timeout).finish();
        auto proxy_addr = sup->create_actor<hasher::hasher_proxy_actor_t>()
                              .timeout(timeout)
                              .hasher_threads(1)
                              .name(net::names::hasher_proxy)
                              .finish()
                              ->get_address();

        sup->do_process();

        auto fs_config = config::fs_config_t{3600, 10, 1024 * 1024, files_scan_iteration_limit};

        target = sup->create_actor<fs::scan_actor_t>()
                     .timeout(timeout)
                     .cluster(cluster)
                     .sequencer(make_sequencer(77))
                     .fs_config(fs_config)
                     .requested_hashes_limit(2ul)
                     .finish();
        sup->do_process();

        main();

        sup->do_process();
        sup->shutdown();
        sup->do_process();

        CHECK(static_cast<r::actor_base_t *>(sup.get())->access<to::state>() == r::state_t::SHUT_DOWN);
    }

    virtual void main() noexcept {}

    std::int64_t files_scan_iteration_limit = 100;
    builder_ptr_t builder;
    r::pt::time_duration timeout = r::pt::millisec{10};
    r::intrusive_ptr_t<supervisor_t> sup;
    cluster_ptr_t cluster;
    device_ptr_t my_device;
    bfs::path root_path;
    path_guard_t path_guard;
    target_ptr_t target;
    model::folder_ptr_t folder;
    model::folder_info_ptr_t folder_info;
    model::folder_info_ptr_t folder_info_peer;
    model::file_infos_map_t *files;
    model::file_infos_map_t *files_peer;
    model::device_ptr_t peer_device;
};

void test_meta_changes() {
    struct F : fixture_t {
        void main() noexcept override {
            sys::error_code ec;

            SECTION("trivial") {
                SECTION("no files") {
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(folder_info->get_file_infos().size() == 0);
                }
#ifndef SYNCSPIRIT_WIN
                SECTION("just 1 subdir, which cannot be read") {
                    auto subdir = root_path / "abc";
                    CHECK(bfs::create_directories(subdir / "def", ec));
                    auto guard = test::path_guard_t(subdir);
                    bfs::permissions(subdir, bfs::perms::none);
                    bfs::permissions(subdir, bfs::perms::owner_read, ec);
                    if (!ec) {
                        builder->scan_start(folder->get_id()).apply(*sup);
                        REQUIRE(folder_info->get_file_infos().size() == 1);
                        auto fi = folder_info->get_file_infos().begin()->item;
                        CHECK(fi->is_dir());
                        CHECK(fi->get_name() == "abc");
                        bfs::permissions(subdir, bfs::perms::all);

                        auto &errs = sup->io_errors;
                        REQUIRE(errs.size() == 1);
                        REQUIRE(errs.at(0).path == (subdir / "def"));
                        REQUIRE(errs.at(0).ec);
                    }
                }
#endif
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            auto modified = 1641828421;
            auto file_name = std::string_view("q.txt");
            proto::FileInfo pr_fi;
            proto::set_name(pr_fi, file_name);
            proto::set_type(pr_fi, proto::FileInfoType::FILE);
            proto::set_sequence(pr_fi, folder_info_peer->get_max_sequence() + 1);
            proto::set_size(pr_fi, 5);
            proto::set_block_size(pr_fi, 5);
            proto::set_modified_s(pr_fi, modified);

            auto &v = proto::get_version(pr_fi);
            auto &counter = proto::add_counters(v);
            proto::set_id(counter, peer_device->device_id().get_uint());
            proto::set_value(counter, 1);

            auto data_1 = as_owned_bytes("12345");
            auto data_1_hash = utils::sha256_digest(data_1).value();

            auto data_2 = as_owned_bytes("67890");
            auto data_2_hash = utils::sha256_digest(data_2).value();

            auto data_3 = as_owned_bytes("abcde");
            auto data_3_hash = utils::sha256_digest(data_3).value();

            auto bi = proto::BlockInfo();
            proto::set_hash(bi, data_1_hash);
            proto::set_size(bi, 5);

            auto bi_2 = proto::BlockInfo();
            proto::set_hash(bi_2, data_2_hash);
            proto::set_size(bi_2, 5);
            proto::set_offset(bi_2, 5);

            auto bi_3 = proto::BlockInfo();
            proto::set_hash(bi_3, data_3_hash);
            proto::set_size(bi_3, 5);
            proto::set_offset(bi_3, 10);

            auto b = block_info_t::create(bi).value();
            auto b2 = block_info_t::create(bi_2).value();
            auto b3 = block_info_t::create(bi_3).value();

            auto &blocks_map = cluster->get_blocks();
            blocks_map.put(b);
            SECTION("a file does not physically exist") {
                auto uuid = sup->sequencer->next_uuid();
                auto file_peer = file_info_t::create(uuid, pr_fi, folder_info_peer).value();
                file_peer->assign_block(b, 0);
                REQUIRE(folder_info_peer->add_strict(file_peer));
                builder->remote_copy(*file_peer).scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name(file_name);
                CHECK(files->size() == 1);
                CHECK(file->is_deleted());
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }
            SECTION("complete file exists") {
                auto uuid = sup->sequencer->next_uuid();
                auto file_peer = file_info_t::create(uuid, pr_fi, folder_info_peer).value();
                file_peer->assign_block(b, 0);
                REQUIRE(folder_info_peer->add_strict(file_peer));

                builder->remote_copy(*file_peer).apply(*sup);
                auto file = files->by_name(file_name);
                auto path = file->get_path();

                SECTION("meta is not changed") {
                    write_file(path, "12345");
                    bfs::last_write_time(path, from_unix(modified));
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(files->size() == 1);
                    CHECK(file->is_locally_available());
                }

                SECTION("meta is changed (modification)") {
                    write_file(path, "12345");
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(files->size() == 1);
                    auto new_file = files->by_name(file_name);
                    REQUIRE(new_file);
                    CHECK(file == new_file);
                    CHECK(new_file->is_locally_available());
                    CHECK(new_file->get_size() == 5);
                    REQUIRE(new_file->get_blocks().size() == 1);
                    CHECK(new_file->get_blocks()[0]->get_size() == 5);
                }

                SECTION("meta is changed (size)") {
                    write_file(path, "123456");
                    bfs::last_write_time(path, from_unix(modified));
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(files->size() == 1);
                    auto new_file = files->by_name(file_name);
                    REQUIRE(new_file);
                    CHECK(file == new_file);
                    CHECK(new_file->is_locally_available());
                    CHECK(new_file->get_size() == 6);
                    REQUIRE(new_file->get_blocks().size() == 1);
                    CHECK(new_file->get_blocks()[0]->get_size() == 6);
                }

                SECTION("meta is changed (content)") {
                    write_file(path, "67890");
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(files->size() == 1);
                    auto new_file = files->by_name(file_name);
                    REQUIRE(new_file);
                    CHECK(file == new_file);
                    CHECK(new_file->is_locally_available());
                    CHECK(new_file->get_size() == 5);
                    REQUIRE(new_file->get_blocks().size() == 1);
                    CHECK(new_file->get_blocks()[0]->get_size() == 5);
                }
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }
            SECTION("incomplete file exists") {
                proto::set_size(pr_fi, 10);
                proto::set_block_size(pr_fi, 5);

                auto uuid = sup->sequencer->next_uuid();
                auto file = file_info_t::create(uuid, pr_fi, folder_info_peer).value();
                file->assign_block(b, 0);
                file->assign_block(b2, 1);
                REQUIRE(folder_info_peer->add_strict(file));

                // auto file = files->by_name(pr_fi.name());
                auto path = file->get_path().string() + ".syncspirit-tmp";
                auto content = "12345\0\0\0\0\0";
                write_file(path, std::string(content, 10));

                SECTION("outdated -> just remove") {
                    auto new_time = modified - 24 * 3600;
                    bfs::last_write_time(path, from_unix(new_time));
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(!file->is_locally_available());
                    CHECK(!file->is_locked());
                    CHECK(!bfs::exists(path));
                }

                SECTION("just 1st block is valid, tmp is kept") {
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(!file->is_locally_available());
                    CHECK(file->is_locally_available(0));
                    CHECK(!file->is_locally_available(1));
                    CHECK(bfs::exists(path));
                }

                SECTION("just 2nd block is valid, tmp is kept") {
                    write_file(path, "2234567890");
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(!file->is_locally_available());
                    CHECK(!file->is_locally_available(0));
                    CHECK(file->is_locally_available(1));
                    CHECK(bfs::exists(path));
                }

                SECTION("source is missing -> tmp is removed") {
                    folder_info_peer->get_file_infos().remove(file);
                    file->unlock();
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(!file->is_locally_available());
                    CHECK(!bfs::exists(path));
                }

                SECTION("corrupted content") {
                    SECTION("1st block") { write_file(path, "223456789z"); }
                    SECTION("2nd block") { write_file(path, "z234567899"); }
                    builder->scan_start(folder->get_id()).apply(*sup);
                    CHECK(!file->is_locally_available(0));
                    CHECK(!file->is_locally_available(1));
                    CHECK(!bfs::exists(path));
                }

#ifndef SYNCSPIRIT_WIN
                SECTION("error on reading -> remove") {
                    bfs::permissions(path, bfs::perms::none);
                    if (!ec) {
                        builder->scan_start(folder->get_id()).apply(*sup);
                        CHECK(!file->is_locally_available());
                        CHECK(!file->is_locked());
                        CHECK(!bfs::exists(path));

                        auto &errs = sup->io_errors;
                        REQUIRE(errs.size() == 0);
                    }
                }
#endif
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("local (previous) file exists") {
                proto::set_size(pr_fi, 15);
                proto::set_block_size(pr_fi, 5);

                proto::set_size(pr_fi, 5);
                auto uuid_1 = sup->sequencer->next_uuid();
                auto file_my = file_info_t::create(uuid_1, pr_fi, folder_info).value();
                file_my->assign_block(b, 0);
                file_my->lock();
                REQUIRE(folder_info->add_strict(file_my));

                proto::set_size(pr_fi, 15);
                proto::set_value(counter, 2);

                auto uuid_2 = sup->sequencer->next_uuid();
                auto file_peer = file_info_t::create(uuid_2, pr_fi, folder_info_peer).value();
                file_peer->assign_block(b, 0);
                file_peer->assign_block(b2, 1);
                file_peer->assign_block(b3, 2);
                REQUIRE(folder_info_peer->add_strict(file_peer));

                auto file = files->by_name(file_name);
                auto path_my = file->get_path().string();
                auto path_peer = file->get_path().string() + ".syncspirit-tmp";
                write_file(path_my, "12345");
                bfs::last_write_time(path_my, from_unix(modified));

                auto content = "1234567890\0\0\0\0\0";
                write_file(path_peer, std::string(content, 15));
                builder->scan_start(folder->get_id()).apply(*sup);

                CHECK(file_my->is_locally_available());
                CHECK(!file_peer->is_locally_available());
                CHECK(file_peer->is_locally_available(0));
                CHECK(file_peer->is_locally_available(1));
                CHECK(!file_peer->is_locally_available(2));
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("local (previous) file changes") {
                proto::set_size(pr_fi, 15);
                proto::set_block_size(pr_fi, 5);
                auto uuid_1 = sup->sequencer->next_uuid();
                auto file_my = file_info_t::create(uuid_1, pr_fi, folder_info).value();
                file_my->assign_block(b, 0);
                file_my->assign_block(b, 1);
                file_my->assign_block(b, 2);
                file_my->lock();
                REQUIRE(folder_info->add_strict(file_my));

                proto::set_value(counter, 2);

                auto file = files->by_name(file_name);
                auto path_my = file->get_path().string();
                write_file(path_my, "12345");
                bfs::last_write_time(path_my, from_unix(modified));

                builder->scan_start(folder->get_id()).apply(*sup);

                CHECK(file_my->is_locally_available());
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }
        }
    };
    F().run();
}

void test_new_files() {
    struct F : fixture_t {
        void main() noexcept override {
            sys::error_code ec;
            auto &blocks = cluster->get_blocks();

#ifndef SYNCSPIRIT_WIN
            SECTION("new symlink") {
                auto file_path = root_path / "symlink";
                bfs::create_symlink(bfs::path("/some/where"), file_path, ec);
                REQUIRE(!ec);
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("symlink");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(!file->is_file());
                CHECK(file->is_link());
                CHECK(file->get_block_size() == 0);
                CHECK(file->get_size() == 0);
                CHECK(blocks.size() == 0);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }
#endif

            SECTION("new dir") {
                auto dir_path = root_path / "some-dir";
                bfs::create_directories(dir_path);
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("some-dir");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(file->is_dir());
                CHECK(!file->is_link());
                CHECK(file->get_block_size() == 0);
                CHECK(file->get_size() == 0);
                CHECK(blocks.size() == 0);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("empty file") {
                CHECK(bfs::create_directories(root_path / "abc"));
                auto file_path = root_path / "abc" / "empty.file";
                write_file(file_path, "");
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("abc/empty.file");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(!file->is_link());
                CHECK(file->is_file());
                CHECK(file->get_block_size() == 0);
                CHECK(file->get_size() == 0);
                CHECK(blocks.size() == 0);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("non-empty file (1 block)") {
                auto file_path = root_path / "file.ext";
                write_file(file_path, "12345");
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("file.ext");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(!file->is_link());
                CHECK(file->is_file());
                CHECK(file->get_block_size() == 5);
                CHECK(file->get_size() == 5);
                CHECK(blocks.size() == 1);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("non-empty file (2 blocks)") {
                auto file_path = root_path / "file.ext";
                auto sz = int64_t{128 * 1024 * 2};
                std::string data(sz, 'x');
                write_file(file_path, data);
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("file.ext");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(!file->is_link());
                CHECK(file->is_file());
                CHECK(file->get_size() == sz);
                CHECK(file->get_blocks().size() == 2);
                CHECK(blocks.size() == 1);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("non-empty file (3 blocks)") {
                auto file_path = root_path / "file.ext";
                auto sz = std::int64_t{128 * 1024 * 3};
                std::string data(sz, 'x');
                write_file(file_path, data);
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file = files->by_name("file.ext");
                REQUIRE(file);
                CHECK(file->is_locally_available());
                CHECK(!file->is_link());
                CHECK(file->is_file());
                CHECK(file->get_size() == sz);
                CHECK(file->get_blocks().size() == 3);
                CHECK(blocks.size() == 1);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("two files, different content") {
                auto file1_path = root_path / "file1.ext";
                write_file(file1_path, "12345");

                auto file2_path = root_path / "file2.ext";
                write_file(file2_path, "67890");
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file1 = files->by_name("file1.ext");
                REQUIRE(file1);
                CHECK(file1->is_locally_available());
                CHECK(!file1->is_link());
                CHECK(file1->is_file());
                CHECK(file1->get_block_size() == 5);
                CHECK(file1->get_size() == 5);

                auto file2 = files->by_name("file2.ext");
                REQUIRE(file2);
                CHECK(file2->is_locally_available());
                CHECK(!file2->is_link());
                CHECK(file2->is_file());
                CHECK(file2->get_block_size() == 5);
                CHECK(file2->get_size() == 5);

                CHECK(blocks.size() == 2);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }

            SECTION("two files, same content") {
                auto file1_path = root_path / "file1.ext";
                write_file(file1_path, "12345");

                auto file2_path = root_path / "file2.ext";
                write_file(file2_path, "12345");
                builder->scan_start(folder->get_id()).apply(*sup);

                auto file1 = files->by_name("file1.ext");
                REQUIRE(file1);
                CHECK(file1->is_locally_available());
                CHECK(!file1->is_link());
                CHECK(file1->is_file());
                CHECK(file1->get_block_size() == 5);
                CHECK(file1->get_size() == 5);

                auto file2 = files->by_name("file2.ext");
                REQUIRE(file2);
                CHECK(file2->is_locally_available());
                CHECK(!file2->is_link());
                CHECK(file2->is_file());
                CHECK(file2->get_block_size() == 5);
                CHECK(file2->get_size() == 5);

                CHECK(blocks.size() == 1);
                REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            }
        }
    };
    F().run();
}

void test_remove_file() {
    struct F : fixture_t {
        void main() noexcept override {
            sys::error_code ec;
            auto &blocks = cluster->get_blocks();

            auto file_path = root_path / "file.ext";
            write_file(file_path, "12345");
            builder->scan_start(folder->get_id()).apply(*sup);
            REQUIRE(!folder->get_scan_finish().is_not_a_date_time());
            REQUIRE(!folder->is_scanning());

            auto file = files->by_name("file.ext");
            REQUIRE(file);
            REQUIRE(blocks.size() == 1);
            REQUIRE(file->get_version()->counters_size() == 1);
            auto counter = file->get_version()->get_best();

            auto prev_finish = folder->get_scan_finish();
            bfs::remove(file_path);

            builder->scan_start(folder->get_id()).apply(*sup);

            file = files->by_name("file.ext");
            CHECK(file->is_deleted() == 1);
            CHECK(file->is_local());
            CHECK(blocks.size() == 0);

            auto v = file->get_version();
            REQUIRE(v->counters_size() == 1);

            auto &c = v->get_best();
            CHECK(proto::get_id(c) == proto::get_id((counter)));
            CHECK(proto::get_value(c) > proto::get_value((counter)));
            REQUIRE(folder->get_scan_finish() >= folder->get_scan_start());
            REQUIRE(folder->get_scan_finish() > prev_finish);
        }
    };
    F().run();
};

void test_synchronization() {
    struct F : fixture_t {
        void main() noexcept override {
            sys::error_code ec;
            auto file_path = root_path / "file.ext";
            write_file(file_path, "12345");
            builder->scan_start(folder->get_id()).synchronization_start(folder->get_id()).apply(*sup);
            REQUIRE(!folder->get_scan_finish().is_not_a_date_time());
            REQUIRE(!folder->is_scanning());
            REQUIRE(files->size() == 0);
        }
    };
    F().run();
};

void test_suspending() {
    struct F : fixture_t {
        F() { files_scan_iteration_limit = 1; }
        void main() noexcept override {
            sys::error_code ec;
            write_file(root_path / "f-1.ext", "123");
            write_file(root_path / "f-2.ext", "456");
            write_file(root_path / "f-3.ext", "789");
            builder->scan_start(folder->get_id()).suspend(*folder).apply(*sup);
            REQUIRE(!folder->get_scan_finish().is_not_a_date_time());
            REQUIRE(!folder->is_scanning());
        }
    };
    F().run();
};

void test_importing() {
    struct F : fixture_t {
        F() { files_scan_iteration_limit = 1; }
        void main() noexcept override {
            auto sha256 = peer_device->device_id().get_sha256();
            auto fi_peer = folder->get_folder_infos().by_device(*peer_device);
            SECTION("non-empty file") {
                auto path = root_path / "a.bin";
                write_file(path, "12345");
                auto status = bfs::status(path);
                auto permissions = static_cast<uint32_t>(status.permissions());

                auto pr_fi = proto::FileInfo();
                proto::set_name(pr_fi, path.filename().string());
                proto::set_type(pr_fi, proto::FileInfoType::FILE);
                proto::set_size(pr_fi, 5);
                proto::set_block_size(pr_fi, 5);
                proto::set_permissions(pr_fi, permissions);

                auto &v = proto::get_version(pr_fi);
                auto &counter = proto::add_counters(v);
                proto::set_id(counter, 1);
                proto::set_value(counter, 1);
                proto::set_sequence(pr_fi, fi_peer->get_max_sequence() + 1);

                auto data_1 = as_owned_bytes("12345");
                auto data_1_h = utils::sha256_digest(data_1).value();
                auto &b1 = proto::add_blocks(pr_fi);
                proto::set_hash(b1, data_1_h);
                proto::set_size(b1, data_1.size());

                builder->make_index(sha256, folder->get_id()).add(pr_fi, peer_device).finish().apply(*sup);

                builder->scan_start(folder->get_id()).apply(*sup);

                auto fi_my = folder->get_folder_infos().by_device(*my_device);
                auto &files_my = fi_my->get_file_infos();
                REQUIRE(files_my.size() == 1);
                auto file = files_my.by_name(path.filename().string());
                REQUIRE(file->get_version()->as_proto() == v);
            }
            SECTION("empty file") {
                auto path = root_path / "b.bin";
                write_file(path, "");
                auto status = bfs::status(path);
                auto permissions = static_cast<uint32_t>(status.permissions());

                auto pr_fi = proto::FileInfo();
                proto::set_name(pr_fi, path.filename().string());
                proto::set_type(pr_fi, proto::FileInfoType::FILE);
                proto::set_permissions(pr_fi, permissions);

                auto &v = proto::get_version(pr_fi);
                auto &counter = proto::add_counters(v);
                proto::set_id(counter, 1);
                proto::set_value(counter, 1);
                proto::set_sequence(pr_fi, fi_peer->get_max_sequence() + 1);

                builder->make_index(sha256, folder->get_id()).add(pr_fi, peer_device).finish().apply(*sup);

                builder->scan_start(folder->get_id()).apply(*sup);

                auto fi_my = folder->get_folder_infos().by_device(*my_device);
                auto &files_my = fi_my->get_file_infos();
                REQUIRE(files_my.size() == 1);
                auto file = files_my.by_name(path.filename().string());
                REQUIRE(file->get_version()->as_proto() == v);
            }
            SECTION("deleted") {
                SECTION("single deleted file") {
                    auto path = root_path / "c.bin";
                    auto pr_fi = proto::FileInfo();
                    proto::set_name(pr_fi, path.filename().string());
                    proto::set_type(pr_fi, proto::FileInfoType::DIRECTORY);
                    proto::set_deleted(pr_fi, true);

                    auto &v = proto::get_version(pr_fi);
                    auto &counter = proto::add_counters(v);
                    proto::set_id(counter, 1);
                    proto::set_value(counter, 1);
                    proto::set_sequence(pr_fi, fi_peer->get_max_sequence() + 1);

                    builder->make_index(sha256, folder->get_id()).add(pr_fi, peer_device).finish().apply(*sup);

                    builder->scan_start(folder->get_id()).apply(*sup);

                    auto fi_my = folder->get_folder_infos().by_device(*my_device);
                    auto &files_my = fi_my->get_file_infos();
                    REQUIRE(files_my.size() == 1);
                    auto file = files_my.by_name(path.filename().string());
                    REQUIRE(file->get_version()->as_proto() == v);
                    CHECK(!bfs::exists(path));
                }
                SECTION("deleted file inside deleted dir") {
                    auto path = root_path / "d" / "file.bin";
                    auto pr_dir = [&]() -> proto::FileInfo {
                        auto f = proto::FileInfo();
                        proto::set_name(f, "d");
                        proto::set_type(f, proto::FileInfoType::DIRECTORY);
                        proto::set_deleted(f, true);
                        auto &v = proto::get_version(f);
                        auto &counter = proto::add_counters(v);
                        proto::set_id(counter, 1);
                        proto::set_value(counter, 1);
                        proto::set_sequence(f, fi_peer->get_max_sequence() + 1);
                        return f;
                    }();
                    auto pr_file = [&]() -> proto::FileInfo {
                        auto f = proto::FileInfo();
                        proto::set_name(f, "d/file.bin");
                        proto::set_type(f, proto::FileInfoType::FILE);
                        proto::set_deleted(f, true);
                        auto &v = proto::get_version(f);
                        auto &counter = proto::add_counters(v);
                        proto::set_id(counter, 1);
                        proto::set_value(counter, 1);
                        proto::set_sequence(f, fi_peer->get_max_sequence() + 2);
                        return f;
                    }();

                    builder->make_index(sha256, folder->get_id())
                        .add(pr_dir, peer_device)
                        .add(pr_file, peer_device)
                        .finish()
                        .apply(*sup);

                    auto fi_my = folder->get_folder_infos().by_device(*my_device);
                    auto &files_my = fi_my->get_file_infos();
                    SECTION("prohibid creation of local deleted file") {
                        SECTION("dir is a file, locally") {
                            write_file(root_path / "d", "");
                            builder->scan_start(folder->get_id()).apply(*sup);
                            bfs::remove(root_path / "d");
                        }
#ifndef SYNCSPIRIT_WIN
                        SECTION("dir does exists, but it is non-readable") {
                            bfs::create_directory(root_path / "d");
                            bfs::permissions(root_path / "d", bfs::perms::none);
                            builder->scan_start(folder->get_id()).apply(*sup);
                            bfs::remove(root_path / "d");
                        }
#endif
                        CHECK(!files_my.by_name("d/file.bin"));
                    }
                    SECTION("allow creation of local deleted file") {
                        SECTION("dir does not exists") {
                            builder->scan_start(folder->get_id()).apply(*sup);
                            auto dir = files_my.by_name("d");
                            CHECK(dir);
                        }
                        SECTION("dir does exists & it is r/w") {
                            bfs::create_directory(root_path / "d");
                            builder->scan_start(folder->get_id()).apply(*sup);
                        }
                        REQUIRE(files_my.size() >= 1);
                        auto file = files_my.by_name("d/file.bin");
                        CHECK(file);
                        CHECK(!bfs::exists(path));
                    }
                }
                SECTION("prohibit creation of deleted record") {
                    auto path = root_path / "e" / "file.bin";
                    auto pr_dir = [&]() -> proto::FileInfo {
                        auto f = proto::FileInfo();
                        proto::set_name(f, "e");
                        proto::set_type(f, proto::FileInfoType::DIRECTORY);
                        proto::set_deleted(f, false);
                        auto &v = proto::get_version(f);
                        auto &counter = proto::add_counters(v);
                        proto::set_id(counter, 1);
                        proto::set_value(counter, 1);
                        proto::set_sequence(f, fi_peer->get_max_sequence() + 1);
                        return f;
                    }();
                    auto pr_file = [&]() -> proto::FileInfo {
                        auto f = proto::FileInfo();
                        proto::set_name(f, "e/file.bin");
                        proto::set_type(f, proto::FileInfoType::FILE);
                        proto::set_deleted(f, true);
                        auto &v = proto::get_version(f);
                        auto &counter = proto::add_counters(v);
                        proto::set_id(counter, 1);
                        proto::set_value(counter, 1);
                        proto::set_sequence(f, fi_peer->get_max_sequence() + 2);
                        return f;
                    }();

                    builder->make_index(sha256, folder->get_id())
                        .add(pr_dir, peer_device)
                        .add(pr_file, peer_device)
                        .finish()
                        .apply(*sup);

                    builder->scan_start(folder->get_id()).apply(*sup);

                    auto fi_my = folder->get_folder_infos().by_device(*my_device);
                    auto &files_my = fi_my->get_file_infos();
                    REQUIRE(files_my.size() == 0);
                }
            }
        }
    };
    F().run();
};

int _init() {
    REGISTER_TEST_CASE(test_meta_changes, "test_meta_changes", "[fs]");
    REGISTER_TEST_CASE(test_new_files, "test_new_files", "[fs]");
    REGISTER_TEST_CASE(test_remove_file, "test_remove_file", "[fs]");
    REGISTER_TEST_CASE(test_synchronization, "test_synchronization", "[fs]");
    REGISTER_TEST_CASE(test_suspending, "test_suspending", "[fs]");
    REGISTER_TEST_CASE(test_importing, "test_importing", "[fs]");
    return 1;
}

static int v = _init();


code:
enable_testing()
include(GenerateExportHeader)

find_package(
    Boost
    COMPONENTS
        system
    REQUIRED
)
find_package(OpenSSL REQUIRED)
find_package(Catch2 REQUIRED)

if(MINGW)
    ENABLE_LANGUAGE(RC)
endif()

add_library(syncspirit_test_lib SHARED
    $<$<PLATFORM_ID:Windows>:win32-resource.rc>
    test_supervisor.cpp
    test-db.cpp
    test-utils.cpp
    diff-builder.cpp
)
target_link_libraries(syncspirit_test_lib syncspirit_core
    Catch2::Catch2WithMain
    $<$<PLATFORM_ID:Windows>:wsock32>
    $<$<PLATFORM_ID:Windows>:ws2_32>
    $<$<PLATFORM_ID:Windows>:iphlpapi>
)
target_include_directories(syncspirit_test_lib PUBLIC
    "${syncspirit_SOURCE_DIR}/src"
    "${CMAKE_CURRENT_BINARY_DIR}/include"
)
generate_export_header(syncspirit_test_lib
    EXPORT_MACRO_NAME SYNCSPIRIT_TEST_API
    EXPORT_FILE_NAME include/syncspirit-test-export.h
)

function(create_test source)
    cmake_path(REMOVE_EXTENSION source LAST_ONLY OUTPUT_VARIABLE target)

    add_executable(${target} ${source} $<$<PLATFORM_ID:Windows>:win32-resource.rc>)
    target_link_libraries(${target} syncspirit_test_lib)
    add_test(
        NAME "${target}-test"
        COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} $<TARGET_FILE_NAME:${target}>
    )
endfunction()

add_custom_command(TARGET syncspirit_test_lib PRE_BUILD
                   COMMAND ${CMAKE_COMMAND} -E copy_directory
                   ${CMAKE_SOURCE_DIR}/tests/data ${CMAKE_BINARY_DIR}/data)

create_test(009-uri.cpp)
create_test(010-upnp-support.cpp)
create_test(011-tls-util.cpp)
create_test(012-base32.cpp)
create_test(013-global-discovery.cpp)
create_test(014-configuration.cpp)
create_test(015-logger.cpp)
create_test(016-relay-support.cpp)
create_test(017-fs-utils.cpp)
create_test(018-dns.cpp)
create_test(020-generic-map.cpp)
create_test(021-orphaned-blocks.cpp)
create_test(022-version.cpp)
create_test(025-device_id.cpp)
create_test(026-file_info.cpp)
create_test(027-cluster.cpp)
create_test(029-diff-generic.cpp)
create_test(030-diff-load-cluster.cpp)
create_test(031-diff-cluster_update.cpp)
create_test(032-diff-modify.cpp)
create_test(033-diffs-trivial.cpp)
create_test(034-diff-local_update.cpp)
create_test(035-diff-update_folder.cpp)
create_test(036-diff-blocks.cpp)
create_test(037-diff-upsert_folder.cpp)
create_test(038-diff-clone_finish-file.cpp)
create_test(039-diff-remove-peer.cpp)
create_test(040-diff-contacts.cpp)
create_test(041-diff-remove-folder.cpp)
create_test(045-diff-advance.cpp)
create_test(050-file_iterator.cpp)
create_test(051-block_iterator.cpp)
create_test(052-file_info.cpp)
create_test(053-scan_task.cpp)
create_test(054-updates_streamer.cpp)
create_test(055-resolver.cpp)
create_test(060-bep.cpp)
create_test(061-presentation.cpp)
create_test(070-db.cpp)
create_test(071-fs_actor.cpp)
create_test(072-global_discovery.cpp)
create_test(073-dialer.cpp)
create_test(074-hasher.cpp)
create_test(075-controller.cpp)
create_test(077-initiator.cpp)
create_test(078-relay.cpp)
create_test(079-peer.cpp)
create_test(080-resolver.cpp)
create_test(085-scan-scheduler.cpp)
create_test(086-scan_actor.cpp)

repo_name: basiliscos/syncspirit

license: mit


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

#pragma once

#include <rotor/actor_base.h>
#include "model/folder.h"

namespace syncspirit::test {
namespace {
namespace to {
struct device {};
struct state {};
struct ignore_delete {};
struct pull_order {};
struct folder_type {};
} // namespace to
} // namespace
} // namespace syncspirit::test

namespace syncspirit::model {

template <> inline auto &folder_t::access<test::to::device>() noexcept { return device; }
template <> inline auto &folder_data_t::access<test::to::ignore_delete>() noexcept { return ignore_delete; }
template <> inline auto &folder_data_t::access<test::to::pull_order>() noexcept { return pull_order; }
template <> inline auto &folder_data_t::access<test::to::folder_type>() noexcept { return folder_type; }

} // namespace syncspirit::model

namespace rotor {

template <> inline auto &actor_base_t::access<syncspirit::test::to::state>() noexcept { return state; }

} // namespace rotor


code:
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>Huawei IGD</friendlyName>
<manufacturer>Huawei</manufacturer>
<manufacturerURL>www.huawei.com</manufacturerURL>
<modelDescription>Huawei EchoLife Series</modelDescription>
<modelName>Huawei EchoLife Series</modelName>
<modelNumber>Huawei ONT</modelNumber>
<modelURL></modelURL>
<serialNumber>48575443736C5D9B</serialNumber>
<UDN>uuid:00e0fc37-2525-2828-2500-E8681986854D</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
<SCPDURL>/L3Fwd.xml</SCPDURL>
<controlURL>/ctrlu/Layer3Forwarding_1</controlURL>
<eventSubURL>/evt/Layer3Forwarding_1</eventSubURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription>Huawei IGD</modelDescription>
<modelName>Huawei IGD</modelName>
<modelNumber>Huawei Model</modelNumber>
<modelURL></modelURL>
<serialNumber>48575443736C5D9B</serialNumber>
<UDN>uuid:00e0fc37-2525-2828-2501-E8681986854D</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/upnp/control/WANCommonIFC1</controlURL>
<eventSubURL>/upnp/control/WANCommonIFC1</eventSubURL>
<SCPDURL>/gateicfgSCPD.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice</friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription>Huawei IGD</modelDescription>
<modelName>Huawei IGD</modelName>
<modelNumber>Huawei Model</modelNumber>
<modelURL></modelURL>
<serialNumber>48575443736C5D9B</serialNumber>
<UDN>uuid:00e0fc37-2525-2828-2502-E8681986854D</UDN>
<UPC></UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/gateconnSCPD.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://192.168.100.1</presentationURL>
</device>
</root>


code:
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring>UPnPError</faultstring>
<detail>
<UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
<errorCode>714</errorCode>
<errorDescription>NoSuchEntryInArray</errorDescription>
</UPnPError>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>


code:
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDCnfh5zKDvOQeJNZY6NA4B3zPPq7qsFimWZImxIy4qBJwevk7LXFgT4
P0AsLwHmloSgBwYFK4EEACKhZANiAAS9hcU/5BjVrxYcRtHc07tNCZ3kaVgN7him
35DfZOHzK9Xs04KolEttSFm4/goc1m/yvFIqD7d5pNgBdYPOjz6DSTBPrU6O+5PF
5L3qnFovwoh/CC+1jmEcnXFwRDYnDuE=
-----END EC PRIVATE KEY-----


code:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring>UPnPError</faultstring>
<detail>
<UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
<errorCode>402</errorCode>
<errorDescription>Invalid Args</errorDescription>
</UPnPError>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>


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

#include "diff-builder.h"
#include "model/messages.h"
#include "model/diff/advance/remote_copy.h"
#include "model/diff/advance/local_update.h"
#include "model/diff/contact/peer_state.h"
#include "model/diff/contact/update_contact.h"
#include "model/diff/local/scan_finish.h"
#include "model/diff/local/scan_request.h"
#include "model/diff/local/scan_start.h"
#include "model/diff/local/synchronization_finish.h"
#include "model/diff/local/synchronization_start.h"
#include "model/diff/modify/add_ignored_device.h"
#include "model/diff/modify/add_pending_device.h"
#include "model/diff/modify/append_block.h"
#include "model/diff/modify/block_ack.h"
#include "model/diff/modify/clone_block.h"
#include "model/diff/modify/finish_file.h"
#include "model/diff/modify/mark_reachable.h"
#include "model/diff/modify/share_folder.h"
#include "model/diff/modify/suspend_folder.h"
#include "model/diff/modify/unshare_folder.h"
#include "model/diff/modify/update_peer.h"
#include "model/diff/modify/remove_folder.h"
#include "model/diff/modify/remove_peer.h"
#include "model/diff/modify/remove_ignored_device.h"
#include "model/diff/modify/remove_pending_device.h"
#include "model/diff/modify/upsert_folder.h"
#include "model/diff/peer/cluster_update.h"
#include "model/diff/peer/update_folder.h"

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

cluster_configurer_t::cluster_configurer_t(diff_builder_t &builder_, utils::bytes_view_t peer_sha256_,
                                           const bfs::path default_location_) noexcept
    : builder{builder_}, peer_sha256{peer_sha256_}, default_location{default_location_} {}

cluster_configurer_t &&cluster_configurer_t::add(utils::bytes_view_t sha256, std::string_view folder_id, uint64_t index,
                                                 int64_t max_sequence, std::string_view url) noexcept {
    proto::Folder folder;
    proto::set_id(folder, folder_id);
    proto::Device device;
    proto::set_id(device, sha256);
    proto::set_index_id(device, index);
    proto::set_max_sequence(device, max_sequence);
    if (!url.empty()) {
        proto::add_addresses(device, url);
    }
    proto::add_devices(folder, std::move(device));
    proto::add_folders(cc, std::move(folder));
    return std::move(*this);
}

cluster_configurer_t &&cluster_configurer_t::add_named(utils::bytes_view_t sha256, std::string_view folder_id,
                                                       uint64_t index, int64_t max_sequence,
                                                       std::string_view folder_name) noexcept {
    proto::Folder folder;
    proto::set_id(folder, folder_id);
    proto::set_label(folder, folder_name);
    proto::Device device;
    proto::set_id(device, sha256);
    proto::set_index_id(device, index);
    proto::set_max_sequence(device, max_sequence);
    proto::add_devices(folder, std::move(device));
    proto::add_folders(cc, std::move(folder));
    return std::move(*this);
}

diff_builder_t &cluster_configurer_t::finish() noexcept {
    auto &cluster = builder.cluster;
    auto peer = builder.cluster.get_devices().by_sha256(peer_sha256);
    auto diff = diff::peer::cluster_update_t::create(default_location, cluster, *builder.sequencer, *peer, cc);
    assert(diff.has_value());
    builder.assign(diff.value().get());
    return builder;
}

std::error_code cluster_configurer_t::fail() noexcept {
    auto &cluster = builder.cluster;
    auto peer = builder.cluster.get_devices().by_sha256(peer_sha256);
    auto opt = diff::peer::cluster_update_t::create(default_location, cluster, *builder.sequencer, *peer, cc);
    return opt.error();
}

index_maker_t::index_maker_t(diff_builder_t &builder_, utils::bytes_view_t sha256, std::string_view folder_id) noexcept
    : builder{builder_}, peer_sha256{sha256} {
    proto::set_folder(index, folder_id);
}

index_maker_t &&index_maker_t::add(const proto::FileInfo &file, const model::device_ptr_t &peer,
                                   bool add_version) noexcept {
    auto f = file;
    auto &v = proto::get_version(f);
    if (add_version && proto::get_counters_size(v) == 0) {
        auto c = proto::Counter(peer->device_id().get_uint(), 1);
        proto::add_counters(v, std::move(c));
    }
    proto::add_files(index, std::move(f));
    return std::move(*this);
}

std::error_code index_maker_t::fail() noexcept {
    auto &cluster = builder.cluster;
    auto peer = builder.cluster.get_devices().by_sha256(peer_sha256);
    auto opt = diff::peer::update_folder_t::create(cluster, *builder.sequencer, *peer, index);
    return opt.error();
}

diff_builder_t &index_maker_t::finish() noexcept {
    auto &cluster = builder.cluster;
    auto peer = builder.cluster.get_devices().by_sha256(peer_sha256);
    auto diff = diff::peer::update_folder_t::create(cluster, *builder.sequencer, *peer, index);
    assert(diff.has_value());
    builder.assign(diff.value().get());
    return builder;
}

diff_builder_t::diff_builder_t(model::cluster_t &cluster_, r::address_ptr_t receiver_) noexcept
    : cluster{cluster_}, receiver{receiver_} {
    sequencer = model::make_sequencer(0);
}

diff_builder_t &diff_builder_t::apply(rotor::supervisor_t &sup) noexcept {
    assert(cluster_diff);
    if (receiver) {
        sup.send<model::payload::model_update_t>(receiver, std::move(cluster_diff), nullptr);
        sup.do_process();
    }
    auto &addr = sup.get_address();
    bool do_try = true;
    while (do_try) {
        do_try = false;
        if (cluster_diff) {
            sup.send<model::payload::model_update_t>(addr, std::move(cluster_diff), nullptr);
            do_try = true;
        }
        sup.do_process();
    }

    return *this;
}

auto diff_builder_t::apply() noexcept -> outcome::result<void> {
    auto r = outcome::result<void>(outcome::success());
    bool do_try = true;
    while (do_try) {
        do_try = false;
        if (r && cluster_diff) {
            r = cluster_diff->apply(cluster, *this);
            cluster_diff.reset();
            do_try = true;
        }
    }
    return r;
}

diff_builder_t &diff_builder_t::then() noexcept {
    auto r = apply();
    if (!r) {
        spdlog::critical("diff application failure: {}", r.error().message());
        std::abort();
    }
    return *this;
}

diff_builder_t &diff_builder_t::upsert_folder(std::string_view id, std::string_view path, std::string_view label,
                                              std::uint64_t index_id) noexcept {
    db::Folder db_folder;
    db::set_id(db_folder, id);
    db::set_label(db_folder, label);
    db::set_path(db_folder, path);
    db::set_folder_type(db_folder, db::FolderType::send_and_receive);
    auto opt = diff::modify::upsert_folder_t::create(cluster, *sequencer, std::move(db_folder), index_id);
    return assign(opt.value().get());
}

diff_builder_t &diff_builder_t::upsert_folder(const db::Folder &data, std::uint64_t index_id) noexcept {
    auto opt = diff::modify::upsert_folder_t::create(cluster, *sequencer, data, index_id);
    return assign(opt.value().get());
}

diff_builder_t &diff_builder_t::update_peer(const model::device_id_t &device, std::string_view name,
                                            std::string_view cert_name, bool auto_accept) noexcept {
    db::Device db_device;
    db::set_name(db_device, name);
    db::set_cert_name(db_device, cert_name);
    db::set_auto_accept(db_device, auto_accept);
    return assign(new diff::modify::update_peer_t(db_device, device, cluster));
}

cluster_configurer_t diff_builder_t::configure_cluster(utils::bytes_view_t sha256,
                                                       const bfs::path &default_location) noexcept {
    return cluster_configurer_t(*this, sha256, default_location);
}

index_maker_t diff_builder_t::make_index(utils::bytes_view_t sha256, std::string_view folder_id) noexcept {
    return index_maker_t(*this, sha256, folder_id);
}

diff_builder_t &diff_builder_t::share_folder(utils::bytes_view_t sha256, std::string_view folder_id,
                                             utils::bytes_view_t introducer_sha256) noexcept {
    auto &devices = cluster.get_devices();
    auto device = devices.by_sha256(sha256);
    auto introcuder = devices.by_sha256(introducer_sha256);
    if (!introcuder) {
        introcuder = cluster.get_device();
    }
    auto folder = cluster.get_folders().by_id(folder_id);
    auto opt = diff::modify::share_folder_t::create(cluster, *sequencer, *device, introcuder->device_id(), *folder);
    if (!opt) {
        spdlog::error("cannot share: {}", opt.assume_error().message());
        return *this;
    }
    return assign(opt.assume_value().get());
}

diff_builder_t &diff_builder_t::unshare_folder(model::folder_info_t &fi) noexcept {
    return assign(new diff::modify::unshare_folder_t(cluster, fi));
}

diff_builder_t &diff_builder_t::remote_copy(const model::file_info_t &source) noexcept {
    auto action = model::advance_action_t::remote_copy;
    auto diff = diff::advance::remote_copy_t::create(action, source, *sequencer);
    return assign(diff.get());
}

diff_builder_t &diff_builder_t::advance(const model::file_info_t &source) noexcept {
    auto action = model::resolve(source);
    auto diff = diff::advance::remote_copy_t::create(action, source, *sequencer);
    return assign(diff.get());
}

diff_builder_t &diff_builder_t::finish_file(const model::file_info_t &file) noexcept {
    return assign(new diff::modify::finish_file_t(file));
}

diff_builder_t &diff_builder_t::local_update(std::string_view folder_id, const proto::FileInfo &file_) noexcept {
    return assign(new diff::advance::local_update_t(cluster, *sequencer, file_, folder_id));
}

diff_builder_t &diff_builder_t::remove_peer(const model::device_t &peer) noexcept {
    return assign(new diff::modify::remove_peer_t(cluster, peer));
    return *this;
}

diff_builder_t &diff_builder_t::remove_folder(const model::folder_t &folder) noexcept {
    return assign(new diff::modify::remove_folder_t(cluster, *sequencer, folder));
    return *this;
}

diff_builder_t &diff_builder_t::update_state(const model::device_t &peer, const r::address_ptr_t &peer_addr,
                                             model::device_state_t state, std::string_view connection_id) noexcept {
    assert(!(state == model::device_state_t::online && connection_id.empty()));
    auto diff = model::diff::contact::peer_state_t::create(cluster, peer.device_id().get_sha256(), peer_addr, state,
                                                           std::string(connection_id));
    return assign(diff.get());
}

diff_builder_t &diff_builder_t::update_contact(const model::device_id_t &device,
                                               const utils::uri_container_t &uris) noexcept {
    return assign(new model::diff::contact::update_contact_t(cluster, device, uris));
}

diff_builder_t &diff_builder_t::append_block(const model::file_info_t &target, size_t block_index,
                                             utils::bytes_t data) noexcept {
    return assign(new diff::modify::append_block_t(target, block_index, std::move(data)));
}

diff_builder_t &diff_builder_t::clone_block(const model::file_block_t &file_block) noexcept {
    return assign(new diff::modify::clone_block_t(file_block));
}

diff_builder_t &diff_builder_t::ack_block(const model::diff::modify::block_transaction_t &diff) noexcept {
    return assign(new diff::modify::block_ack_t(diff));
    return *this;
}

diff_builder_t &diff_builder_t::add_ignored_device(const model::device_id_t &device,
                                                   db::SomeDevice db_device) noexcept {
    return assign(new diff::modify::add_ignored_device_t(cluster, device, db_device));
}

diff_builder_t &diff_builder_t::add_unknown_device(const model::device_id_t &device,
                                                   db::SomeDevice db_device) noexcept {
    return assign(new diff::modify::add_pending_device_t(device, db_device));
}

diff_builder_t &diff_builder_t::remove_ignored_device(const model::ignored_device_t &device) noexcept {
    return assign(new diff::modify::remove_ignored_device_t(device));
}

diff_builder_t &diff_builder_t::remove_unknown_device(const model::pending_device_t &device) noexcept {
    return assign(new diff::modify::remove_pending_device_t(device));
}

diff_builder_t &diff_builder_t::scan_start(std::string_view id, const r::pt::ptime &at) noexcept {
    auto final_at = at.is_not_a_date_time() ? r::pt::microsec_clock::local_time() : at;
    return assign(new model::diff::local::scan_start_t(std::string(id), final_at));
}

diff_builder_t &diff_builder_t::scan_finish(std::string_view id, const r::pt::ptime &at) noexcept {
    auto final_at = at.is_not_a_date_time() ? r::pt::microsec_clock::local_time() : at;
    return assign(new model::diff::local::scan_finish_t(std::string(id), final_at));
}

diff_builder_t &diff_builder_t::scan_request(std::string_view id) noexcept {
    return assign(new model::diff::local::scan_request_t(std::string(id)));
}

diff_builder_t &diff_builder_t::synchronization_start(std::string_view id) noexcept {
    return assign(new model::diff::local::synchronization_start_t(std::string(id)));
}

diff_builder_t &diff_builder_t::synchronization_finish(std::string_view id) noexcept {
    return assign(new model::diff::local::synchronization_finish_t(std::string(id)));
}

diff_builder_t &diff_builder_t::mark_reacheable(model::file_info_ptr_t peer_file, bool value) noexcept {
    return assign(new model::diff::modify::mark_reachable_t(*peer_file, value));
}

diff_builder_t &diff_builder_t::suspend(const model::folder_t &folder) noexcept {
    return assign(new model::diff::modify::suspend_folder_t(folder));
}

template <typename Holder, typename Diff> static void generic_assign(Holder *holder, Diff *diff) noexcept {
    if (!(*holder)) {
        holder->reset(diff);
    } else {
        auto h = *holder;
        while (h && h->sibling) {
            h = h->sibling;
        }
        h->assign_sibling(diff);
    }
}

diff_builder_t &diff_builder_t::assign(model::diff::cluster_diff_t *diff) noexcept {
    generic_assign(&cluster_diff, diff);
    return *this;
}

auto diff_builder_t::get_sequencer() noexcept -> model::sequencer_t & { return *sequencer; }


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

#pragma once

#include <filesystem>
#include <boost/outcome.hpp>
#include <rotor/supervisor.h>

#include "syncspirit-test-export.h"
#include "model/device.h"
#include "model/ignored_device.h"
#include "model/pending_device.h"
#include "model/file_info.h"
#include "model/diff/apply_controller.h"
#include "model/diff/cluster_diff.h"
#include "model/diff/block_diff.h"
#include "model/diff/modify/block_transaction.h"
#include "model/misc/sequencer.h"

namespace syncspirit::test {

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

struct diff_builder_t;

struct SYNCSPIRIT_TEST_API cluster_configurer_t {
    cluster_configurer_t(diff_builder_t &builder, utils::bytes_view_t peer_sha256,
                         const bfs::path default_location = {}) noexcept;
    cluster_configurer_t &&add(utils::bytes_view_t sha256, std::string_view folder_id, uint64_t index,
                               int64_t max_sequence, std::string_view url = {}) noexcept;
    cluster_configurer_t &&add_named(utils::bytes_view_t sha256, std::string_view folder_id, uint64_t index,
                                     int64_t max_sequence, std::string_view folder_name) noexcept;
    diff_builder_t &finish() noexcept;
    std::error_code fail() noexcept;

  private:
    proto::ClusterConfig cc;
    diff_builder_t &builder;
    utils::bytes_view_t peer_sha256;
    bfs::path default_location;
};

struct SYNCSPIRIT_TEST_API index_maker_t {
    index_maker_t(diff_builder_t &builder, utils::bytes_view_t peer_sha256, std::string_view folder_id) noexcept;
    index_maker_t &&add(const proto::FileInfo &, const model::device_ptr_t &peer, bool add_version = true) noexcept;
    diff_builder_t &finish() noexcept;
    std::error_code fail() noexcept;

  private:
    proto::Index index;
    diff_builder_t &builder;
    utils::bytes_view_t peer_sha256;
};

struct SYNCSPIRIT_TEST_API diff_builder_t : private model::diff::apply_controller_t {
    using blocks_t = std::vector<proto::BlockInfo>;
    using apply_controller_t::apply;

    diff_builder_t(model::cluster_t &, r::address_ptr_t receiver = {}) noexcept;
    cluster_configurer_t configure_cluster(utils::bytes_view_t sha256, const bfs::path &default_location = {}) noexcept;
    diff_builder_t &apply(r::supervisor_t &sup) noexcept;
    outcome::result<void> apply() noexcept;
    diff_builder_t &then() noexcept;
    index_maker_t make_index(utils::bytes_view_t sha256, std::string_view folder_id) noexcept;

    diff_builder_t &upsert_folder(std::string_view id, std::string_view path, std::string_view label = "",
                                  std::uint64_t index_id = 0) noexcept;
    diff_builder_t &upsert_folder(const db::Folder &data, std::uint64_t index_id = 0) noexcept;
    diff_builder_t &update_peer(const model::device_id_t &device, std::string_view name = "",
                                std::string_view cert_name = "", bool auto_accept = false) noexcept;
    diff_builder_t &share_folder(utils::bytes_view_t sha256, std::string_view folder_id,
                                 utils::bytes_view_t introducer_sha256 = {}) noexcept;
    diff_builder_t &unshare_folder(model::folder_info_t &fi) noexcept;
    diff_builder_t &remote_copy(const model::file_info_t &source) noexcept;
    diff_builder_t &advance(const model::file_info_t &source) noexcept;
    diff_builder_t &finish_file(const model::file_info_t &file) noexcept;
    diff_builder_t &local_update(std::string_view folder_id, const proto::FileInfo &file_) noexcept;
    diff_builder_t &append_block(const model::file_info_t &target, size_t block_index, utils::bytes_t data) noexcept;
    diff_builder_t &clone_block(const model::file_block_t &) noexcept;
    diff_builder_t &ack_block(const model::diff::modify::block_transaction_t &) noexcept;
    diff_builder_t &remove_folder(const model::folder_t &folder) noexcept;
    diff_builder_t &remove_peer(const model::device_t &peer) noexcept;
    diff_builder_t &update_state(const model::device_t &peer, const r::address_ptr_t &peer_addr,
                                 model::device_state_t state, std::string_view connection_id = {}) noexcept;
    diff_builder_t &update_contact(const model::device_id_t &device, const utils::uri_container_t &uris) noexcept;
    diff_builder_t &add_ignored_device(const model::device_id_t &device, db::SomeDevice db_device) noexcept;
    diff_builder_t &add_unknown_device(const model::device_id_t &device, db::SomeDevice db_device) noexcept;
    diff_builder_t &remove_ignored_device(const model::ignored_device_t &device) noexcept;
    diff_builder_t &remove_unknown_device(const model::pending_device_t &device) noexcept;
    diff_builder_t &scan_start(std::string_view id, const r::pt::ptime & = {}) noexcept;
    diff_builder_t &scan_finish(std::string_view id, const r::pt::ptime & = {}) noexcept;
    diff_builder_t &scan_request(std::string_view id) noexcept;
    diff_builder_t &synchronization_start(std::string_view id) noexcept;
    diff_builder_t &synchronization_finish(std::string_view id) noexcept;
    diff_builder_t &mark_reacheable(model::file_info_ptr_t peer_file, bool value) noexcept;
    diff_builder_t &suspend(const model::folder_t &folder) noexcept;

    model::sequencer_t &get_sequencer() noexcept;

    diff_builder_t &assign(model::diff::cluster_diff_t *) noexcept;

  private:
    model::sequencer_ptr_t sequencer;
    model::cluster_t &cluster;
    model::diff::cluster_diff_ptr_t cluster_diff;
    r::address_ptr_t receiver;

    friend struct cluster_configurer_t;
    friend struct index_maker_t;
};

} // namespace syncspirit::test


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

#include "fixture.h"
#include "test-utils.h"
#include "sample_db.h"
#include "net/names.h"
#include "access.h"
#include "hasher/hasher_actor.h"
#include "hasher/hasher_proxy_actor.h"
#include "catch.hpp"

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

Fixture::Fixture() { test::init_logging() }

void Fixture::run() {
    root_path = bfs::unique_path();
    bfs::create_directory(root_path);
    auto root_path_guard = path_guard_t(root_path);

    std::uint64_t key = 0;
    db::Device db_my;
    db_my.set_id(test::device_id2sha256("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD"));
    db_my.set_name("d1");
    db_my.set_cert_name("d1_cert_name");
    device_my = model::device_ptr_t(new model::device_t(db_my, ++key));

    db::Device db_peer;
    db_peer.set_id(test::device_id2sha256("KUEQE66-JJ7P6AD-BEHD4ZW-GPBNW6Q-Y4C3K4Y-X44WJWZ-DVPIDXS-UDRJMA7"));
    db_peer.set_name("d2");
    db_peer.set_cert_name("d2_cert_name");
    device_peer = model::device_ptr_t(new model::device_t(db_peer, ++key));

    cluster = new cluster_t(device_my);
    setup();

    r::system_context_t ctx;
    auto timeout = r::pt::milliseconds{10};
    sup = ctx.create_supervisor<supervisor_t>().timeout(timeout).create_registry().finish();
    sup->start();
    sup->create_actor<fs::file_actor_t>().timeout(timeout).finish();
    sup->create_actor<hasher::hasher_actor_t>().index(1).timeout(timeout).finish();
    sup->create_actor<hasher::hasher_proxy_actor_t>()
        .hasher_threads(1)
        .name(names::hasher_proxy)
        .timeout(timeout)
        .finish();
    sup->create_actor<sample_db_t>().timeout(timeout).finish();
    peer = sup->create_actor<sample_peer_t>().timeout(timeout).finish();

    pre_run();
    sup->do_process();

    create_controller();

    main();

    sup->shutdown();
    sup->do_process();
    CHECK(controller->access<to::state>() == r::state_t::SHUT_DOWN);
    CHECK(peer->access<to::state>() == r::state_t::SHUT_DOWN);
}

void Fixture::create_controller() {
    auto bep_config = config::bep_config_t();
    bep_config.rx_buff_size = 1024;
    auto timeout = r::pt::milliseconds{10};
    controller = sup->create_actor<controller_actor_t>()
                     .cluster(cluster)
                     .device(device_my)
                     .peer_addr(peer->get_address())
                     .peer(device_peer)
                     .request_timeout(timeout)
                     .peer_cluster_config(std::move(peer_cluster_config))
                     .ignored_folders(&ignored_folders)
                     .bep_config(bep_config)
                     .timeout(timeout)
                     .finish();
    sup->do_process();
    auto reason = controller->get_shutdown_reason();
    if (reason) {
        spdlog::warn("shutdown reason = {}", reason->message());
    };
    REQUIRE(!reason);
}

void Fixture::setup() {}

void Fixture::pre_run() {}
void Fixture::main() {}


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

#pragma once

#include "model/device.h"
#include "model/cluster.h"
#include "net/controller_actor.h"
#include "fs/file_actor.h"
#include "sample_peer.h"
#include "test_supervisor.h"

namespace syncspirit::test {

namespace bfs = std::filesystem;
namespace payload = syncspirit::net::payload;

struct Fixture {
    model::device_ptr_t device_my;
    model::device_ptr_t device_peer;
    model::cluster_ptr_t cluster;
    model::ignored_folders_map_t ignored_folders;
    r::intrusive_ptr_t<sample_peer_t> peer;
    r::intrusive_ptr_t<supervisor_t> sup;
    r::intrusive_ptr_t<net::controller_actor_t> controller;
    payload::cluster_config_ptr_t peer_cluster_config;
    bfs::path root_path;

    Fixture();
    virtual void setup();
    virtual void pre_run();
    virtual void main();
    void create_controller();

    void run();
    uint64_t seq = 1;
};

}; // namespace syncspirit::test


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

#include "test-db.h"
#include "test-utils.h"

namespace syncspirit::test {

env_t::~env_t() {
    if (env) {
        mdbx_env_close(env);
    }
    // std::cout << path.c_str() << "\n";
    bfs::remove_all(path);
}

env_t mk_env() {
    auto path = unique_path();
    MDBX_env *env;
    auto r = mdbx_env_create(&env);
    assert(r == MDBX_SUCCESS);
    (void)r;
    MDBX_env_flags_t flags =
        MDBX_EXCLUSIVE | MDBX_SAFE_NOSYNC | MDBX_WRITEMAP | MDBX_NOSTICKYTHREADS | MDBX_LIFORECLAIM;
    r = mdbx_env_open(env, path.string().c_str(), flags, 0664);
    assert(r == MDBX_SUCCESS);
    // std::cout << path.c_str() << "\n";
    return env_t{env, std::move(path)};
}

db::transaction_t mk_txn(env_t &env, db::transaction_type_t type) {
    auto r = db::make_transaction(type, env.env);
    assert((bool)r);
    return std::move(r.value());
}

} // namespace syncspirit::test


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

#pragma once

#include "db/utils.h"
#include <filesystem>

namespace syncspirit::test {

namespace bfs = std::filesystem;
namespace db = syncspirit::db;

struct env_t {
    MDBX_env *env;
    bfs::path path;
    ~env_t();
};

env_t mk_env();

db::transaction_t mk_txn(env_t &env, db::transaction_type_t type);

} // namespace syncspirit::test


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

#include "test-utils.h"
#include "model/device_id.h"
#include "utils/base32.h"
#include "utils/log-setup.h"
#include <random>
#include <cstdint>
#include <spdlog/sinks/stdout_color_sinks.h>

int main(int argc, char *argv[]) { return Catch::Session().run(argc, argv); }

namespace syncspirit::test {

bfs::path locate_path(const char *test_file) {
    auto path = bfs::path(test_file);
    if (bfs::exists(path)) {
        return path;
    }
    path = bfs::path("../") / path;
    if (bfs::exists(path)) {
        return path;
    }
    std::string err = "path not found: ";
    err += test_file;
    throw std::runtime_error(err);
}

std::string read_file(const bfs::path &path) {
    sys::error_code ec;
    auto file_path = path.string();
    auto file_path_c = file_path.c_str();
    auto in = fopen(file_path_c, "rb");
    if (!in) {
        auto ec = sys::error_code{errno, sys::generic_category()};
        std::cout << "can't open " << file_path_c << " : " << ec.message() << "\n";
        return "";
    }

    fseek(in, 0L, SEEK_END);
    auto filesize = ftell(in);
    fseek(in, 0L, SEEK_SET);
    std::vector<char> buffer(filesize, 0);
    auto r = fread(buffer.data(), filesize, 1, in);
    assert(r == 1);
    (void)r;
    fclose(in);
    return std::string(buffer.data(), filesize);
}

void write_file(const bfs::path &path, std::string_view content) {
    bfs::create_directories(path.parent_path());
    auto file_path = path.string();
    auto out = fopen(file_path.c_str(), "wb");
    if (!out) {
        auto ec = sys::error_code{errno, sys::generic_category()};
        std::cout << "can't open " << file_path << " : " << ec.message() << "\n";
        std::abort();
    }
    if (content.size()) {
        auto r = fwrite(content.data(), content.size(), 1, out);
        assert(r);
        (void)r;
    }
    fclose(out);
}

utils::bytes_t device_id2sha256(std::string_view device_id_) {
    auto device_id = model::device_id_t::from_string(device_id_).value();
    auto sha256 = device_id.get_sha256();
    return {sha256.begin(), sha256.end()};
}

model::device_ptr_t make_device(std::string_view device_id, std::string_view name) {
    auto id = model::device_id_t::from_string(device_id).value();
    return model::device_t::create(id, name).assume_value();
}

std::string hash_string(const std::string_view &hash) noexcept {
    auto r = std::string();
    r.reserve(hash.size() * 2);
    for (size_t i = 0; i < hash.size(); ++i) {
        r += fmt::format("{:02x}", (unsigned char)hash[i]);
    }
    return r;
}

static model::diff::apply_controller_t apply_controller;

model::diff::apply_controller_t &get_apply_controller() { return apply_controller; }

void init_logging() {
    auto dist_sink = utils::create_root_logger();
    auto console_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
    dist_sink->add_sink(console_sink);
}

static std::random_device rd;
static std::uniform_int_distribution<std::uint64_t> dist;

bfs::path unique_path() {
    auto n = dist(rd);
    auto view = utils::bytes_view_t(reinterpret_cast<const unsigned char *>(&n), sizeof(n));
    auto random_name = utils::base32::encode(view);
    std::transform(random_name.begin(), random_name.end(), random_name.begin(),
                   [](unsigned char c) { return std::tolower(c); });
    auto name = std::string("tmp-") + random_name;
    return bfs::path(name);
}

utils::bytes_view_t as_bytes(std::string_view str) {
    auto ptr = (const unsigned char *)str.data();
    return {ptr, str.size()};
}

utils::bytes_t as_owned_bytes(std::string_view str) {
    auto ptr = (const unsigned char *)str.data();
    return {ptr, ptr + str.size()};
}

bool has_ipv6() noexcept {
    namespace ip = boost::asio::ip;
    namespace sys = boost::system;
    auto ec = sys::error_code();
    ip::make_address_v6("1:2:3::4", ec);
    return !ec;
}

} // namespace syncspirit::test


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

#pragma once

#include <catch2/catch_all.hpp>
#include <filesystem>

#include "model/device.h"
#include "model/diff/apply_controller.h"
#include "model/diff/cluster_diff.h"
#include "proto/proto-helpers.h"
#include "syncspirit-test-export.h"

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#define SYNCSPIRIT_WIN
#endif

namespace syncspirit::test {

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

struct SYNCSPIRIT_TEST_API path_guard_t {
    bfs::path &path;
    path_guard_t(bfs::path &path_) : path{path_} {}
    ~path_guard_t() {
        if (!getenv("SYNCSPIRIT_TEST_KEEP_PATH")) {
            sys::error_code ec;

            if (bfs::exists(path, ec)) {
                bfs::permissions(path, bfs::perms::owner_all, ec);
                if (ec) {
                    printf("error setting permissions : %s: %s\n", path.string().c_str(), ec.message().c_str());
                }
            }

            bfs::remove_all(path, ec);
            if (ec) {
                printf("error removing %s : %s\n", path.string().c_str(), ec.message().c_str());
            }
        }
    }
};

SYNCSPIRIT_TEST_API utils::bytes_view_t as_bytes(std::string_view);
SYNCSPIRIT_TEST_API utils::bytes_t as_owned_bytes(std::string_view);
SYNCSPIRIT_TEST_API bfs::path unique_path();
SYNCSPIRIT_TEST_API model::diff::apply_controller_t &get_apply_controller();
SYNCSPIRIT_TEST_API void init_logging();
SYNCSPIRIT_TEST_API bfs::path locate_path(const char *test_file);
SYNCSPIRIT_TEST_API std::string read_file(const bfs::path &path);
SYNCSPIRIT_TEST_API void write_file(const bfs::path &path, std::string_view content);
SYNCSPIRIT_TEST_API utils::bytes_t device_id2sha256(std::string_view device_id);
SYNCSPIRIT_TEST_API model::device_ptr_t make_device(std::string_view device_id, std::string_view name = "");
SYNCSPIRIT_TEST_API std::string hash_string(const std::string_view &hash) noexcept;
SYNCSPIRIT_TEST_API bool has_ipv6() noexcept;

} // namespace syncspirit::test


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

#include "test_supervisor.h"
#include "model/diff/modify/clone_block.h"
#include "model/diff/modify/append_block.h"
#include "model/diff/modify/finish_file.h"
#include "model/diff/modify/upsert_folder.h"
#include "model/diff/modify/upsert_folder_info.h"
#include "model/diff/advance/remote_copy.h"
#include "model/diff/peer/update_folder.h"
#include "presentation/folder_entity.h"
#include "proto/proto-helpers-bep.h"
#include "proto/proto-helpers-db.h"
#include "net/names.h"

namespace to {
struct queue {};
struct on_timer_trigger {};
} // namespace to

template <> inline auto &rotor::supervisor_t::access<to::queue>() noexcept { return queue; }

namespace rotor {

template <>
inline auto rotor::actor_base_t::access<to::on_timer_trigger, request_id_t, bool>(request_id_t request_id,
                                                                                bool cancelled) noexcept {
    on_timer_trigger(request_id, cancelled);
}

} // namespace rotor

using namespace syncspirit::net;
using namespace syncspirit::test;
using namespace syncspirit::presentation;

supervisor_t::supervisor_t(config_t &cfg) : parent_t(cfg) {
    auto_finish = cfg.auto_finish;
    auto_ack_blocks = cfg.auto_ack_blocks;
    make_presentation = cfg.make_presentation;
    sequencer = model::make_sequencer(1234);
}

void supervisor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    parent_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity(std::string(names::coordinator) + ".test", false);
        log = utils::get_logger(identity);
        sink = p.create_address();
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.register_name(names::coordinator, get_address());
        p.register_name(names::sink, get_address());
    });
    plugin.with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
        p.subscribe_actor(&supervisor_t::on_model_update);
        p.subscribe_actor(&supervisor_t::on_model_sink, sink);
    });
    if (configure_callback) {
        configure_callback(plugin);
    }
}

void supervisor_t::do_start_timer(const r::pt::time_duration &, r::timer_handler_base_t &handler) noexcept {
    timers.emplace_back(&handler);
}

void supervisor_t::do_cancel_timer(r::request_id_t timer_id) noexcept {
    auto it = timers.begin();
    while (it != timers.end()) {
        auto &handler = *it;
        if (handler->request_id == timer_id) {
            auto &actor_ptr = handler->owner;
            actor_ptr->access<to::on_timer_trigger, r::request_id_t, bool>(timer_id, true);
            on_timer_trigger(timer_id, true);
            timers.erase(it);
            return;
        } else {
            ++it;
        }
    }
    assert(0 && "should not happen");
}

void supervisor_t::do_invoke_timer(r::request_id_t timer_id) noexcept {
    LOG_DEBUG(log, "{}, invoking timer {}", identity, timer_id);
    auto predicate = [&](auto &handler) { return handler->request_id == timer_id; };
    auto it = std::find_if(timers.begin(), timers.end(), predicate);
    assert(it != timers.end());
    auto &handler = *it;
    auto &actor_ptr = handler->owner;
    actor_ptr->access<to::on_timer_trigger, r::request_id_t, bool>(timer_id, false);
    timers.erase(it);
}

void supervisor_t::start() noexcept {}
void supervisor_t::shutdown() noexcept { do_shutdown(); }

void supervisor_t::enqueue(r::message_ptr_t message) noexcept {
    locality_leader->access<to::queue>().emplace_back(std::move(message));
}

void supervisor_t::on_model_update(model::message::model_update_t &msg) noexcept {
    LOG_TRACE(log, "{}, updating model", identity);
    auto &diff = msg.payload.diff;
    auto r = diff->apply(*cluster, *this);
    if (!r) {
        LOG_ERROR(log, "{}, error updating model: {}", identity, r.assume_error().message());
        do_shutdown(make_error(r.assume_error()));
    }

    r = diff->visit(*this, nullptr);
    if (!r) {
        LOG_ERROR(log, "{}, error visiting model: {}", identity, r.assume_error().message());
        do_shutdown(make_error(r.assume_error()));
    }
}

void supervisor_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>(get_address(), std::move(diff));
    }
}

auto supervisor_t::consume_errors() noexcept -> io_errors_t { return std::move(io_errors); }

auto supervisor_t::operator()(const model::diff::local::io_failure_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto &errs = diff.errors;
    std::copy(errs.begin(), errs.end(), std::back_inserter(io_errors));
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::modify::finish_file_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (auto_finish) {
        auto folder = cluster->get_folders().by_id(diff.folder_id);
        auto file_info = folder->get_folder_infos().by_device_id(diff.peer_id);
        auto file = file_info->get_file_infos().by_name(diff.file_name);
        auto ack = model::diff::advance::advance_t::create(diff.action, *file, *sequencer);
        send<model::payload::model_update_t>(get_address(), std::move(ack), this);
    }
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::modify::append_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto ack_diff = diff.ack();
    if (auto_ack_blocks) {
        send<model::payload::model_update_t>(address, diff.ack(), this);
    } else {
        if (delayed_ack_holder) {
            delayed_ack_current = delayed_ack_current->assign_sibling(ack_diff.get());
        } else {
            delayed_ack_holder = ack_diff;
            delayed_ack_current = ack_diff.get();
        }
    }
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::modify::clone_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (auto_ack_blocks) {
        send<model::payload::model_update_t>(address, diff.ack(), this);
    }
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::modify::upsert_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (make_presentation) {
        auto folder_id = db::get_id(diff.db);
        auto folder = cluster->get_folders().by_id(folder_id);
        if (!folder->get_augmentation()) {
            auto folder_entity = folder_entity_ptr_t(new folder_entity_t(folder));
            folder->set_augmentation(folder_entity);
        }
    }
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::modify::upsert_folder_info_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto r = diff.visit_next(*this, custom);
    if (make_presentation) {
        auto &folder = *cluster->get_folders().by_id(diff.folder_id);
        auto &device = *cluster->get_devices().by_sha256(diff.device_id);
        auto folder_info = folder.is_shared_with(device);
        if (&device != cluster->get_device()) {
            auto augmentation = folder.get_augmentation().get();
            auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
            folder_entity->on_insert(*folder_info);
        }
    }
    return r;
}

auto supervisor_t::operator()(const model::diff::advance::advance_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (make_presentation) {
        auto folder = cluster->get_folders().by_id(diff.folder_id);
        auto augmentation = folder->get_augmentation().get();
        auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
        if (folder_entity) {
            auto &folder_infos = folder->get_folder_infos();
            auto local_fi = folder_infos.by_device(*cluster->get_device());
            auto file_name = proto::get_name(diff.proto_local);
            auto local_file = local_fi->get_file_infos().by_name(file_name);
            if (local_file) {
                folder_entity->on_insert(*local_file);
            }
        }
    }
    return diff.visit_next(*this, custom);
}

auto supervisor_t::operator()(const model::diff::peer::update_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    if (make_presentation) {
        auto folder = cluster->get_folders().by_id(diff.folder_id);
        auto folder_aug = folder->get_augmentation().get();
        auto folder_entity = static_cast<presentation::folder_entity_t *>(folder_aug);

        auto &devices_map = cluster->get_devices();
        auto peer = devices_map.by_sha256(diff.peer_id);
        auto &files_map = folder->get_folder_infos().by_device(*peer)->get_file_infos();

        for (auto &file : diff.files) {
            auto file_name = proto::get_name(file);
            auto file_info = files_map.by_name(file_name);
            auto augmentation = file_info->get_augmentation().get();
            if (!augmentation) {
                folder_entity->on_insert(*file_info);
            }
        }
    }
    return diff.visit_next(*this, custom);
}


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

#pragma once

#include "rotor/supervisor.h"
#include "model/messages.h"
#include "model/diff/apply_controller.h"
#include "model/diff/cluster_visitor.h"
#include "model/diff/local/io_failure.h"
#include "model/misc/sequencer.h"
#include "utils/log.h"
#include "syncspirit-test-export.h"

namespace syncspirit::test {

namespace r = rotor;
namespace outcome = boost::outcome_v2;

struct supervisor_config_t : r::supervisor_config_t {
    using parent_t = r::supervisor_config_t;
    using parent_t::parent_t;
    bool auto_finish = true;
    bool auto_ack_blocks = true;
    bool make_presentation = false;
};

template <typename Supervisor> struct supervisor_config_builder_t : r::supervisor_config_builder_t<Supervisor> {
    /** \brief final builder class */
    using builder_t = typename Supervisor::template config_builder_t<Supervisor>;

    /** \brief parent config builder */
    using parent_t = r::supervisor_config_builder_t<Supervisor>;

    using parent_t::parent_t;

    /** \brief defines actor's startup policy */
    builder_t &&auto_finish(bool value) && noexcept {
        parent_t::config.auto_finish = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

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

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

struct SYNCSPIRIT_TEST_API supervisor_t : r::supervisor_t,
                                          private model::diff::cluster_visitor_t,
                                          private model::diff::apply_controller_t {
    using config_t = supervisor_config_t;
    template <typename Actor> using config_builder_t = supervisor_config_builder_t<Actor>;

    using timers_t = std::list<r::timer_handler_base_t *>;
    using parent_t = r::supervisor_t;
    using io_errors_t = model::diff::local::io_errors_t;
    using configure_callback_t = std::function<void(r::plugin::plugin_base_t &)>;

    supervisor_t(config_t &cfg);
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void start() noexcept override;
    void shutdown() noexcept override;
    void enqueue(r::message_ptr_t message) noexcept override;

    void on_model_update(model::message::model_update_t &) noexcept;
    void on_model_sink(model::message::model_update_t &) noexcept;
    void do_start_timer(const r::pt::time_duration &interval, r::timer_handler_base_t &handler) noexcept override;
    void do_invoke_timer(r::request_id_t timer_id) noexcept;
    void do_cancel_timer(r::request_id_t timer_id) noexcept override;
    io_errors_t consume_errors() noexcept;

    outcome::result<void> operator()(const model::diff::local::io_failure_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::finish_file_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::append_block_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::clone_block_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_info_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::advance::advance_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::peer::update_folder_t &, void *) noexcept override;

    utils::logger_t log;
    model::cluster_ptr_t cluster;
    model::sequencer_ptr_t sequencer;
    configure_callback_t configure_callback;
    model::diff::cluster_diff_ptr_t delayed_ack_holder;
    model::diff::cluster_diff_t *delayed_ack_current;
    timers_t timers;
    bool auto_finish;
    bool auto_ack_blocks;
    bool make_presentation;
    io_errors_t io_errors;
    r::address_ptr_t sink;
};

}; // namespace syncspirit::test


code:
LANGUAGE 0, 0

/* CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST MOVEABLE PURE DISCARDABLE */
1 24 MOVEABLE PURE DISCARDABLE
BEGIN
  "<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>\n"
  "<assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0"">\n"
  "  <trustInfo xmlns=""urn:schemas-microsoft-com:asm.v3"">\n"
  "    <security>\n"
  "      <requestedPrivileges>\n"
  "        <requestedExecutionLevel level=""asInvoker""/>\n"
  "      </requestedPrivileges>\n"
  "    </security>\n"
  "  </trustInfo>\n"
  "  <compatibility xmlns=""urn:schemas-microsoft-com:compatibility.v1"">\n"
  "    <application>\n"
  "      <!--The ID below indicates application support for Windows Vista -->\n"
  "      <supportedOS Id=""{e2011457-1546-43c5-a5fe-008deee3d3f0}""/>\n"
  "      <!--The ID below indicates application support for Windows 7 -->\n"
  "      <supportedOS Id=""{35138b9a-5d96-4fbd-8e2d-a2440225f93a}""/>\n"
  "      <!--The ID below indicates application support for Windows 8 -->\n"
  "      <supportedOS Id=""{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}""/>\n"
  "      <!--The ID below indicates application support for Windows 8.1 -->\n"
  "      <supportedOS Id=""{1f676c76-80e1-4239-95bb-83d0f6d0da78}""/> \n"
  "      <!--The ID below indicates application support for Windows 10 -->\n"
  "      <supportedOS Id=""{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}""/> \n"
  "    </application>\n"
  "  </compatibility>\n"
  "</assembly>\n"
END


code:
log:
  copy/paste rows, add "export" button"

 local state: files, folders, size

- conflict handling is not available

core:
  - remove folder for controller test
  - remove folder for fs-scanner test

===========
5. разобраться с лог-левелами?

1. ^ если в block iterator'е были проскипанные блоки (из-за того, что были залочены), то файл(!) надо добавить на рескан через некоторое время

3. скан-актор не детектит скаченные файлы

4. надо как-то понимать, что folder is up-to-date?

- syncspirit-daemon: /home/b/development/cpp/syncspirit/src/net/dialer_actor.cpp:88: void syncspirit::net::dialer_actor_t::on_timer(rotor::request_id_t, bool): Assertion `it != redial_map.end()' failed.
- [2023-10-31 13:56:49.488] [D/23191] init/active:KUEQE66, resolving tcp://93.31.21.95:443 (transport = 0x5585be9747e0)

- 2022-03-28 20:47:54.664] [E/25546] fs::scan_actor, on_hash, file: my_label/Camera/VID_20210122_121411.mp4, error: fs::scan_actor request timeout

=====================
- идёт много file_clone, но нету скачки файла
- check 4 symlinks test (scaner) ?
- simulate file failure on win32 (open file before it will be opened in fs_actor)
- app-image : ./linuxdeploy-x86_64.AppImage --appdir AppDir -e bin/eratosthenes -d my.desktop -i /usr/share/icons/hicolor/48x48/apps/claws-mail.png --output appimage
- image icon:
    https://www.stockio.com/free-icons/?q=sync,
    https://www.freepik.com/free-vector/settings-icons-flat_1530486.htm#query=sync%20icon&position=1&from_view=search

===================

- восстанавливаться при креше (из-за MDBX_SAFE_NOSYNC) : держать заммапленый файлик
   file-uuid | (source) device_id | seq_no
   ----------+--------------------+-------
    ...
   к-во записей N задаётся в конфиге. Когда файл флушится, пишем запись.
   Как только произошло N записей, флушим БД на диск.

- controller: не падать, если пришла ошибка на запрос блока

- https://docs.syncthing.net/users/faq.html#what-things-are-synced
- когда нам прислали update, но нету sequence/устаревшая инфа, нужно апдейтить бд, но не запрашивать файлы
- test: validation happens before opening file?
- send indices deltas (after restart detects changes)
- test: fs_actor, deleted file from index
- test: controller, share folder -> index is sent
- test: file is not available
- test: device file
- start syncing folder when new folder has been added, нужно слать cancel{}
- backpressue on controller by file-actor?
- Canonicalize, TempNameWithPrefix, TempName
- локальные таймстемпы?
- metadata (timestamp, etc.)
- cluster is update by 3rd party: регать итераторы в cluster'е и инвалидировать, если надо.
- https://github.com/syncthing/syncthing/pull/7876
- https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
- https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
- https://github.com/wheybags/glibc_version_header

./src/ui-daemon/syncspirit-daemon --log_level trace --config_dir=../  --command add_peer:zzz:KUEQE66-JJ7P6AD-BEHD4ZW-GPBNW6Q-Y4C3K4Y-X44WJWZ-DVPIDXS-UDRJMA7  --command add_folder:label=my_label:id=nagkw-srrjz:path=/tmp/syncspirit --command share:folder=my_label:device=KUEQE66
- https://docs.0install.net/about/faq/

3. use wxWidgets

https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
https://stackoverflow.com/questions/64035952/how-to-key-press-detection-on-a-linux-terminal-low-level-style-in-python

- local-discovery
    iptables -t nat -A OUTPUT -p udp --dport 21027 -j DNAT --to-destination 127.0.0.1:21026

-relay
    syncthing-relay(7)
    relay://78.46.89.67:22067/?id=WAVJNLF-AVVZAPT-V6JEQA5-B3EPEW6-6SAR3C4-NFZBOW2-CWR3QM4-QJT2OA6&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070&providedBy=frogeye.fr

- discovery_support, upnp_support-> proto ?
- вообще не отвечает upnp

- restart all on config change

openssl x509 -inform PEM -outform DER -in ~/.config/syncthing/cert.pem -out /tmp/cert.der

- статически собирать бинарь под линь ( https://www.youtube.com/watch?v=Z7WuUhPJ-cU&list=PLZN9ZGiWZoZruMY-MlkAd-nHzdP9TfLRC&index=25 )
- case: когда коннектимся к пиру, но он ранее приконнектился к нам
- dialer-actor
- protobuf light?

- DB
https://github.com/erthink/libmdbx
https://iowow.io/
https://symas.com/lmdb/
https://en.wikipedia.org/wiki/LevelDB

https://wiki.lyrasis.org/display/FEDORA471/How+to+inspect+LevelDB
https://softwarerecs.stackexchange.com/questions/14409/leveldb-database-client

[RF7ZH] 2020/12/10 10:25:13.662765 logfs.go:61: DEBUG: casefs.go:152 basic /home/b/Desktop/finances Lstat invoices/samples/Rechnung_Nr._B2020128.pdf {0xc000a14f70} <nil>

https://forum.syncthing.net/t/a-few-questions-about-quic-hole-punching-and-stun/16490

=========

1. Есть глобальй регистратор

 message_type -> ack_count

2. Акторы при старте регаются что они выслают ack. При выключении - дерегаются.

3. Интерфес visitor'а (Effector)

 OnBegin(diff&) -> outcome::result;
 OnCommit(diff&) -> void;
 OnRevert(diff&) -> void;


code:
# gaming_without_spying

##### Blocklist against spyware, tracking and analytics in games.

Tracking and analyzing users doesn't stop when it comes to gaming.

On **[gameindustry](https://gameindustry.eu/)** you will find an unbelievably large list which opposes this.

You can grab the whole blocklist (hosts file) from this site. Alternately I provide a slightly modified list here. The reason for was making it integrateable as addition for other platforms and tools.

_Modifications:_

Entries that contain Google,Microsoft,Apple,Facebook,Twitter,OCSP,CRL,CDNs were removed. Else online services could be severely limited. There are a many other blocklists out there for trackers and ads in general.

Modified blocklist:

`https://codeberg.org/angrytux/gaming_without_spying/raw/branch/master/tracking-games.txt`

I only use a simple script and offer the finished file. Updates are made regularly when changes are made.

### **All the hard work and testing is done by the awesome guy from [gameindustry](https://gameindustry.eu/)**

[<img src="https://gameindustry.eu/images/header/logo_2024.png">](https://gameindustry.eu/)

**License:** [CC BY-NC-SA 4.0](https://gameindustry.eu/u/lizenz/)


code:
#!/bin/bash
curdir=$(pwd)
dl_url="https://hosts.gameindustry.eu/pc-main-template/dl/"
list_name="tracking-games.txt"
rm -f games.list
wget -qO- "$dl_url" > games.list
sed -i '/google/Id' games.list
sed -i '/APPLE/Id' games.list
sed -i '/Facebook/Id' games.list
sed -i '/Microsoft/Id' games.list
sed -i '/Twitter/Id' games.list
sed -i '/OCSP/Id' games.list
sed -i '/CRL/Id' games.list
sed -i '/CDN/Id' games.list
sed -i '/127/d' games.list
sed -i '/255.255/d' games.list
sed -i '/::1/d' games.list
sed -i '/ff00:/d' games.list
sed -i '/ff02:/d' games.list
sed -i '/fe80:/d' games.list
sed -i '/fe00:/d' games.list