diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a578e4b..0669dc5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,7 @@ jobs: ./gucc/tests/test-fetch_file ./gucc/tests/test-locale ./gucc/tests/test-package_profiles + ./gucc/tests/test-btrfs shell: bash build-cmake_withoutdev: name: Build with CMake (DEVENV OFF) @@ -98,6 +99,7 @@ jobs: ./gucc/tests/test-fetch_file ./gucc/tests/test-locale ./gucc/tests/test-package_profiles + ./gucc/tests/test-btrfs shell: bash build-meson_withoutdev: name: Build with Meson (DEVENV OFF) diff --git a/gucc/include/gucc/btrfs.hpp b/gucc/include/gucc/btrfs.hpp index e531621..76bd7e6 100644 --- a/gucc/include/gucc/btrfs.hpp +++ b/gucc/include/gucc/btrfs.hpp @@ -1,6 +1,8 @@ #ifndef BTRFS_HPP #define BTRFS_HPP +#include "gucc/partition.hpp" + #include // for string #include // for string_view #include // for vector @@ -21,6 +23,10 @@ auto btrfs_create_subvols(const std::vector& subvols, std::strin // Mounts btrfs subvolumes auto btrfs_mount_subvols(const std::vector& subvols, std::string_view device, std::string_view root_mountpoint, std::string_view mount_opts) noexcept -> bool; +// Appends btrfs subvolumes into Partition scheme +// with sorting scheme by device field +auto btrfs_append_subvolumes(std::vector& partitions, const std::vector& subvols) noexcept -> bool; + } // namespace gucc::fs #endif // BTRFS_HPP diff --git a/gucc/src/btrfs.cpp b/gucc/src/btrfs.cpp index b87dd28..d8d8bf1 100644 --- a/gucc/src/btrfs.cpp +++ b/gucc/src/btrfs.cpp @@ -1,7 +1,12 @@ #include "gucc/btrfs.hpp" #include "gucc/io_utils.hpp" +#include "gucc/partition.hpp" -#include +#include // for find_if +#include // for create_directories +#include // for optional +#include // for ranges::* +#include // for make_optional #include #include @@ -9,12 +14,13 @@ #include namespace fs = std::filesystem; +using namespace std::string_view_literals; namespace { // same behaviour as os.path.dirname from python constexpr auto get_dirname(std::string_view full_path) noexcept -> std::string_view { - if (full_path == "/") { + if (full_path == "/"sv) { return full_path; } auto pos = full_path.find_last_of('/'); @@ -24,6 +30,19 @@ constexpr auto get_dirname(std::string_view full_path) noexcept -> std::string_v return full_path.substr(0, pos); } +constexpr auto find_partition(const gucc::fs::Partition& part, const gucc::fs::BtrfsSubvolume& subvol) noexcept -> bool { + return (part.mountpoint == subvol.mountpoint) || (part.subvolume && *part.subvolume == subvol.subvolume); +} + +constexpr auto is_root_btrfs_part(const gucc::fs::Partition& part) noexcept -> bool { + return (part.mountpoint == "/"sv) && (part.fstype == "btrfs"sv); +} + +constexpr auto find_root_btrfs_part(auto&& parts) noexcept { + return std::ranges::find_if(parts, + [](auto&& part) { return is_root_btrfs_part(part); }); +} + } // namespace namespace gucc::fs { @@ -92,4 +111,51 @@ auto btrfs_mount_subvols(const std::vector& subvols, std::string return true; } +auto btrfs_append_subvolumes(std::vector& partitions, const std::vector& subvols) noexcept -> bool { + // if the list of subvolumes is empty, just return success at the beginning of the function + if (subvols.empty()) { + return true; + } + + auto root_part_it = find_root_btrfs_part(partitions); + if (root_part_it == std::ranges::end(partitions)) { + spdlog::error("Unable to find root btrfs partition!"); + return false; + } + + for (auto&& subvol : subvols) { + // check if we already have a partition with such subvolume + auto part_it = std::ranges::find_if(partitions, + [&subvol](auto&& part) { return find_partition(part, subvol); }); + + // we have found it. proceed to overwrite the values + if (part_it != std::ranges::end(partitions)) { + part_it->mountpoint = subvol.mountpoint; + part_it->subvolume = std::make_optional(subvol.subvolume); + continue; + } + // overwise, let's insert the partition based on the root partition + + // NOTE: we don't need to check for root part here, + // because it was already checked in the beginning + root_part_it = find_root_btrfs_part(partitions); + + Partition part{}; + part.fstype = root_part_it->fstype; + part.mountpoint = subvol.mountpoint; + part.uuid_str = root_part_it->uuid_str; + part.device = root_part_it->device; + part.mount_opts = root_part_it->mount_opts; + part.subvolume = std::make_optional(subvol.subvolume); + part.luks_mapper_name = root_part_it->luks_mapper_name; + part.luks_uuid = root_part_it->luks_uuid; + part.luks_passphrase = root_part_it->luks_passphrase; + partitions.emplace_back(std::move(part)); + } + + // sort by device + std::ranges::sort(partitions, {}, &Partition::device); + return true; +} + } // namespace gucc::fs diff --git a/gucc/tests/meson.build b/gucc/tests/meson.build index 5280d5b..d3c9e95 100644 --- a/gucc/tests/meson.build +++ b/gucc/tests/meson.build @@ -77,3 +77,11 @@ executable( link_with: [gucc_lib], include_directories: [include_directories('../include')], install: false) + +executable( + 'test-btrfs', + files('unit-btrfs.cpp'), + dependencies: deps, + link_with: [gucc_lib], + include_directories: [include_directories('../include')], + install: false) diff --git a/gucc/tests/unit-btrfs.cpp b/gucc/tests/unit-btrfs.cpp new file mode 100644 index 0000000..746eecf --- /dev/null +++ b/gucc/tests/unit-btrfs.cpp @@ -0,0 +1,141 @@ +#include "gucc/btrfs.hpp" +#include "gucc/logger.hpp" + +#include + +#include +#include +#include + +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; + +int main() { + auto callback_sink = std::make_shared([](const spdlog::details::log_msg&) { + // noop + }); + auto logger = std::make_shared("default", callback_sink); + spdlog::set_default_logger(logger); + gucc::logger::set_logger(logger); + + const std::vector subvolumes{ + gucc::fs::BtrfsSubvolume{.subvolume = "/@"s, .mountpoint = "/"s}, + gucc::fs::BtrfsSubvolume{.subvolume = "/@home"s, .mountpoint = "/home"s}, + gucc::fs::BtrfsSubvolume{.subvolume = "/@cache"s, .mountpoint = "/var/cache"s}, + // gucc::fs::BtrfsSubvolume{.subvolume = "/@snapshots"sv, .mountpoint = "/.snapshots"sv}, + }; + + // btrfs with subvolumes + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions.size() == 4); + assert(partitions == expected_partitions); + } + // invalid btrfs with subvolumes + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(!gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions.size() == 3); + assert(partitions == expected_partitions); + } + // invalid (without root part) btrfs with subvolumes + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(!gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions.size() == 3); + assert(partitions == expected_partitions); + } + // btrfs without subvolumes + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(gucc::fs::btrfs_append_subvolumes(partitions, {})); + assert(partitions.size() == 2); + assert(partitions == expected_partitions); + } + // luks xfs + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "xfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,lazytime,noatime,attr2,inode64,logbsize=256k,noquota"s, .luks_mapper_name = "luks_device"s, .luks_uuid = "00e1b836-81b6-433f-83ca-0fd373e3cd50"s}, + gucc::fs::Partition{.fstype = "linuxswap"s, .mountpoint = ""s, .uuid_str = ""s, .device = "/dev/nvme0n1p3"s, .mount_opts = "defaults,noatime"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "xfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,lazytime,noatime,attr2,inode64,logbsize=256k,noquota"s, .luks_mapper_name = "luks_device"s, .luks_uuid = "00e1b836-81b6-433f-83ca-0fd373e3cd50"s}, + gucc::fs::Partition{.fstype = "linuxswap"s, .mountpoint = ""s, .uuid_str = ""s, .device = "/dev/nvme0n1p3"s, .mount_opts = "defaults,noatime"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(!gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions == expected_partitions); + } + // valid zfs + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(!gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions == expected_partitions); + } + // luks btrfs with subvolumes + { + const std::vector expected_partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@"s, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@home"s, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@cache"s, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s, .subvolume = "/@"s, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + assert(gucc::fs::btrfs_append_subvolumes(partitions, subvolumes)); + assert(partitions.size() == 4); + assert(partitions == expected_partitions); + } +}