~nabijaczleweli/klapki

186aa727ed7803950797c70c770d0e30533ff374 — наб 1 year, 9 days ago e946700
You could now read boot entries, if there had been any
M Makefile => Makefile +1 -1
@@ 23,7 23,7 @@
include configMakefile


LDDLLS := fmt efivar crypto
LDDLLS := fmt efivar efiboot crypto
LDAR := $(LNCXXAR) $(foreach l,fmt,-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 README.md => README.md +1 -1
@@ 12,7 12,7 @@ dunno yet

CXX='clang++ -stdlib=libc++' make -j && ./to-zoot owo scp -P 10023 out/klapki nab@127.0.0.1:
libssl-dev
libefivar
libefi{var,boot}-dev

#### From Debian repository


M ext/ucs2.h => ext/ucs2.h +8 -4
@@ 1,4 1,8 @@
//! Borrowed from Debian src:efivar 37-5 at src/ucs2.h
//!
//! Modifications up to the copyright notice and casts on lines 36, 87, 91 and 143 for klapki.
#define UNUSED
#define NONNULL(_)

/*
 * Copyright 2012-2016 Red Hat, Inc.


@@ 28,7 32,7 @@ static inline size_t UNUSED
ucs2len(const void *vs, ssize_t limit)
{
	ssize_t i;
	const uint8_t *s8 = vs;
	const uint8_t *s8 = (const uint8_t *)vs;

	for (i = 0;
	     i < (limit >= 0 ? limit : i+1) && !(s8[0] == 0 && s8[1] == 0);


@@ 79,11 83,11 @@ ucs2_to_utf8(const void * const voidchars, ssize_t limit)
{
	ssize_t i, j;
	unsigned char *ret;
	const uint16_t * const chars = voidchars;
	const uint16_t * const chars = (const uint16_t *)voidchars;

	if (limit < 0)
		limit = ucs2len(chars, -1);
	ret = malloc(limit * 6 + 1);
	ret = (unsigned char *)malloc(limit * 6 + 1);
	if (!ret)
		return NULL;
	memset(ret, 0, limit * 6 +1);


@@ 135,7 139,7 @@ utf8_to_ucs2(void *ucs2void, ssize_t size, int terminate, uint8_t *utf8)
{
	ssize_t req;
	ssize_t i, j;
	uint16_t *ucs2 = ucs2void;
	uint16_t *ucs2 = (uint16_t *)ucs2void;
	uint16_t val16;

	if (!ucs2 && size > 0) {

M src/context.cpp => src/context.cpp +125 -103
@@ 21,113 21,135 @@


#include "context.hpp"
#include "util.hpp"
#include <algorithm>
#include <cstring>
#include <fmt/format.h>
#include <iterator>
#include <numeric>
#include <openssl/sha.h>
#include <string_view>
extern "C" {
#include <efivar/efiboot.h>
#include <efivar/efivar-dp.h>
#include <ucs2.h>
}


using sha_t = std::uint8_t[20];
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);

struct sha_f {
	const std::uint8_t * sha;
};
template <>
struct fmt::formatter<sha_f> {
	constexpr auto parse(format_parse_context & ctx) { return ctx.begin(); }
		auto next = chunk.find_first_of(" \f\n\r\t\v");
		func(chunk.substr(0, next));

	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); });
		chunk = chunk.substr(next);
	}
};


std::variant<klapki::state::state, std::string> klapki::resolve_state_context(const state::state & input_state) {
	std::map<std::uint16_t, state::boot_entry> entries{input_state.entries};
	// TODO: parallellise this
	// [02:06] ((наб *)(войд)())(): argh fuck
	// [02:07] ((наб *)(войд)())(): libc++12 from the LLVM repo doesn't have a working <execution>
	// [02:08] Griwes: lmao the only actually shipping and actually working <execution> in existence is the one in the NVHPC toolkit :vv:
	// [02:08] Griwes: ...well, maybe msvc has something, can't remember if they shipped yet
	std::for_each(std::begin(entries), std::end(entries),
	              [](auto & bent) { SHA1(bent.second.load_option.get(), bent.second.load_option_len, bent.second.load_option_sha); });


	state::stated_config statecfg{input_state.statecfg.boot_position, {}};
	statecfg.wanted_entries.reserve(input_state.statecfg.wanted_entries.size());


	// TODO: uniquify by SHA


	std::vector<std::pair<sha_t, std::uint16_t>> remaps;
	std::copy_if(std::begin(input_state.statecfg.wanted_entries), std::end(input_state.statecfg.wanted_entries), std::back_inserter(statecfg.wanted_entries),
	             [&](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\b");  // verbose
				             return true;
			             } else
				             fmt::print(stderr, "Boot entry {:04X}: mismatched invalid hash, searching elsewhere\n", went.bootnum_hint);
		             } else
			             fmt::print(stderr, "Boot 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)); });
		                bent != end(entries)) {
			             fmt::print(stderr, "Found entry formerly {:04X} at {:04X}.\n", went.bootnum_hint, bent->first);

			             std::pair<sha_t, std::uint16_t> remap;
			             memcpy(remap.first, went.load_option_sha, sizeof(sha_t));
			             remap.second = bent->first;
			             remaps.emplace_back(std::move(remap));

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

	for(auto && [sha, bootnum] : remaps)
		if(auto went = std::find_if(std::begin(statecfg.wanted_entries), std::end(statecfg.wanted_entries),
		                            [&, &sha = sha](auto && went) { return !std::memcmp(went.load_option_sha, sha, sizeof(sha_t)); });
		   went != std::end(statecfg.wanted_entries))
			went->bootnum_hint = bootnum;


	auto boot_order = std::visit(overload{[&](const state::boot_order_flat & bof) {
		                                      state::boot_order_structured ret{};

		                                      bool ours = false;
		                                      std::vector<std::uint16_t> bootnums;
		                                      for(std::uint16_t * cur = bof.order.get(); cur < bof.order.get() + bof.order_cnt; ++cur) {
			                                      bool our = std::find_if(std::begin(statecfg.wanted_entries), std::end(statecfg.wanted_entries), [&](auto && went) {
				                                                 return went.bootnum_hint == *cur;
			                                                 }) != std::end(statecfg.wanted_entries);

			                                      if(our == ours)
				                                      bootnums.emplace_back(*cur);
			                                      else {
				                                      std::vector<std::uint16_t> bns{*cur};
				                                      bns.swap(bootnums);
				                                      ret.order.emplace_back(std::move(bns), ours);
				                                      ours = our;
			                                      }
		                                      }

		                                      if(!bootnums.empty())
			                                      ret.order.emplace_back(std::move(bootnums), ours);

		                                      return ret;
	                                      },
	                                      [](const state::boot_order_structured & bos) { return bos; }},
	                             input_state.order);


	return state::state{std::move(boot_order), std::move(entries), std::move(statecfg)};
}


klapki::context::context klapki::context::context::derive(state::state & output_state) {
	context ret{};

	for(auto && went : output_state.statecfg.wanted_entries)
		if(auto bent = output_state.entries.find(went.bootnum_hint); bent != std::end(output_state.entries)) {
			our_kernel kern{};

			const auto efi_opt        = reinterpret_cast<efi_load_option *>(bent->second.load_option.get());
			const ssize_t efi_opt_len = bent->second.load_option_len;
			if(!efi_loadopt_is_valid(efi_opt, efi_opt_len)) {
				fmt::print(stderr, "Entry {:04X} not a valid EFI Load Option. Dropping.\n", went.bootnum_hint);
				// TODO: drop this from the state entirely
				continue;
			}

			const auto dp = efi_loadopt_path(efi_opt, efi_opt_len);
			if(dp->type != EFIDP_MEDIA_TYPE) {
				fmt::print(stderr, "Entry {:04X} not Media Device Path. Dropping.\n", went.bootnum_hint);
				// TODO: drop this from the state entirely
				continue;
			}
			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", pp.c_str());  // verbose
			}

			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)) {
				case -1:
					fmt::print(stderr, "Entry {:04X}: second (file) node: {}. Dropping.\n", went.bootnum_hint, strerror(errno));
					// TODO: drop this from the state entirely
					continue;
				case 0:
					fmt::print(stderr, "Entry {:04X}: second (file) node doesn't exist. Dropping.\n", went.bootnum_hint);
					// TODO: drop this from the state entirely
					continue;
				case 1: {
					if(dp->type != EFIDP_MEDIA_TYPE) {
						fmt::print(stderr, "Entry {:04X} file not Media Device Path. Dropping.\n", went.bootnum_hint);
						// TODO: drop this from the state entirely
						continue;
					}
					if(dp->subtype != EFIDP_MEDIA_FILE) {
						fmt::print(stderr, "Entry {:04X} file not Media Device Path File. Dropping.\n", went.bootnum_hint);
						// TODO: drop this from the state entirely
						continue;
					}

					const std::unique_ptr<char[], free_deleter> path_c{reinterpret_cast<char *>(ucs2_to_utf8(reinterpret_cast<char16_t *>(dp->file.name), dp->length))};
					std::string_view path{path_c.get()};
					if(const auto last_backslash = path.rfind('\\'); last_backslash != std::string::npos)
						path = path.substr(last_backslash + 1);
					else
						fmt::print(stderr, "Entry {:04X}: kernel image {}: no backslashes? Using as seen.\n", went.bootnum_hint, path);
					kern.image_basename = path;
				} break;
				default:
					__builtin_unreachable();
			}

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

			fmt::print("Entry {:04X}: {}\n", kern.image_basename);  // verbose

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

			unsigned char * opt_data;
			size_t opt_data_len;
			efi_loadopt_optional_data(efi_opt, efi_opt_len, &opt_data, &opt_data_len);
			tokenise_cmdline({reinterpret_cast<char *>(opt_data), opt_data_len}, [&](auto && arg) {
				if(!arg.compare(0, std::strlen("initrd="), "initrd=")) {
					auto initrd_path = arg.substr(std::strlen("initrd="));
					if(const auto last_backslash = initrd_path.find_last_of('\\'); last_backslash != std::string::npos)
						initrd_path = initrd_path.substr(last_backslash + 1);
					else
						fmt::print(stderr, "Entry {:04X}: initrd={}: no backslashes? Using as seen.\n", initrd_path);

					kern.initrd_basenames.emplace_back(initrd_path);
				} else {
					kern.cmdline += arg;
					kern.cmdline += ' ';
				}
			});
			if(!kern.cmdline.empty())
				kern.cmdline.pop_back();  // extra space

			ret.our_kernels.emplace(went.bootnum_hint, std::move(kern));
		} else
			throw __func__;  // unreachable, by this point all wanted entries match up to boot entries

	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);
		else
			throw __func__;  // unreachable, by this point all wanted entries match up to boot entries

	return ret;
}

M src/context.hpp => src/context.hpp +55 -1
@@ 24,9 24,63 @@


#include "state.hpp"
#include <fmt/format.h>
#include <map>
#include <variant>
#include <vector>
extern "C" {
#include <efivar/efiboot.h>
#include <efivar/efivar-dp.h>
}


namespace klapki::context {
	struct our_kernel {
		std::string description;
		std::string cmdline;

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

		std::string image_basename;
		std::vector<std::string> initrd_basenames;
	};

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

		static context derive(state::state & input_state);
	};

namespace klapki {
	std::variant<state::state, std::string> resolve_state_context(const state::state & input_state);
}


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

	template <typename FormatContext>
	auto format(const klapki::context::context & context, FormatContext & ctx) {
		auto out = ctx.out();

		out = format_to(out, "Kernel versions: ");
		if(context.kernel_versions.empty())
			out = format_to(out, "(none)");
		else {
			bool first = true;
			for(auto && el : context.kernel_versions) {
				if(!first)
					out = format_to(out, ", ");
				else
					first = false;

				out = format_to(out, "{}", el);
			}
		}

		out = format_to(out, "\n{} of our kernels", context.our_kernels.size());

		return out;
	}
};

A src/context_state.cpp => src/context_state.cpp +154 -0
@@ 0,0 1,154 @@
// The MIT License (MIT)

// Copyright (c) 2020 nabijaczleweli

// 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.


#include "context.hpp"
#include "util.hpp"
#include <algorithm>
#include <cstring>
#include <fmt/format.h>
#include <iterator>
#include <numeric>
#include <openssl/sha.h>


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};
	// TODO: parallellise this
	// [02:06] ((наб *)(войд)())(): argh fuck
	// [02:07] ((наб *)(войд)())(): libc++12 from the LLVM repo doesn't have a working <execution>
	// [02:08] Griwes: lmao the only actually shipping and actually working <execution> in existence is the one in the NVHPC toolkit :vv:
	// [02:08] Griwes: ...well, maybe msvc has something, can't remember if they shipped yet
	std::for_each(std::begin(entries), std::end(entries),
	              [](auto & bent) { SHA1(bent.second.load_option.get(), bent.second.load_option_len, bent.second.load_option_sha); });


	state::stated_config statecfg{input_state.statecfg.boot_position, input_state.statecfg.variants, {}};
	statecfg.wanted_entries.reserve(input_state.statecfg.wanted_entries.size());


	// Uniquify by SHA
	{
		std::vector<std::pair<std::uint16_t, const std::uint8_t *>> shas;
		shas.reserve(statecfg.wanted_entries.size());
		statecfg.wanted_entries.erase(
		    std::remove_if(std::begin(statecfg.wanted_entries), std::end(statecfg.wanted_entries),
		                   [&](auto && went) {
			                   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,
				                              sha_f{went.load_option_sha}, dupe->first);
				                   return true;
			                   }

			                   shas.emplace_back(went.bootnum_hint, went.load_option_sha);
			                   return false;
		                   }),
		    std::end(statecfg.wanted_entries));
	}


	// Match wanted entries to boot entries
	std::vector<std::pair<sha_t, std::uint16_t>> remaps;
	std::copy_if(std::begin(input_state.statecfg.wanted_entries), std::end(input_state.statecfg.wanted_entries), std::back_inserter(statecfg.wanted_entries),
	             [&](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");  // verbose
				             return true;
			             } else
				             fmt::print(stderr, "Boot entry {:04X}: mismatched invalid hash, searching elsewhere\n", went.bootnum_hint);
		             } else
			             fmt::print(stderr, "Boot 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)); });
		                bent != end(entries)) {
			             fmt::print(stderr, "Found entry formerly {:04X} at {:04X}.\n", went.bootnum_hint, bent->first);

			             std::pair<sha_t, std::uint16_t> remap;
			             memcpy(remap.first, went.load_option_sha, sizeof(sha_t));
			             remap.second = bent->first;
			             remaps.emplace_back(std::move(remap));

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

	for(auto && [sha, bootnum] : remaps)
		if(auto went = std::find_if(std::begin(statecfg.wanted_entries), std::end(statecfg.wanted_entries),
		                            [&, &sha = sha](auto && went) { return !std::memcmp(went.load_option_sha, sha, sizeof(sha_t)); });
		   went != std::end(statecfg.wanted_entries))
			went->bootnum_hint = bootnum;


	// Structure boot order
	auto boot_order = std::visit(overload{[&](const state::boot_order_flat & bof) {
		                                      state::boot_order_structured ret{};

		                                      bool ours = false;
		                                      std::vector<std::uint16_t> bootnums;
		                                      for(std::uint16_t * cur = bof.order.get(); cur < bof.order.get() + bof.order_cnt; ++cur) {
			                                      bool our = std::find_if(std::begin(statecfg.wanted_entries), std::end(statecfg.wanted_entries), [&](auto && went) {
				                                                 return went.bootnum_hint == *cur;
			                                                 }) != std::end(statecfg.wanted_entries);

			                                      if(our == ours)
				                                      bootnums.emplace_back(*cur);
			                                      else {
				                                      std::vector<std::uint16_t> bns{*cur};
				                                      bns.swap(bootnums);
				                                      ret.order.emplace_back(std::move(bns), ours);
				                                      ours = our;
			                                      }
		                                      }

		                                      if(!bootnums.empty())
			                                      ret.order.emplace_back(std::move(bootnums), ours);

		                                      return ret;
	                                      },
	                                      [](const state::boot_order_structured & bos) { return bos; }},
	                             input_state.order);


	return state::state{std::move(boot_order), std::move(entries), std::move(statecfg)};
}

M src/main.cpp => src/main.cpp +4 -2
@@ 35,11 35,13 @@ namespace klapki {

			                           return std::visit(overload{[&](const state::state & input_state) {
				                                                      return std::visit(overload{[&](state::state && output_state) {
					                                                                                 auto context = context::context::derive(output_state);

					                                                                                 for(auto && op : cfg.ops) {
						                                                                                 if(cfg.verbose)
							                                                                                 fmt::print("Running {}\n", klapki::op{op});

						                                                                                 if(auto err = op::execute(op, cfg, output_state); err) {
						                                                                                 if(auto err = op::execute(op, cfg, output_state, context); err) {
							                                                                                 fmt::print("{}\n", *err);
							                                                                                 return 4;
						                                                                                 }


@@ 51,7 53,7 @@ namespace klapki {
					                                                                                 fmt::print("{}\n", errmsg);
					                                                                                 return 3;
				                                                                                 }},
				                                                                        resolve_state_context(input_state));
				                                                                        context::resolve_state_context(input_state));
			                                                      },
			                                                      [](std::string && errmsg) {
				                                                      fmt::print("{}\n", errmsg);

M src/ops.cpp => src/ops.cpp +39 -29
@@ 22,6 22,33 @@

#include "ops.hpp"
#include <cstring>
#include <limits>


template <class T, class N>
static std::variant<klapki::ops::op_t, std::string> pos_op(const char * argv0, const char **& argv, const char * name) {
	char * end;
	// strtoul(3): In particular, if *nptr is not '\0' but **endptr is '\0' on return, the entire string is valid.
	if(!argv[0] || argv[0][0] == '\0')
		return fmt::format("{}: {} requires a position", argv0, name);
	errno               = 0;
	const auto position = argv[0];
	++argv;
	const auto num = std::strtoull(position, &end, 0);
	if(num > std::numeric_limits<N>::max())
		return fmt::format("{}: {} position {} must fit in {} bits", argv0, name, position, sizeof(N) * 8);
	if(*end != '\0')
		return fmt::format("{}: {} position {}: {}", argv0, name, position, errno ? strerror(errno) : "not a number");

	return T{static_cast<N>(num)};
}

template <class T>
static std::variant<klapki::ops::op_t, std::string> vers_op(const char * argv0, const char **& argv, const char * name) {
	if(!argv[0])
		return fmt::format("{}: {} needs version", argv0, name);
	return T{argv++[0]};
}


std::variant<klapki::ops::op_t, std::string> klapki::op::from_cmdline(const char * argv0, const char **& argv) {


@@ 32,40 59,23 @@ std::variant<klapki::ops::op_t, std::string> klapki::op::from_cmdline(const char
	++argv;
	if(!std::strcmp(opname, "dump"))
		return ops::dump{};
	else if(!std::strcmp(opname, "bootpos")) {
		char * end;
		// strtoul(3): In particular, if *nptr is not '\0' but **endptr is '\0' on return, the entire string is valid.
		if(!argv[0] || argv[0][0] == '\0')
			return fmt::format("{}: bootpos requires a position", argv0);
		errno               = 0;
		const auto position = argv[0];
		++argv;
		const auto pos = std::strtoul(position, &end, 0);
		if(pos > 0xFFFF)
			return fmt::format("{}: bootpos position {} must fit in 16 bits", argv0, position);
		if(*end != '\0')
			return fmt::format("{}: bootpos position {}: {}", argv0, position, errno ? strerror(errno) : "not a number");

		return ops::bootpos{static_cast<std::uint16_t>(pos)};
	} else if(!std::strcmp(opname, "addkernel")) {
		if(!argv[0])
			return fmt::format("{}: addkernel needs version", argv0);
		return ops::addkernel{argv++[0]};
	} else if(!std::strcmp(opname, "delkernel")) {
		if(!argv[0])
			return fmt::format("{}: delkernel needs version", argv0);
		return ops::delkernel{argv++[0]};
	} else
	else if(!std::strcmp(opname, "bootpos"))
		return pos_op<ops::bootpos, std::uint16_t>(argv0, argv, "bootpos");
	else if(!std::strcmp(opname, "addkernel"))
		return vers_op<ops::addkernel>(argv0, argv, "addkernel");
	else if(!std::strcmp(opname, "delkernel"))
		return vers_op<ops::delkernel>(argv0, argv, "delkernel");
	else
		return fmt::format("{}: unknown op {}", argv0, opname);
}


std::optional<std::string> klapki::op::execute(const klapki::ops::op_t & op, const config & cfg, state::state & state) {
std::optional<std::string> klapki::op::execute(const klapki::ops::op_t & op, const config & cfg, state::state & state, context::context & context) {
	return std::visit(klapki::overload{
	                      [&](const klapki::ops::dump & d) { return ops::execute(d, cfg, state); },
	                      [&](const klapki::ops::bootpos & bp) { return ops::execute(bp, cfg, state); },
	                      [&](const klapki::ops::addkernel & ak) { return ops::execute(ak, cfg, state); },
	                      [&](const klapki::ops::delkernel & dk) { return ops::execute(dk, cfg, state); },
	                      [&](const klapki::ops::dump & d) { return ops::execute(d, cfg, state, context); },
	                      [&](const klapki::ops::bootpos & bp) { return ops::execute(bp, cfg, state, context); },
	                      [&](const klapki::ops::addkernel & ak) { return ops::execute(ak, cfg, state, context); },
	                      [&](const klapki::ops::delkernel & dk) { return ops::execute(dk, cfg, state, context); },
	                  },
	                  op);
}

M src/ops.hpp => src/ops.hpp +8 -5
@@ 35,6 35,9 @@
namespace klapki {
	struct config;
}
namespace klapki::context {
	struct context;
}


namespace klapki::ops {


@@ 51,10 54,10 @@ namespace klapki::ops {

	using op_t = std::variant<dump, bootpos, addkernel, delkernel>;

	std::optional<std::string> execute(const dump & d, const config & cfg, state::state & state);
	std::optional<std::string> execute(const bootpos & bp, const config & cfg, state::state & state);
	std::optional<std::string> execute(const addkernel & ak, const config & cfg, state::state & state);
	std::optional<std::string> execute(const delkernel & dk, const config & cfg, state::state & state);
	std::optional<std::string> execute(const dump & d, const config & cfg, state::state & state, context::context & context);
	std::optional<std::string> execute(const bootpos & bp, const config & cfg, state::state & state, context::context & context);
	std::optional<std::string> execute(const addkernel & ak, const config & cfg, state::state & state, context::context & context);
	std::optional<std::string> execute(const delkernel & dk, const config & cfg, state::state & state, context::context & context);
}




@@ 63,7 66,7 @@ namespace klapki {
		const ops::op_t & op;

		static std::variant<ops::op_t, std::string> from_cmdline(const char * argv0, const char **& argv);
		static std::optional<std::string> execute(const ops::op_t & op, const config & cfg, state::state & state);
		static std::optional<std::string> execute(const ops::op_t & op, const config & cfg, state::state & state, context::context & context);
	};
	struct mops {
		const std::vector<ops::op_t> & ops;

M src/ops_execute.cpp => src/ops_execute.cpp +15 -9
@@ 21,31 21,37 @@


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


std::optional<std::string> klapki::ops::execute(const klapki::ops::dump &, const klapki::config & cfg, klapki::state::state & state) {
std::optional<std::string> klapki::ops::execute(const klapki::ops::dump &, const klapki::config & cfg, klapki::state::state & state,
                                                context::context & context) {
	fmt::print("\n"
	           "Boot order: {}\n"
	           "{} boot entries\n"
	           "Desired boot position: {}\n"
	           "\n",
	           state.order, state.entries.size(), state.statecfg.boot_position);
	           "  Boot order: {}\n"
	           "  {} boot entries\n"
	           "  Desired boot position: {}\n"
	           "\n"
	           "  {}\n",
	           state.order, state.entries.size(), state.statecfg.boot_position, context);
	return {};
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::bootpos & bp, const klapki::config & cfg, klapki::state::state & state) {
std::optional<std::string> klapki::ops::execute(const klapki::ops::bootpos & bp, const klapki::config & cfg, klapki::state::state & state,
                                                context::context & context) {
	state.statecfg.boot_position = bp.position;
	return {};
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & ak, const klapki::config & cfg, klapki::state::state & state) {
std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & ak, const klapki::config & cfg, klapki::state::state & state,
                                                context::context & context) {
	throw;
	return {};
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & dk, const klapki::config & cfg, klapki::state::state & state) {
std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & dk, const klapki::config & cfg, klapki::state::state & state,
                                                context::context & context) {
	throw;
	return {};
}

M src/state.cpp => src/state.cpp +2 -1
@@ 153,10 153,11 @@ 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->kernel_dirname == other.kernel_dirname &&                               //
	       this->initrd_dirnames == other.initrd_dirnames;
}

bool klapki::state::stated_config::operator==(const klapki::state::stated_config & other) const noexcept {
	return this->boot_position == other.boot_position && this->wanted_entries == other.wanted_entries;
	return this->boot_position == other.boot_position && this->variants == other.variants && this->wanted_entries == other.wanted_entries;
}

M src/state.hpp => src/state.hpp +4 -2
@@ 55,8 55,9 @@ namespace klapki::state {

	struct stated_config_entry {
		/// Hint initially, then pointer
		std::uint16_t bootnum_hint;  // Big-Endian
		std::uint16_t bootnum_hint;  // big-endian
		sha_t load_option_sha;
		std::string version;                             // NUL-terminated
		std::string kernel_dirname;                      // NUL-terminated
		std::vector<nonbase_dirname_t> initrd_dirnames;  // entries NUL-terminated; list empty-terminated



@@ 65,7 66,8 @@ namespace klapki::state {
	};

	struct stated_config {
		std::uint16_t boot_position;  // Big-Endian
		std::uint16_t boot_position;        // big-endian
		std::vector<std::string> variants;  // entries NUL-terminated; list empty-terminated
		std::vector<stated_config_entry> wanted_entries;

		/// 0 on OK, errno on error

M src/state_parse.cpp => src/state_parse.cpp +30 -0
@@ 38,6 38,26 @@ int klapki::state::stated_config::parse(klapki::state::stated_config & into, con
	data += sizeof(into.boot_position);
	size -= sizeof(into.boot_position);

	for(;;) {
		const auto variant_end = std::find_if(data, data + size, [](auto b) { return b == '\0'; });
		if(variant_end == data + size) {
			fmt::print(stderr, "extraneous data; -1\n");
			data = data + size;
			size = 0;
			break;
		}

		std::string variant{data, static_cast<std::size_t>(variant_end - data)};
		data += variant.size() + 1;  // NUL
		size -= variant.size() + 1;  // NUL
		// fmt::print(stderr, "variant: {} ({})\n", variant, variant.size());

		if(variant.empty())
			break;
		else
			into.variants.emplace_back(std::move(variant));
	}

	while(size != 0) {
		stated_config_entry new_entry{};
		if(size <= sizeof(new_entry.bootnum_hint) + sizeof(new_entry.load_option_sha)) {


@@ 54,6 74,16 @@ int klapki::state::stated_config::parse(klapki::state::stated_config & into, con
		data += sizeof(new_entry.load_option_sha);
		size -= sizeof(new_entry.load_option_sha);

		const auto kver_end = std::find_if(data, data + size, [](auto b) { return b == '\0'; });
		if(kver_end == data + size) {
			fmt::print(stderr, "extraneous data; 0.5; have: {:04X}\n", new_entry.bootnum_hint);
			break;
		}
		new_entry.version.assign(data, kver_end - data);
		data += new_entry.version.size() + 1;  // NUL
		size -= new_entry.version.size() + 1;  // NUL
		// fmt::print(stderr, "version: {} ({})\n", new_entry.kernel_dirname, new_entry.kernel_dirname.size());

		const auto kdir_end = std::find_if(data, data + size, [](auto b) { return b == '\0'; });
		if(kdir_end == data + size) {
			fmt::print(stderr, "extraneous data; 1; have: {:04X}\n", new_entry.bootnum_hint);

M src/util.hpp => src/util.hpp +8 -0
@@ 37,4 37,12 @@ namespace klapki {
	};
	template <class... Ts>
	overload(Ts...)->overload<Ts...>;

	/// unique_ptr deleter that uses free(3)
	struct free_deleter {
		template <class T>
		void operator()(T * ptr) const noexcept {
			std::free(ptr);
		}
	};
}

M test-data/state_parse/just-bootplace => test-data/state_parse/just-bootplace +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 +30 -16
@@ 28,28 28,39 @@


static std::unordered_map<const char *, klapki::state::stated_config> states = {
    {"just-bootplace", klapki::state::stated_config{0x0a01, {}}},
    {"just-bootplace", klapki::state::stated_config{0x0a01, {}, {}}},
    {"just-kbase",
     klapki::state::stated_config{
         0x0b02,
         {{0x0102, {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13}, "/boot/", {}}}}},
     klapki::state::stated_config{0x0b02,
                                  {},
                                  {{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",
                                    "/boot/",
                                    {}}}}},
    {"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",
                                    "/boot/",
                                    {"/boot/initrds"}}}}},
    {"one-initrd-copy+two-initrds",
     klapki::state::stated_config{
         0x0b02,
         {{0x0102, {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13}, "/boot/", {{}}},
          {0x0102,
           {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13},
           "/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",
                                    "/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",
                                    "/boot/",
                                    {
                                        "/boot/initrds",
                                        "/boot",
                                    }}}}},
};




@@ 74,14 85,17 @@ TEST_CASE("klapki::state::stated_config::parse() okay", "[klapki::state::stated_
			// 	fmt::print(stderr,
			// 	           "\tbootnum_hint: {}\n"
			// 	           "\tload_option_sha: {}\n"
			// 	           "\tversion: {}\n"
			// 	           "\tkernel_dirname: {}\n"
			// 	           "\tinitrd_dirnames: [{}]\n",
			// 	           ent.bootnum_hint, std::string{(char *)ent.load_option_sha, sizeof(ent.load_option_sha)}, ent.kernel_dirname, ent.initrd_dirnames.size());
			// REQUIRE(incfg.boot_place == cfg.boot_place);
			// 	           ent.bootnum_hint, std::string{(char *)ent.load_option_sha, sizeof(ent.load_option_sha)}, ent.version, ent.kernel_dirname,
			// 	           ent.initrd_dirnames.size());
			// REQUIRE(incfg.boot_position == cfg.boot_position);
			// REQUIRE(incfg.wanted_entries.size() == cfg.wanted_entries.size());
			// for(std::size_t i = 0; i < incfg.wanted_entries.size(); ++i) {
			// 	REQUIRE(incfg.wanted_entries[i].bootnum_hint == cfg.wanted_entries[i].bootnum_hint);
			// 	// REQUIRE(incfg.wanted_entries[i].load_option_sha == cfg.wanted_entries[i].load_option_sha);
			// 	REQUIRE(incfg.wanted_entries[i].version == cfg.wanted_entries[i].version);
			// 	REQUIRE(incfg.wanted_entries[i].kernel_dirname == cfg.wanted_entries[i].kernel_dirname);
			// 	REQUIRE(incfg.wanted_entries[i].initrd_dirnames.size() == cfg.wanted_entries[i].initrd_dirnames.size());
			// 	for(std::size_t n = 0; n < incfg.wanted_entries[i].initrd_dirnames.size(); ++n) {