~nabijaczleweli/klapki

913953a8053ee16d5f18a5423f884687370fc25b — наб 1 year, 3 days ago 985a61b
Add delkernel. Do LTO on GCC. Probably other things
M .build.yml => .build.yml +0 -0
M Makefile => Makefile +1 -1
@@ 23,7 23,7 @@
include configMakefile


LDDLLS := efivar efiboot crypto
LDDLLS := efivar efiboot crypto $(OS_LD_LIBS)
LDAR := $(LNCXXAR) $(foreach l,,-L$(BLDDIR)$(l)) $(foreach dll,$(LDDLLS),-l$(dll))
INCAR := $(foreach l,$(foreach l,fmt,$(l)/include) Catch2/single_include/catch2,-isystemext/$(l)) $(foreach l,,-isystem$(BLDDIR)$(l)/include) -isystemext
VERAR := $(foreach l,KLAPKI,-D$(l)_VERSION='$($(l)_VERSION)')

M configMakefile => configMakefile +2 -2
@@ 27,10 27,10 @@ OS_LD_LIBS :=

CXXVER := $(shell $(CXX) --version)
ifneq "$(findstring clang,$(CXXVER))" ""
	# GCC doesn't have this granularity, nor LTO
	# GCC doesn't have this granularity
	PEDANTIC := -pedantic -Wno-gnu-statement-expression -flto=full
else
	PEDANTIC :=
	PEDANTIC := -flto
endif



M src/context.hpp => src/context.hpp +18 -6
@@ 45,8 45,6 @@ namespace klapki::context {
		std::string description;
		std::string cmdline;

		efidp_hd device;  // TODO: potentially support more than just this

		std::pair<std::string, std::string> image_path;                              // path in ESP, basename
		std::vector<std::pair<state::nonbase_dirname_t, std::string>> initrd_paths;  // path in ESP, basename
	};


@@ 62,16 60,18 @@ namespace klapki::context {
		std::map<std::uint16_t, our_kernel> our_kernels;
		std::vector<fresh_kernel> fresh_kernels;

		std::set<std::pair<std::string, std::string>> deleted_files;  // path in ESP, basename

		// TODO: add/remove variants to/from existing kernels
		std::optional<std::string> age(const config & cfg, state::state & state);
		std::optional<std::string> wisen(const config & cfg, state::state & state);
		std::optional<std::string> save(const config & cfg, state::state & state);
		std::optional<std::string> commit(const config & cfg, state::state & state);  // Only updates file SHAs in state

		static context derive(state::state & input_state);
		static context derive(const config & cfg, state::state & input_state);
	};

	std::variant<state::state, std::string> resolve_state_context(const state::state & input_state);
	std::variant<state::state, std::string> resolve_state_context(const config & cfg, const state::state & input_state);
}




@@ 127,6 127,19 @@ struct fmt::formatter<klapki::context::context> {
			out = format_to(out, "  {}\n", el);
		}

		out = format_to(out, "]\n"
		                     "Deleted files: [");

		first = true;
		for(auto && el : context.deleted_files) {
			if(first) {
				*out++ = '\n';
				first  = false;
			}

			out = format_to(out, "  (\"{}\", \"{}\")\n", el.first, el.second);
		}

		out = format_to(out, "]");

		return out;


@@ 141,8 154,7 @@ struct fmt::formatter<klapki::context::our_kernel> {
	auto format(const klapki::context::our_kernel & kern, FormatContext & ctx) {
		auto out = ctx.out();

		out = format_to(out, "{{ \"{}\", \"{}\", {}, (\"{}\", \"{}\"), [", kern.description, kern.cmdline,
		                kern.device.header.length ? "(device)" : "(unknown device)", kern.image_path.first, kern.image_path.second);
		out = format_to(out, "{{ \"{}\", \"{}\", (\"{}\", \"{}\"), [", kern.description, kern.cmdline, kern.image_path.first, kern.image_path.second);

		bool first = true;
		for(auto && el : kern.initrd_paths) {

M src/context_age.cpp => src/context_age.cpp +1 -1
@@ 156,7 156,7 @@ std::optional<std::string> klapki::context::context::age(const config & cfg, sta

			state.entries.emplace(new_bootnum,
			                      state::boot_entry{{}, 0, {0x00}, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS});
			this->our_kernels.emplace(new_bootnum, our_kernel{"", "", {}, {efi_base, std::string{fkern.image.second}}, our_initrd_paths});
			this->our_kernels.emplace(new_bootnum, our_kernel{"", "", {efi_base, std::string{fkern.image.second}}, our_initrd_paths});
			// state::stated_config_entry{new_bootnum, {0xFF}, std::string{fkern.version}, var, efi_base, });

			return std::monostate{};

M src/context_commit.cpp => src/context_commit.cpp +82 -49
@@ 28,14 28,20 @@
#include <fcntl.h>
#include <openssl/sha.h>
#include <set>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <chrono>
#include <sys/mman.h>

#define TRY(...)                                       \
	({                                                   \
		auto ret = __VA_ARGS__;                            \
		if(auto err = std::get_if<std::string>(&ret); err) \
			return std::move(*err);                          \
		std::move(std::get<0>(ret));                       \
	})

#define TRY_OPT(...)              \
	if(auto err = __VA_ARGS__; err) \


@@ 51,7 57,7 @@ using sha_t = std::uint8_t[20];
/// Plus, I've no idea if libefivar even works on Berkeley distros; it's got separate linux*.c implementations, but.
///
/// Just use sendfile(2) here and potentially ifdef for non-Linux later.
static std::optional<std::string> copy_file(int srcdir, int destdir, const char * basename, sha_t cursha, bool verbose) {
static std::variant<bool, std::string> copy_file(int srcdir, int destdir, const char * basename, sha_t cursha, bool verbose) {
	auto srcfd = openat(srcdir, basename, O_RDONLY);
	if(srcfd < 0)
		return fmt::format("Couldn't open {} for reading: {}\n", basename, strerror(errno));


@@ 71,13 77,12 @@ static std::optional<std::string> copy_file(int srcdir, int destdir, const char 
				fmt::print(stderr, "munmap {}: {}\n", basename, strerror(errno));
		}};

		SHA1((const unsigned char *)map, src_sb.st_size, insha);
		SHA1(reinterpret_cast<const unsigned char *>(map), src_sb.st_size, insha);
	}
	auto mid = std::chrono::high_resolution_clock::now();
	if(!std::memcmp(cursha, insha, sizeof(sha_t))) {
		if(verbose)
			fmt::print("{} unchanged ({})\n", basename, klapki::context::detail::sha_f{cursha});
		return {};
		return false;
	}

	if(verbose)


@@ 99,58 104,63 @@ static std::optional<std::string> copy_file(int srcdir, int destdir, const char 

		src_sb.st_size -= res;
	}
	auto end = std::chrono::high_resolution_clock::now();
	fmt::print(stderr, "{} ({}/{})\n", (end - mid).count(), decltype(end - mid)::period{}.num, decltype(end - mid)::period{}.den);

	return {};
	return true;
}


std::optional<std::string> klapki::context::context::commit(const config & cfg, state::state & state) {
	auto esp_fd = open(cfg.esp.data(), O_RDONLY | O_DIRECTORY | O_PATH);
	if(esp_fd < 0)
		return fmt::format("Couldn't open ESP ({}): {}\n", cfg.esp, strerror(errno));
	quickscope_wrapper esp_fd_deleter{[&] { close(esp_fd); }};

	std::map<std::string_view, int> esp_dirs;
	quickscope_wrapper esp_dirs_deleter{[&] {
		for(auto && [dir, fd] : esp_dirs)
			close(fd);
	}};
	{
		auto esp_fd = open(cfg.esp.data(), O_RDONLY | O_DIRECTORY | O_PATH);
		if(esp_fd < 0)
			return fmt::format("Couldn't open ESP ({}): {}\n", cfg.esp, strerror(errno));
		quickscope_wrapper esp_fd_deleter{[&] { close(esp_fd); }};

		for(auto && kern : this->our_kernels)
			if(esp_dirs.find(kern.second.image_path.first) == std::end(esp_dirs)) {
				auto dir = kern.second.image_path.first;
				std::transform(std::begin(dir), std::end(dir), std::begin(dir), [](auto c) { return c == '\\' ? '/' : c; });
				dir.erase(std::remove_if(std::begin(dir), std::end(dir),
				                         [prev = '\0'](auto c) mutable {
					                         if(prev == '/' && c == '/')
						                         return true;
					                         else {
						                         prev = c;
						                         return false;
					                         }
				                         }),
				          std::end(dir));
				if(dir.back() == '/')
					dir.pop_back();
				dir.erase(0, dir.find_first_not_of('/'));

				for(auto slash_idx = dir.find('/'); slash_idx != std::string::npos; slash_idx = dir.find('/', slash_idx + 1)) {
					dir[slash_idx] = '\0';
					if(mkdirat(esp_fd, dir.c_str(), 0755) < 0 && errno != EEXIST)
						return fmt::format("Couldn't create {} under ESP ({}): {}\n", dir.c_str(), cfg.esp, strerror(errno));
					dir[slash_idx] = '/';
				}
	std::set<std::string> deleted_dir_names;
	auto adddir = [&](auto && ddir, auto && cbk) -> std::optional<std::string> {
		if(esp_dirs.find(ddir) == std::end(esp_dirs)) {
			auto dir = ddir;
			std::transform(std::begin(dir), std::end(dir), std::begin(dir), [](auto c) { return c == '\\' ? '/' : c; });
			dir.erase(std::remove_if(std::begin(dir), std::end(dir),
			                         [prev = '\0'](auto c) mutable {
				                         if(prev == '/' && c == '/')
					                         return true;
				                         else {
					                         prev = c;
					                         return false;
				                         }
			                         }),
			          std::end(dir));
			if(dir.back() == '/')
				dir.pop_back();
			dir.erase(0, dir.find_first_not_of('/'));

			for(auto slash_idx = dir.find('/'); slash_idx != std::string::npos; slash_idx = dir.find('/', slash_idx + 1)) {
				dir[slash_idx] = '\0';
				if(mkdirat(esp_fd, dir.c_str(), 0755) < 0 && errno != EEXIST)
					return fmt::format("Couldn't create {} under ESP ({}): {}\n", dir, cfg.esp, strerror(errno));

				auto fd = openat(esp_fd, dir.c_str(), O_RDONLY | O_DIRECTORY | O_PATH);
				if(fd < 0)
					return fmt::format("Couldn't open {} under ESP ({}): {}\n", dir, cfg.esp, strerror(errno));
				esp_dirs.emplace(kern.second.image_path.first, fd);
					return fmt::format("Couldn't create {} under ESP ({}): {}\n", dir.c_str(), cfg.esp, strerror(errno));
				dir[slash_idx] = '/';
			}
	}
			if(mkdirat(esp_fd, dir.c_str(), 0755) < 0 && errno != EEXIST)
				return fmt::format("Couldn't create {} under ESP ({}): {}\n", dir, cfg.esp, strerror(errno));

			auto fd = openat(esp_fd, dir.c_str(), O_RDONLY | O_DIRECTORY | O_PATH);
			if(fd < 0)
				return fmt::format("Couldn't open {} under ESP ({}): {}\n", dir, cfg.esp, strerror(errno));
			esp_dirs.emplace(ddir, fd);
			cbk(std::move(dir));
		}

		return {};
	};
	for(auto && kern : this->our_kernels)
		TRY_OPT(adddir(kern.second.image_path.first, [](auto &&) {}));
	for(auto && [ddir, _] : this->deleted_files)
		TRY_OPT(adddir(ddir, [&](auto && dname) { deleted_dir_names.emplace(std::move(dname)); }));


	std::map<detail::bad_cow, int> source_dir_fds;


@@ 160,7 170,6 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
				close(fd);  // No error checking, just directories
	}};

	// TODO: maybe, just maybe, deduplicate these?
	for(auto && skern : state.statecfg.wanted_entries) {
		source_dir_fds[detail::bad_cow{skern.kernel_dirname}] = -1;
		for(auto && [initrd_dirname, _] : skern.initrd_dirnames)


@@ 194,7 203,8 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 

		if(cfg.verbose)
			fmt::print("Entry {:04X}: copying {} from {} to {}\n", bootnum, basename, srcdir, destdir);
		TRY_OPT(copy_file(srcfd->second, destfd->second, basename.c_str(), cursha, cfg.verbose));
		if(TRY(copy_file(srcfd->second, destfd->second, basename.c_str(), cursha, cfg.verbose)))
			fmt::print("Entry {:04X}: copied {} from {} to {}\n", bootnum, basename, srcdir, destdir);
		already_copied.emplace(std::pair{destfd->second, basename}, cursha);
		return {};
	};


@@ 225,7 235,30 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
	}


	// TODO: delete old files from delkernel here
	for(auto && [ddir, dfile] : this->deleted_files) {
		auto destfd = esp_dirs.find(ddir);
		if(destfd == std::end(esp_dirs))
			return fmt::format("ESP dir {} not in cache?", ddir);

		if(already_copied.find(std::pair{destfd->second, std::string_view{dfile}}) != std::end(already_copied)) {
			if(cfg.verbose)
				fmt::print("Not removing {} from {} after having copied it there\n", dfile, ddir);
			continue;
		}

		if(cfg.verbose)
			fmt::print("Removing {} from {}\n", dfile, ddir);
		if(unlinkat(destfd->second, dfile.c_str(), 0) < 0 && unlinkat(destfd->second, dfile.c_str(), AT_REMOVEDIR) < 0)
			return fmt::format("Removing {} from {}: {}", dfile, ddir, strerror(errno));
	}

	for(auto && ddir : deleted_dir_names) {
		if(cfg.verbose)
			fmt::print("Cleaning up {} from {}\n", ddir, cfg.esp);
		if(unlinkat(esp_fd, ddir.c_str(), AT_REMOVEDIR) < 0)
			if(cfg.verbose)
				fmt::print("Removing {} from {}: {}\n", ddir, cfg.esp, strerror(errno));
	}


	return {};

M src/context_derive.cpp => src/context_derive.cpp +10 -11
@@ 20,6 20,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#include "config.hpp"
#include "context.hpp"
#include "context_detail.hpp"
#include <fmt/format.h>


@@ 31,7 32,7 @@ extern "C" {
}


klapki::context::context klapki::context::context::derive(state::state & output_state) {
klapki::context::context klapki::context::context::derive(const config & cfg, state::state & output_state) {
	context ret{};

	for(auto && went : output_state.statecfg.wanted_entries)


@@ 54,18 55,14 @@ klapki::context::context klapki::context::context::derive(state::state & output_
			}
			if(dp->subtype != EFIDP_MEDIA_HD) {
				fmt::print(stderr, "Entry {:04X} not Media Device Path HD. Dropping.\n", went.bootnum_hint);
				// TODO: support this maybe?
				continue;
			}

			{
				std::string pp(256, '\0');
				efidp_format_device_path(pp.data(), pp.size(), dp, efi_opt_len);
				fmt::print("Entry {:04X}: {}\n", went.bootnum_hint, pp.c_str());  // verbose
			if(cfg.verbose) {
				fmt::print("Entry {:04X}: ", went.bootnum_hint);
				klapki::context::detail::print_devpath(dp, efi_opt_len);
			}

			kern.device = dp->hd;

			// We're assuming all we have is HD+file for now, and dog help me
			const efidp_data * file;
			switch(efidp_next_node(dp, &file)) {


@@ 103,12 100,14 @@ klapki::context::context klapki::context::context::derive(state::state & output_
					__builtin_unreachable();
			}

			// TODO: verify this is the last path entry maybe
			// TODO: verify this is the last path entry

			fmt::print("Entry {:04X}: [\"{}\", \"{}\"]\n", went.bootnum_hint, kern.image_path.first, kern.image_path.second);  // verbose
			if(cfg.verbose)
				fmt::print("Entry {:04X}: [\"{}\", \"{}\"]\n", went.bootnum_hint, kern.image_path.first, kern.image_path.second);

			kern.description = reinterpret_cast<const char *>(efi_loadopt_desc(efi_opt, efi_opt_len));
			fmt::print("Entry {:04X}: {}\n", went.bootnum_hint, kern.description);  // verbose
			if(cfg.verbose)
				fmt::print("Entry {:04X}: {}\n", went.bootnum_hint, kern.description);

			unsigned char * opt_data;
			size_t opt_data_len;

M src/context_detail.hpp => src/context_detail.hpp +6 -1
@@ 24,8 24,11 @@


#include <fmt/format.h>
#include <string_view>
#include <numeric>
#include <string_view>
extern "C" {
#include <efivar/efiboot.h>
}


namespace klapki::context::detail {


@@ 64,6 67,8 @@ namespace klapki::context::detail {
	struct sha_f {
		const std::uint8_t * sha;
	};

	void print_devpath(const efidp_data * dp, ssize_t dp_len);
}



M src/context_save.cpp => src/context_save.cpp +9 -5
@@ 22,6 22,7 @@

#include "config.hpp"
#include "context.hpp"
#include "context_detail.hpp"
#include "quickscope_wrapper.hpp"
#include <algorithm>
#include <fcntl.h>


@@ 47,7 48,8 @@ static constexpr bool isslash(char c) {
	return c == '\\' || c == '/';
}

static void print_devpath(const efidp_data * dp, ssize_t dp_len) {

void klapki::context::detail::print_devpath(const efidp_data * dp, ssize_t dp_len) {
	const auto size = efidp_format_device_path(nullptr, 0, dp, dp_len);
	if(size < 0)
		fmt::print("couldn't format?\n");


@@ 83,8 85,10 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st

		esp_devpath = reinterpret_cast<efidp_data *>(esp_devpath_raw.data());
		{  // esp_devpath is currently HD(some path)/File("\"). Trim it to just HD() for appending later
			efidp_data * fnode;
			(void)efidp_next_node(esp_devpath, const_cast<const efidp_data **>(&fnode));
			efidp_data * fnode{};
			if(efidp_next_node(esp_devpath, const_cast<const efidp_data **>(&fnode)) != 1)
				throw __func__;

			fnode->type    = EFIDP_END_TYPE;
			fnode->subtype = EFIDP_END_ENTIRE;
		}


@@ 92,7 96,7 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st

		if(cfg.verbose) {
			fmt::print("ESP devpath: ");
			print_devpath(reinterpret_cast<const efidp_data *>(esp_devpath_raw.data()), esp_devpath_raw.size());
			detail::print_devpath(reinterpret_cast<const efidp_data *>(esp_devpath_raw.data()), esp_devpath_raw.size());
		}
	}



@@ 133,7 137,7 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st

		if(cfg.verbose) {
			fmt::print("Entry {:04X} devpath: ", bootnum);
			print_devpath(devpath, devpath_len);
			detail::print_devpath(devpath, devpath_len);
		}

		// Must be at start, we use position in derive() to match extraneous ones from cmdline

M src/context_state.cpp => src/context_state.cpp +7 -5
@@ 20,6 20,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#include "config.hpp"
#include "context.hpp"
#include "context_detail.hpp"
#include "util.hpp"


@@ 34,7 35,7 @@
using sha_t = std::uint8_t[20];


std::variant<klapki::state::state, std::string> klapki::context::resolve_state_context(const state::state & input_state) {
std::variant<klapki::state::state, std::string> klapki::context::resolve_state_context(const config & cfg, const state::state & input_state) {
	std::map<std::uint16_t, state::boot_entry> entries{input_state.entries};
	// When measured (KVM, amd64 on amd64, Xeon E5645 @ 2.40GHz, 6 vCPUs), this took 23`636ns all told for 14 SHAs (+outlier at 207`817);
	// multithreading is much much more expensive: 2`633`277.86ns on same dataset when spawning a std::thread per.


@@ 56,7 57,7 @@ std::variant<klapki::state::state, std::string> klapki::context::resolve_state_c
			                   if(auto dupe = std::find_if(std::begin(shas), std::end(shas),
			                                               [&](auto && stored_sha) { return !std::memcmp(went.load_option_sha, stored_sha.second, sizeof(sha_t)); });
			                      dupe != std::end(shas)) {
				                   fmt::print(stderr, "Boot entry {:04X}: duplicate SHA {} with entry {:04X}. Dropping\n", went.bootnum_hint,
				                   fmt::print(stderr, "Entry {:04X}: duplicate SHA {} with entry {:04X}. Dropping\n", went.bootnum_hint,
				                              detail::sha_f{went.load_option_sha}, dupe->first);
				                   return true;
			                   }


@@ 74,12 75,13 @@ std::variant<klapki::state::state, std::string> klapki::context::resolve_state_c
	             [&](auto && went) {
		             if(auto bent = entries.find(went.bootnum_hint); bent != std::end(entries)) {
			             if(!std::memcmp(bent->second.load_option_sha, went.load_option_sha, sizeof(sha_t))) {
				             fmt::print("{:04X} matches\n", went.bootnum_hint);  // verbose
				             if(cfg.verbose)
					             fmt::print("Entry {:04X} matches\n", went.bootnum_hint);
				             return true;
			             } else
				             fmt::print(stderr, "Boot entry {:04X}: mismatched invalid hash, searching elsewhere\n", went.bootnum_hint);
				             fmt::print(stderr, "Entry {:04X}: mismatched hash, searching elsewhere\n", went.bootnum_hint);
		             } else
			             fmt::print(stderr, "Boot entry {:04X} doesn't exist; moved?, searching elsewhere\n", went.bootnum_hint);
			             fmt::print(stderr, "Entry {:04X} doesn't exist; moved?, searching elsewhere\n", went.bootnum_hint);

		             if(auto bent = std::find_if(begin(entries), end(entries),
		                                         [&](const auto & bent) { return !std::memcmp(bent.second.load_option_sha, went.load_option_sha, sizeof(sha_t)); });

M src/main.cpp => src/main.cpp +4 -4
@@ 51,13 51,13 @@ namespace klapki {
			fmt::print("{}\n", cfg);


		const auto input_state = TRY(2, state::state::load(cfg.argv0, cfg.host.c_str()));
		const auto input_state = TRY(2, state::state::load(cfg.argv0, cfg.host));


		auto output_state = TRY(3, context::resolve_state_context(input_state));
		auto output_state = TRY(3, context::resolve_state_context(cfg, input_state));


		auto context = context::context::derive(output_state);
		auto context = context::context::derive(cfg, output_state);


		for(auto && op : cfg.ops) {


@@ 96,7 96,7 @@ namespace klapki {
				op::execute(klapki::ops::dump{}, cfg, output_state, context);
			}

			TRY_OPT(8, "output_state.commit()", output_state.commit(cfg, input_state));
			TRY_OPT(8, "output_state.commit()", output_state.commit(cfg.host, input_state));
		}

		return 0;

M src/ops_execute.cpp => src/ops_execute.cpp +50 -3
@@ 105,9 105,56 @@ std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & a
	return {};
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & dk, const klapki::config &, klapki::state::state &, context::context & context) {
	throw;  // TODO: dunno what to do for this
	throw;  // TODO: add referenced files to context to be deleted during commit
std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & dk, const klapki::config &, klapki::state::state & state,
                                                context::context & context) {
	state.statecfg.wanted_entries.erase(std::remove_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
	                                                   [&](auto && skern) {
		                                                   if(skern.version != dk.version)
			                                                   return false;

		                                                   if(!state.entries.erase(skern.bootnum_hint))
			                                                   fmt::print(stderr, "Deleted entry {:04X} not in boot entries?\n", skern.bootnum_hint);

		                                                   auto kern_n = context.our_kernels.extract(skern.bootnum_hint);
		                                                   if(!kern_n)
			                                                   throw __func__;
		                                                   auto && kern = kern_n.mapped();

		                                                   std::string_view dprev = context.deleted_files.emplace(std::move(kern.image_path)).first->first;
		                                                   for(auto && [didir, dibase] : kern.initrd_paths)
			                                                   if(didir)
				                                                   dprev = context.deleted_files.emplace(std::move(*didir), std::move(dibase)).first->first;
			                                                   else
				                                                   context.deleted_files.emplace(dprev, std::move(dibase));

		                                                   std::visit(klapki::overload{
		                                                                  [&](klapki::state::boot_order_flat &) { throw __func__; },
		                                                                  [&](klapki::state::boot_order_structured & bord) {
			                                                                  for(auto && [chunk, ours] : bord.order) {
				                                                                  if(!ours)
					                                                                  continue;

				                                                                  if(auto itr = std::find(std::begin(chunk), std::end(chunk), skern.bootnum_hint);
				                                                                     itr != std::end(chunk)) {
					                                                                  chunk.erase(itr);
					                                                                  return;
				                                                                  }
			                                                                  }

			                                                                  fmt::print(stderr, "Deleted entry {:04X} not in boot order?\n", skern.bootnum_hint);
		                                                                  },
		                                                              },
		                                                              state.order);

		                                                   return true;
	                                                   }),
	                                    std::end(state.statecfg.wanted_entries));

	context.fresh_kernels.erase(
	    std::remove_if(std::begin(context.fresh_kernels), std::end(context.fresh_kernels), [&](auto && fkern) { return fkern.version == dk.version; }),
	    std::end(context.fresh_kernels));

	context.kernel_versions.erase(dk.version);

	return {};
}

M src/state.hpp => src/state.hpp +1 -1
@@ 94,7 94,7 @@ namespace klapki::state {
		std::map<std::uint16_t, boot_entry> entries;
		stated_config statecfg;

		std::optional<std::string> commit(const config & cfg, const state & original_state);
		std::optional<std::string> commit(std::string_view us, const state & original_state);

		static std::variant<state, std::string> load(const char * argv0, std::string_view us);
	};

M src/state_commit.cpp => src/state_commit.cpp +11 -16
@@ 20,7 20,6 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#include "config.hpp"
#include "efi.hpp"
#include "state.hpp"
#include <fmt/format.h>


@@ 30,22 29,19 @@ extern "C" {
#include <efivar/efivar-guids.h>
}

#include <typeinfo>


#define TRY_OPT(...)              \
	if(auto err = __VA_ARGS__; err) \
		return err;


std::optional<std::string> klapki::state::state::commit(const config & cfg, const state & original_state) {
std::optional<std::string> klapki::state::state::commit(std::string_view us, const state & original_state) {
	if(this->statecfg != original_state.statecfg) {
		if(cfg.verbose)
			fmt::print("Updating state config…\n");
		fmt::print("Updating state config\n");

		auto statecfg_raw = this->statecfg.serialise();

		if(efi_set_variable(klapki::efi_guid_klapki, cfg.host.c_str(), statecfg_raw.data(), statecfg_raw.size(),
		if(efi_set_variable(klapki::efi_guid_klapki, us.data(), statecfg_raw.data(), statecfg_raw.size(),
		                    EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, 0600) < 0)
			return fmt::format("Couldn't write new state config: {}", strerror(errno));
	}


@@ 56,19 52,17 @@ std::optional<std::string> klapki::state::state::commit(const config & cfg, cons
		                       if(now.order_cnt == orig.order_cnt && !std::memcmp(now.order.get(), orig.order.get(), now.order_cnt * sizeof(now.order[0])))
			                       return std::nullopt;

		                       if(cfg.verbose)
			                       fmt::print("Updating boot order…\n");
		                       fmt::print("Updating boot order\n");

		                       if(efi_set_variable(efi_guid_global, "BootOrder", reinterpret_cast<std::uint8_t *>(now.order.get()),
		                                           now.order_cnt * sizeof(now.order[0]),
		                                           EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, 0600) < 0)
			                       return fmt::format("Couldn't write new boot order: {}", strerror(errno));
		                       return std::nullopt;
		                       return {};
	                       },
	                       [&](const auto &, const auto &) -> std::optional<std::string> {
		                       throw __func__;  // nothing can be done now. context::save() should've set this, and the original state is also flat
	                       },
	                       [&](const auto & a, const auto & b) -> std::optional<std::string> {
		                       fmt::print(stderr, "{}  {}\n", typeid(a).name(), typeid(b).name());
		                       throw __func__;
	                       },  // nothing can be done now. context::save() should've set this, and the original state is also flat
	                   },
	                   this->order, original_state.order));



@@ 81,16 75,17 @@ std::optional<std::string> klapki::state::state::commit(const config & cfg, cons
		if(now == std::end(this->entries) && orig == std::end(original_state.entries))  // gap
			continue;
		else if(now == std::end(this->entries) && orig != std::end(original_state.entries)) {  // deleted
			fmt::print("Deleting entry {:04X}\n", i);
			if(efi_del_variable(efi_guid_global, fmt::format("Boot{:04X}", i).c_str()) < 0)
				return fmt::format("Couldn't delete entry for {:04X}: {}", i, strerror(errno));
			continue;
		} else if(now != std::end(this->entries) && orig == std::end(original_state.entries))  // new
			;
		else if(now->second.load_option_len == orig->second.load_option_len &&
		        !std::memcmp(now->second.load_option.get(), orig->second.load_option.get(), now->second.load_option_len))  // exists in both, only write if changed
			continue;

		if(cfg.verbose)
			fmt::print("{} entry {:04X}…\n", (orig == std::end(original_state.entries) ? "Writing" : "Updating"), i);
		fmt::print("{} entry {:04X}\n", (orig == std::end(original_state.entries)) ? "Writing" : "Updating", i);
		if(efi_set_variable(efi_guid_global, fmt::format("Boot{:04X}", i).c_str(), now->second.load_option.get(), now->second.load_option_len,
		                    now->second.attributes, 0600) < 0)
			return fmt::format("Couldn't write entry {:04X}: {}", i, strerror(errno));

M test/state_parse.cpp => test/state_parse.cpp +6 -4
@@ 95,11 95,13 @@ static std::unordered_map<const char *, klapki::state::stated_config> states = {
TEST_CASE("klapki::state::stated_config::parse() ENODATA", "[klapki::state::stated_config::parse]") {
	klapki::state::stated_config clean{};

	REQUIRE(klapki::state::stated_config::parse(clean, nullptr, 0) == ENODATA);
	REQUIRE(clean == klapki::state::stated_config{});
	REQUIRE(klapki::state::stated_config::parse(clean, nullptr, 0) == -1);
	REQUIRE(errno == ENODATA);
  REQUIRE(clean == klapki::state::stated_config{});

	REQUIRE(klapki::state::stated_config::parse(clean, nullptr, 1) == ENODATA);
	REQUIRE(clean == klapki::state::stated_config{});
	REQUIRE(klapki::state::stated_config::parse(clean, nullptr, 1) == -1);
	REQUIRE(errno == ENODATA);
  REQUIRE(clean == klapki::state::stated_config{});
}

TEST_CASE("klapki::state::stated_config::parse() okay", "[klapki::state::stated_config::parse]") {