~nabijaczleweli/klapki

2170ed103522d57fc08457c222cd6233ab025c7f — наб 1 year, 4 days ago 893993d
This might be releasable some day!
M src/context.hpp => src/context.hpp +8 -41
@@ 26,6 26,7 @@
#include "state.hpp"
#include <fmt/format.h>
#include <map>
#include <set>
#include <string_view>
#include <variant>
#include <vector>


@@ 40,40 41,6 @@ namespace klapki {


namespace klapki::context {
	namespace detail {
		template <class F>
		static void tokenise_cmdline(std::string_view chunk, F && func) {
			// Character set stolen from isspace()
			for(auto cur = chunk.find_first_not_of(" \f\n\r\t\v"); cur != std::string::npos; cur = chunk.find_first_not_of(" \f\n\r\t\v")) {
				chunk = chunk.substr(cur);

				auto next = chunk.find_first_of(" \f\n\r\t\v");
				if(!func(chunk.substr(0, next)))
					break;

				if(next == std::string::npos)
					break;
				chunk = chunk.substr(next);
			}
		}

		struct bad_cow {
			std::variant<std::string_view, std::string> data;

			constexpr std::string_view get() const noexcept {
				return std::visit([](auto && h) { return std::string_view{h}; }, this->data);
			}

			template <class O>
			constexpr bool operator==(const O & other) const noexcept {
				return this->get() == other;
			}
			constexpr bool operator==(const bad_cow & other) const noexcept { return this->get() == other.get(); }

			constexpr bool operator<(const bad_cow & other) const noexcept { return this->get() < other.get(); }
		};
	}

	struct our_kernel {
		std::string description;
		std::string cmdline;


@@ 91,7 58,7 @@ namespace klapki::context {
	};

	struct context {
		std::vector<std::string> kernel_versions;
		std::set<std::string, std::greater<std::string>> kernel_versions;
		std::map<std::uint16_t, our_kernel> our_kernels;
		std::vector<fresh_kernel> fresh_kernels;



@@ 99,7 66,7 @@ namespace klapki::context {
		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, const 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);
	};


@@ 174,7 141,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,
		out = format_to(out, "{{ \"{}\", \"{}\", {}, (\"{}\", \"{}\"), [", kern.description, kern.cmdline,
		                kern.device.header.length ? "(device)" : "(unknown device)", kern.image_path.first, kern.image_path.second);

		bool first = true;


@@ 184,13 151,13 @@ struct fmt::formatter<klapki::context::our_kernel> {
			else
				first = false;

			*out++ = '[';
			*out++ = '(';
			if(el.first)
				*out++ = '\"';
			out = format_to(out, "{}", el.first ? std::string_view{*el.first} : "←");
			if(el.first)
				*out++ = '\"';
			out = format_to(out, ", \"{}\"]", el.second);
			out = format_to(out, ", \"{}\")", el.second);
		}

		out = format_to(out, "] }}");


@@ 207,7 174,7 @@ struct fmt::formatter<klapki::context::fresh_kernel> {
	auto format(const klapki::context::fresh_kernel & kern, FormatContext & ctx) {
		auto out = ctx.out();

		out = format_to(out, "{{ \"{}\", [\"{}\", \"{}\"], [", kern.version, kern.image.first, kern.image.second);
		out = format_to(out, "{{ \"{}\", (\"{}\", \"{}\"), [", kern.version, kern.image.first, kern.image.second);

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


@@ 216,7 183,7 @@ struct fmt::formatter<klapki::context::fresh_kernel> {
			else
				first = false;

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

		out = format_to(out, "] }}");

M src/context_age.cpp => src/context_age.cpp +7 -6
@@ 22,6 22,7 @@

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


@@ 97,16 98,16 @@ namespace {
		           bord);
	}

	static std::vector<klapki::state::nonbase_dirname_t> compact_initrd_dirs(std::string_view prev_dir,
	                                                                         std::vector<klapki::context::detail::bad_cow> initrd_dirs) {
		std::vector<klapki::state::nonbase_dirname_t> ret;
	static std::vector<std::pair<klapki::state::nonbase_dirname_t, klapki::state::shaa_t>>
	compact_initrd_dirs(std::string_view prev_dir, std::vector<klapki::context::detail::bad_cow> initrd_dirs) {
		std::vector<std::pair<klapki::state::nonbase_dirname_t, klapki::state::shaa_t>> ret;
		ret.reserve(initrd_dirs.size());

		for(auto && idir : initrd_dirs) {
			if(idir.get() == prev_dir)
				ret.emplace_back();
				ret.emplace_back(klapki::state::nonbase_dirname_t{}, klapki::state::shaa_t{});
			else
				ret.emplace_back(idir.get());
				ret.emplace_back(idir.get(), klapki::state::shaa_t{});

			prev_dir = idir.get();
		}


@@ 151,7 152,7 @@ std::optional<std::string> klapki::context::context::age(const config & cfg, sta

			append_bootorder_entry(state.order, new_bootnum);
			state.statecfg.wanted_entries.emplace_back(
			    state::stated_config_entry{new_bootnum, {0xFF}, std::string{fkern.version}, var, std::string{image_dir.get()}, initrd_dirs});
			    state::stated_config_entry{new_bootnum, {0xFF}, std::string{fkern.version}, var, std::string{image_dir.get()}, {0x00}, initrd_dirs});

			state.entries.emplace(new_bootnum,
			                      state::boot_entry{{}, 0, {0x00}, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS});

M src/context_commit.cpp => src/context_commit.cpp +47 -14
@@ 22,28 22,36 @@

#include "config.hpp"
#include "context.hpp"
#include "context_detail.hpp"
#include "quickscope_wrapper.hpp"
#include <algorithm>
#include <fcntl.h>
#include <openssl/sha.h>
#include <set>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

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


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


using sha_t = std::uint8_t[20];


/// https://stackoverflow.com/a/2180157/2851815 says FreeBSD and Darwin have fcopyfile(3),
/// but the only reference I could find was (a) copies of that answer and (b) Apple documentation, which wasn't helpful.
///
/// 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) {
static std::optional<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));


@@ 53,6 61,29 @@ static std::optional<std::string> copy_file(int srcdir, int destdir, const char 
	if(fstat(srcfd, &src_sb) < 0)
		return fmt::format("Couldn't stat() source {}: {}\n", basename, strerror(errno));

	sha_t insha{};
	{
		auto map = mmap(nullptr, src_sb.st_size, PROT_READ, MAP_PRIVATE, srcfd, 0);
		if(!map)
			return fmt::format("Couldn't mmap() source {}: {}\n", basename, strerror(errno));
		klapki::quickscope_wrapper map_deleter{[&] {
			if(munmap(map, src_sb.st_size))
				fmt::print(stderr, "munmap {}: {}\n", basename, strerror(errno));
		}};

		SHA1((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 {};
	}

	if(verbose)
		fmt::print("{} changed ({} -> {})\n", basename, klapki::context::detail::sha_f{cursha}, klapki::context::detail::sha_f{insha});
	std::memcpy(cursha, insha, sizeof(sha_t));

	auto destfd = openat(destdir, basename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if(destfd < 0)
		return fmt::format("Couldn't open {} for writing: {}\n", basename, strerror(errno));


@@ 68,14 99,14 @@ 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 {};
}


std::optional<std::string> klapki::context::context::commit(const config & cfg, const state::state & state) {
	// TODO: parallelise the copying somehow?

std::optional<std::string> klapki::context::context::commit(const config & cfg, state::state & state) {
	std::map<std::string_view, int> esp_dirs;
	quickscope_wrapper esp_dirs_deleter{[&] {
		for(auto && [dir, fd] : esp_dirs)


@@ 132,7 163,7 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
	// 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)
		for(auto && [initrd_dirname, _] : skern.initrd_dirnames)
			if(initrd_dirname)
				source_dir_fds[detail::bad_cow{*initrd_dirname}] = -1;
	}


@@ 144,15 175,16 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
	}


	std::set<std::pair<int, std::string_view>> already_copied;
	auto do_copy = [&](auto bootnum, auto && srcdir, auto && destdir, auto && basename) -> std::optional<std::string> {
	std::map<std::pair<int, std::string_view>, const std::uint8_t *> already_copied;
	auto do_copy = [&](auto bootnum, auto && srcdir, auto && destdir, auto && basename, auto cursha) -> std::optional<std::string> {
		auto destfd = esp_dirs.find(destdir);
		if(destfd == std::end(esp_dirs))
			return fmt::format("ESP dir {} not in cache?", destdir);

		if(already_copied.count(std::pair{destfd->second, basename})) {
		if(auto ac = already_copied.find(std::pair{destfd->second, basename}); ac != std::end(already_copied)) {
			if(cfg.verbose)
				fmt::print("Entry {:04X}: already copied {} from {} to {}\n", bootnum, basename, srcdir, destdir);
			std::memcpy(cursha, ac->second, sizeof(sha_t));
			return {};
		}



@@ 162,8 194,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()));
		already_copied.emplace(destfd->second, basename);
		TRY_OPT(copy_file(srcfd->second, destfd->second, basename.c_str(), cursha, cfg.verbose));
		already_copied.emplace(std::pair{destfd->second, basename}, cursha);
		return {};
	};



@@ 173,7 205,7 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
		if(skern == std::end(state.statecfg.wanted_entries))
			throw __func__;

		TRY_OPT(do_copy(bootnum, skern->kernel_dirname, kern.image_path.first, kern.image_path.second));
		TRY_OPT(do_copy(bootnum, skern->kernel_dirname, kern.image_path.first, kern.image_path.second, skern->kernel_image_sha));

		if(skern->initrd_dirnames.size() != kern.initrd_paths.size())
			throw __func__;


@@ 182,12 214,13 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
		std::string_view dprev = kern.image_path.first;
		for(std::size_t i = 0; i < skern->initrd_dirnames.size(); ++i) {
			auto && [didir, dibase] = kern.initrd_paths[i];
			auto && [sidir, ssha]   = skern->initrd_dirnames[i];

			if(skern->initrd_dirnames[i])
				sprev = *skern->initrd_dirnames[i];
			if(sidir)
				sprev = *sidir;
			if(didir)
				dprev = *didir;
			TRY_OPT(do_copy(bootnum, sprev, dprev, dibase));
			TRY_OPT(do_copy(bootnum, sprev, dprev, dibase, &ssha[0]));
		}
	}


M src/context_derive.cpp => src/context_derive.cpp +2 -1
@@ 21,6 21,7 @@


#include "context.hpp"
#include "context_detail.hpp"
#include <fmt/format.h>
#include <string_view>
extern "C" {


@@ 144,7 145,7 @@ klapki::context::context klapki::context::context::derive(state::state & output_

	for(auto && went : output_state.statecfg.wanted_entries)
		if(auto bent = output_state.entries.find(went.bootnum_hint); bent != std::end(output_state.entries))
			ret.kernel_versions.push_back(went.version);
			ret.kernel_versions.insert(went.version);
		else
			throw __func__;  // unreachable, by this point all wanted entries match up to boot entries


A src/context_detail.hpp => src/context_detail.hpp +79 -0
@@ 0,0 1,79 @@
// The MIT License (MIT)

// Copyright (c) 2020 наб <nabijaczleweli@nabijaczleweli.xyz>

// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#pragma once


#include <fmt/format.h>
#include <string_view>
#include <numeric>


namespace klapki::context::detail {
	template <class F>
	static void tokenise_cmdline(std::string_view chunk, F && func) {
		// Character set stolen from isspace()
		for(auto cur = chunk.find_first_not_of(" \f\n\r\t\v"); cur != std::string::npos; cur = chunk.find_first_not_of(" \f\n\r\t\v")) {
			chunk = chunk.substr(cur);

			auto next = chunk.find_first_of(" \f\n\r\t\v");
			if(!func(chunk.substr(0, next)))
				break;

			if(next == std::string::npos)
				break;
			chunk = chunk.substr(next);
		}
	}

	struct bad_cow {
		std::variant<std::string_view, std::string> data;

		constexpr std::string_view get() const noexcept {
			return std::visit([](auto && h) { return std::string_view{h}; }, this->data);
		}

		template <class O>
		constexpr bool operator==(const O & other) const noexcept {
			return this->get() == other;
		}
		constexpr bool operator==(const bad_cow & other) const noexcept { return this->get() == other.get(); }

		constexpr bool operator<(const bad_cow & other) const noexcept { return this->get() < other.get(); }
	};

	struct sha_f {
		const std::uint8_t * sha;
	};
}


template <>
struct fmt::formatter<klapki::context::detail::sha_f> {
	constexpr auto parse(format_parse_context & ctx) { return ctx.begin(); }

	template <typename FormatContext>
	auto format(klapki::context::detail::sha_f s, FormatContext & ctx) {
		using sha_t = std::uint8_t[20];
		return std::accumulate(s.sha, s.sha + sizeof(sha_t), ctx.out(), [](auto && out, auto b) { return format_to(out, "{:02X}", b); });
	}
};

M src/context_save.cpp => src/context_save.cpp +92 -27
@@ 22,15 22,24 @@

#include "config.hpp"
#include "context.hpp"
#include "quickscope_wrapper.hpp"
#include <algorithm>
// #include <fcntl.h>
#include <fcntl.h>
#include <openssl/sha.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <ucs2.h>
#include <unistd.h>
extern "C" {
#include <efivar/efiboot.h>
}


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


using sha_t = std::uint8_t[20];




@@ 38,6 47,54 @@ static constexpr bool isslash(char c) {
	return c == '\\' || c == '/';
}

/// Make temp dirs down to fpath, call func, delete the ones created.
/// Zero-alloc, but fpath isn't reverted to being useful at the end, hence the &&.
template <class F>
static std::optional<std::string> with_temp_at(std::string && fpath, F && func) {
	auto nul = std::string::npos;
	for(struct stat sb; stat(fpath.c_str(), &sb) < 0;)
		if(errno == ENOENT) {
			if(nul != std::string::npos)
				fpath[nul] = '\0';

			nul = fpath.rfind('/', nul);
			if(nul == std::string::npos)
				break;
		} else
			return fmt::format("stat({}): {}", fpath.c_str(), strerror(errno));

	for(auto mknul = nul; (mknul = fpath.find('\0', mknul)) != std::string::npos;) {
		fpath[mknul] = '/';
		if(mkdir(fpath.c_str(), 0500) < 0)
			return fmt::format("mkdir({}): {}", fpath.c_str(), strerror(errno));
	}

	auto delete_paths = [&]() -> std::optional<std::string> {
		for(auto rmnul = std::string::npos; nul != std::string::npos;) {
			if(rmnul != std::string::npos)
				fpath[rmnul] = '\0';

			auto next = fpath.rfind('/', rmnul);
			if(next <= nul)
				break;

			if(rmdir(fpath.c_str()) < 0)
				return fmt::format("rmdir({}): {}", fpath.c_str(), strerror(errno));

			rmnul = next;
		}
		return {};
	};
	klapki::quickscope_wrapper path_deleter{delete_paths};

	TRY_OPT(func());

	TRY_OPT(delete_paths());
	nul = std::string::npos;

	return {};
}


std::optional<std::string> klapki::context::context::save(const config & cfg, state::state & state) {
	for(auto && [bootnum, kern] : this->our_kernels) {


@@ 62,34 119,42 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st
		                                }),
		                 std::end(image_path));
		std::transform(std::begin(image_path), std::end(image_path), std::begin(image_path), [](auto c) { return isslash(c) ? '/' : c; });
		fmt::print("{}\n", image_path);
		fmt::print("{}\n", kern.image_path.second);

		// efi_generate_file_device_path() requires the image file to exist,
		// and the implementation used for ESP detexion there is (a) hidden so we can't link to it and (b) massive and not something I wish to, or can, maintain.
		// This blows, but we take precautions to revert the filesystem back to how we found it if we needed to modify it.
		std::vector<std::uint8_t> devpath;
		do {
			devpath.resize(devpath.size() + 512);

			// extern ssize_t efi_generate_file_device_path(uint8_t *buf, ssize_t size,
			// 	      const char * const filepath,
			// 	      uint32_t options, ...)
			if(auto size = efi_generate_file_device_path(devpath.data(), devpath.size(),  //
			                                             image_path.c_str(),              //
			                                             EFIBOOT_ABBREV_HD);
			   size >= 0)
				devpath.resize(size);
			else if(errno != ENOSPC)
				return fmt::format("Making device path for {:04X}: {}", bootnum, strerror(errno));
		} while(errno == ENOSPC);
		if(cfg.verbose) {
			const auto size = efidp_format_device_path(nullptr, 0, reinterpret_cast<const efidp_data *>(devpath.data()), devpath.size());
			fmt::print("Entry {:04X} devpath: ", bootnum);
			if(size < 0)
				fmt::print("couldn't format?\n");
			else {
				std::string path(size, '\0');
				efidp_format_device_path(path.data(), path.size(), reinterpret_cast<const efidp_data *>(devpath.data()), devpath.size());
				fmt::print("{}\n", path);
		TRY_OPT(with_temp_at(std::move(image_path), [&, bootnum = bootnum]() -> std::optional<std::string> {
			do {
				devpath.resize(devpath.size() + 512);

				// extern ssize_t efi_generate_file_device_path(uint8_t *buf, ssize_t size,
				// 	      const char * const filepath,
				// 	      uint32_t options, ...)
				// EFIBOOT_ABBREV_HD matches what's produced by bootctl(1) install, and produces just HD()\File()
				if(auto size = efi_generate_file_device_path(devpath.data(), devpath.size(),  //
				                                             image_path.c_str(),              //
				                                             EFIBOOT_ABBREV_HD);
				   size >= 0)
					devpath.resize(size);
				else if(errno != ENOSPC)
					return fmt::format("Making device path for {:04X}: {}", bootnum, strerror(errno));
			} while(errno == ENOSPC);

			if(cfg.verbose) {
				const auto size = efidp_format_device_path(nullptr, 0, reinterpret_cast<const efidp_data *>(devpath.data()), devpath.size());
				fmt::print("Entry {:04X} devpath: ", bootnum);
				if(size < 0)
					fmt::print("couldn't format?\n");
				else {
					std::string path(size, '\0');
					efidp_format_device_path(path.data(), path.size(), reinterpret_cast<const efidp_data *>(devpath.data()), devpath.size());
					fmt::print("{}\n", path);
				}
			}
		}

			return {};
		}));


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

M src/context_state.cpp => src/context_state.cpp +3 -15
@@ 21,6 21,7 @@


#include "context.hpp"
#include "context_detail.hpp"
#include "util.hpp"
#include <algorithm>
#include <cstring>


@@ 32,19 33,6 @@

using sha_t = std::uint8_t[20];

struct sha_f {
	const std::uint8_t * sha;
};
template <>
struct fmt::formatter<sha_f> {
	constexpr auto parse(format_parse_context & ctx) { return ctx.begin(); }

	template <typename FormatContext>
	auto format(sha_f s, FormatContext & ctx) {
		return std::accumulate(s.sha, s.sha + sizeof(sha_t), ctx.out(), [](auto && out, auto b) { return format_to(out, "{:02X}", b); });
	}
};


std::variant<klapki::state::state, std::string> klapki::context::resolve_state_context(const state::state & input_state) {
	std::map<std::uint16_t, state::boot_entry> entries{input_state.entries};


@@ 69,7 57,7 @@ std::variant<klapki::state::state, std::string> klapki::context::resolve_state_c
			                                               [&](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,
				                              sha_f{went.load_option_sha}, dupe->first);
				                              detail::sha_f{went.load_option_sha}, dupe->first);
				                   return true;
			                   }



@@ 105,7 93,7 @@ std::variant<klapki::state::state, std::string> klapki::context::resolve_state_c

			             return true;
		             } else {
			             fmt::print(stderr, "Entry formerly {:04X} ({}) not found. Abandoning.\n", went.bootnum_hint, sha_f{went.load_option_sha});
			             fmt::print(stderr, "Entry formerly {:04X} ({}) not found. Abandoning.\n", went.bootnum_hint, detail::sha_f{went.load_option_sha});
			             return false;
		             }
	             });

M src/context_wisen.cpp => src/context_wisen.cpp +1 -0
@@ 22,6 22,7 @@

#include "config.hpp"
#include "context.hpp"
#include "context_detail.hpp"
#include "quickscope_wrapper.hpp"
#include <sys/mman.h>
#include <sys/types.h>

M src/main.cpp => src/main.cpp +3 -2
@@ 103,6 103,7 @@ namespace klapki {
	}
}

int main(int, const char ** argv) {
	return klapki::main(argv);
int main(int, const char ** argv) try { return klapki::main(argv); } catch(const char * thrown) {
	fmt::print(stderr, "{}\n", thrown);
	throw;
}

M src/ops_execute.cpp => src/ops_execute.cpp +6 -0
@@ 86,6 86,11 @@ std::optional<std::string> klapki::ops::execute(const klapki::ops::bootpos & bp,
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & ak, const klapki::config &, klapki::state::state &, context::context & context) {
	if(context.kernel_versions.count(ak.version)) {
		fmt::print(stderr, "addkernel: kernel version {} already known\n", ak.version);
		return {};
	}

	klapki::context::fresh_kernel kern{ak.version, {}, {}};

	slash_path(kern.image, ak.image);


@@ 95,6 100,7 @@ std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & a
		slash_path(kern.initrds.back(), initrd);
	}

	context.kernel_versions.emplace(std::move(ak.version));
	context.fresh_kernels.emplace_back(std::move(kern));
	return {};
}

M src/state.cpp => src/state.cpp +6 -5
@@ 152,11 152,12 @@ std::variant<klapki::state::state, std::string> klapki::state::state::load(const


bool klapki::state::stated_config_entry::operator==(const klapki::state::stated_config_entry & other) const noexcept {
	return this->bootnum_hint == other.bootnum_hint &&                                   //
	       !std::memcmp(this->load_option_sha, other.load_option_sha, sizeof(sha_t)) &&  //
	       this->version == other.version &&                                             //
	       this->variant == other.variant &&                                             //
	       this->kernel_dirname == other.kernel_dirname &&                               //
	return this->bootnum_hint == other.bootnum_hint &&                                     //
	       !std::memcmp(this->load_option_sha, other.load_option_sha, sizeof(sha_t)) &&    //
	       this->version == other.version &&                                               //
	       this->variant == other.variant &&                                               //
	       this->kernel_dirname == other.kernel_dirname &&                                 //
	       !std::memcmp(this->kernel_image_sha, other.kernel_image_sha, sizeof(sha_t)) &&  //
	       this->initrd_dirnames == other.initrd_dirnames;
}


M src/state.hpp => src/state.hpp +8 -5
@@ 24,6 24,7 @@


#include "util.hpp"
#include <array>
#include <cstdint>
#include <fmt/format.h>
#include <map>


@@ 38,6 39,7 @@ namespace klapki {

namespace klapki::state {
	using sha_t             = std::uint8_t[20];
	using shaa_t            = std::array<std::uint8_t, 20>;
	using nonbase_dirname_t = std::optional<std::string>;  // Use SUB to copy from previous entry

	struct boot_order_flat {


@@ 62,10 64,11 @@ namespace klapki::state {
		/// Hint initially, then pointer
		std::uint16_t bootnum_hint;  // big-endian
		sha_t load_option_sha;
		std::string version;                             // NUL-terminated
		std::string variant;                             // NUL-terminated
		std::string kernel_dirname;                      // on rootfs; NUL-terminated
		std::vector<nonbase_dirname_t> initrd_dirnames;  // on rootfs; entries NUL-terminated; list empty-terminated
		std::string version;         // NUL-terminated
		std::string variant;         // NUL-terminated
		std::string kernel_dirname;  // on rootfs; NUL-terminated
		sha_t kernel_image_sha;
		std::vector<std::pair<nonbase_dirname_t, shaa_t>> initrd_dirnames;  // on rootfs; entries NUL-terminated; list empty-terminated w/o SHA


		bool operator==(const stated_config_entry & other) const noexcept;


@@ 153,7 156,7 @@ struct fmt::formatter<klapki::state::stated_config_entry> {
		                ent.kernel_dirname);

		bool first = true;
		for(auto && el : ent.initrd_dirnames) {
		for(auto && [el, _] : ent.initrd_dirnames) {
			if(!first)
				out = format_to(out, ", ");
			else

M src/state_config.cpp => src/state_config.cpp +25 -5
@@ 105,6 105,14 @@ int klapki::state::stated_config::parse(klapki::state::stated_config & into, con
		size -= new_entry.kernel_dirname.size() + 1;  // NUL
		// fmt::print(stderr, "variant: {} ({})\n", new_entry.kernel_dirname, new_entry.kernel_dirname.size());

		if(size <= sizeof(new_entry.kernel_image_sha)) {
			fmt::print(stderr, "extraneous data; 1.5\n");
			break;
		}
		memcpy(&new_entry.kernel_image_sha, data, sizeof(new_entry.kernel_image_sha));
		data += sizeof(new_entry.kernel_image_sha);
		size -= sizeof(new_entry.kernel_image_sha);

		for(;;) {
			const auto idir_end = std::find_if(data, data + size, [](auto b) { return b == '\0'; });
			if(idir_end == data + size) {


@@ 115,14 123,23 @@ int klapki::state::stated_config::parse(klapki::state::stated_config & into, con
			std::string idir{data, static_cast<std::size_t>(idir_end - data)};
			data += idir.size() + 1;  // NUL
			size -= idir.size() + 1;  // NUL
			// fmt::print(stderr, "initrd: {} ({})\n", idir, idir.size());

			if(idir.empty())
				break;
			else if(idir == SUB)
				new_entry.initrd_dirnames.emplace_back();

			shaa_t idir_sha;
			if(size <= idir_sha.size()) {
				fmt::print(stderr, "extraneous data; 2.5\n");
				break;
			}
			memcpy(&idir_sha[0], data, idir_sha.size());
			data += idir_sha.size();
			size -= idir_sha.size();

			if(idir == SUB)
				new_entry.initrd_dirnames.emplace_back(nonbase_dirname_t{}, idir_sha);
			else
				new_entry.initrd_dirnames.emplace_back(std::move(idir));
				new_entry.initrd_dirnames.emplace_back(std::move(idir), idir_sha);
		}

		into.wanted_entries.emplace_back(std::move(new_entry));


@@ 154,13 171,16 @@ std::vector<std::uint8_t> klapki::state::stated_config::serialise() const {
		*cur++ = '\0';
		cur    = std::copy(std::begin(went.kernel_dirname), std::end(went.kernel_dirname), cur);
		*cur++ = '\0';
		cur    = std::copy(std::begin(went.kernel_image_sha), std::end(went.kernel_image_sha), cur);

		for(auto && idir : went.initrd_dirnames) {
		for(auto && [idir, isha] : went.initrd_dirnames) {
			if(idir)
				cur = std::copy(std::begin(*idir), std::end(*idir), cur);
			else
				cur = std::copy(SUB, (SUB) + std::strlen(SUB), cur);
			*cur++ = '\0';

			cur = std::copy(std::begin(isha), std::end(isha), cur);
		}
		*cur++ = '\0';
	}

M test-data/state_parse/exempli-gratia => test-data/state_parse/exempli-gratia +0 -0
M test-data/state_parse/just-kbase => test-data/state_parse/just-kbase +0 -0
M test-data/state_parse/one-initrd => test-data/state_parse/one-initrd +0 -0
M test-data/state_parse/one-initrd-copy+two-initrds => test-data/state_parse/one-initrd-copy+two-initrds +0 -0
M test/state_parse.cpp => test/state_parse.cpp +48 -39
@@ 37,49 37,58 @@ static std::unordered_map<const char *, klapki::state::stated_config> states = {
                                    "5.8.0-1-amd64",
                                    "",
                                    "/boot/",
                                    {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
                                    {}}}}},
    {"one-initrd",
     klapki::state::stated_config{0x0b02,
                                  {"test"},
                                  {{0x0102,
                                    {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
                                    "5.8.0-1-amd64",
                                    "debug",
                                    "/boot/",
                                    {"/boot/initrds"}}}}},
     klapki::state::stated_config{
         0x0b02,
         {"test"},
         {{0x0102,
           {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
           "5.8.0-1-amd64",
           "debug",
           "/boot/",
           {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
           {{"/boot/initrds", {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}}}}}}},
    {"one-initrd-copy+two-initrds",
     klapki::state::stated_config{0x0b02,
                                  {"test", "2st"},
                                  {{0x0102,
                                    {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
                                    "5.8.0-1-amd64",
                                    "loud",
                                    "/boot/",
                                    {{}}},
                                   {0x0102,
                                    {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
                                    "5.8.0-1-amd64",
                                    "quiet",
                                    "/boot/",
                                    {
                                        "/boot/initrds",
                                        "/boot",
                                    }}}}},
     klapki::state::stated_config{
         0x0b02,
         {"test", "2st"},
         {{0x0102,
           {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
           "5.8.0-1-amd64",
           "loud",
           "/boot/",
           {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
           {{{}, {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}}}},
          {0x0102,
           {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
           "5.8.0-1-amd64",
           "quiet",
           "/boot/",
           {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
           {
               {"/boot/initrds", {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}},
               {"/boot", {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}},
           }}}}},
    {"exempli-gratia",
     klapki::state::stated_config{0x0020,
                                  {"owo"},
                                  {{0x000B,
                                    {0x7C, 0x72, 0x2D, 0xF4, 0x1B, 0x78, 0xEF, 0xC7, 0xC4, 0x07, 0x20, 0xBA, 0x67, 0x54, 0xFC, 0x36, 0x6D, 0x8F, 0xE5, 0x39},
                                    "5.8.0-1-amd64",
                                    "",
                                    "/boot",
                                    {{}}},
                                   {0x000D,
                                    {0xB0, 0x7C, 0x71, 0x35, 0x33, 0xEC, 0x0F, 0x2E, 0x21, 0x3C, 0xBF, 0xA2, 0x46, 0x1F, 0xD7, 0x25, 0xD5, 0x5D, 0xFE, 0xD4},
                                    "5.8.0-1-amd64",
                                    "owo",
                                    "/boot",
                                    {{}}}}}},
     klapki::state::stated_config{
         0x0020,
         {"owo"},
         {{0x000B,
           {0x7C, 0x72, 0x2D, 0xF4, 0x1B, 0x78, 0xEF, 0xC7, 0xC4, 0x07, 0x20, 0xBA, 0x67, 0x54, 0xFC, 0x36, 0x6D, 0x8F, 0xE5, 0x39},
           "5.8.0-1-amd64",
           "",
           "/boot",
           {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
           {{{}, {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}}}},
          {0x000D,
           {0xB0, 0x7C, 0x71, 0x35, 0x33, 0xEC, 0x0F, 0x2E, 0x21, 0x3C, 0xBF, 0xA2, 0x46, 0x1F, 0xD7, 0x25, 0xD5, 0x5D, 0xFE, 0xD4},
           "5.8.0-1-amd64",
           "owo",
           "/boot",
           {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00},
           {{{}, {0x13, 0x12, 0x11, 0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}}}}}}},
};