~nabijaczleweli/klapki

1fee319d55dfd63d66122d036e426fb691f60ad2 — наб 11 months ago cb46a59
https://twitter.com/nabijaczleweli/status/1307513189064740864
M src/config.cpp => src/config.cpp +1 -1
@@ 168,7 168,7 @@ std::variant<klapki::config, std::string> klapki::config::read(const char ** arg
	}

	if(ops.empty())
		fmt::print("{}: no operations given, committing cleanly instead.", argv0);  // TODO: remove this maybe?
		fmt::print("{}: no operations given, committing cleanly instead.\n", argv0);  // TODO: remove this maybe?

	return config{argv0, verbose, commit, TRY(find_esp()), std::move(ops), get_host(), get_wisom_root()};
}

M src/config.hpp => src/config.hpp +1 -0
@@ 39,6 39,7 @@ namespace klapki {
		bool verbose;
		bool commit;
		/// Tries /boot/efi, or /boot if that doesn't exist. Must be a mount-point.
		// TODO: nobody's using the device part now
		std::pair<dev_t, const char *> esp;
		std::vector<ops::op_t> ops;


M src/context_commit.cpp => src/context_commit.cpp +6 -5
@@ 32,8 32,9 @@
#include <unistd.h>


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


/// https://stackoverflow.com/a/2180157/2851815 says FreeBSD and Darwin have fcopyfile(3),


@@ 52,7 53,7 @@ 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));

	auto destfd = openat(destdir, basename, O_WRONLY | O_CREAT | O_TRUNC);
	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));
	klapki::quickscope_wrapper destfd_deleter{[&] {


@@ 73,7 74,7 @@ static std::optional<std::string> copy_file(int srcdir, int destdir, const char 


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

	std::map<std::string_view, int> esp_dirs;
	quickscope_wrapper esp_dirs_deleter{[&] {


@@ 191,7 192,7 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
	}


// TODO: prune unknown dirs
	// TODO: delete old files from delkernel here


	return {};

M src/context_derive.cpp => src/context_derive.cpp +3 -3
@@ 60,7 60,7 @@ klapki::context::context klapki::context::context::derive(state::state & output_
			{
				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
				fmt::print("Entry {:04X}: {}\n", went.bootnum_hint, pp.c_str());  // verbose
			}

			kern.device = dp->hd;


@@ 103,10 103,10 @@ klapki::context::context klapki::context::context::derive(state::state & output_

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

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

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

			unsigned char * opt_data;
			size_t opt_data_len;

M src/context_save.cpp => src/context_save.cpp +45 -10
@@ 25,6 25,7 @@
#include <algorithm>
// #include <fcntl.h>
#include <openssl/sha.h>
#include <ucs2.h>
extern "C" {
#include <efivar/efiboot.h>
}


@@ 39,7 40,7 @@ static constexpr bool isslash(char c) {


std::optional<std::string> klapki::context::context::save(const config & cfg, state::state & state) {
	const auto esp_blockdev = fmt::format("/dev/block/{}:0", major(cfg.esp.first));
	// const auto esp_blockdev = fmt::format("/dev/block/{}:0", major(cfg.esp.first));

	for(auto && [bootnum, kern] : this->our_kernels) {
		auto skern = std::find_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),


@@ 50,8 51,8 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st
		if(bent == std::end(state.entries))
			throw __func__;

		std::vector<std::uint8_t> devpath;
		auto image_path = fmt::format("{}{}{}", skern->kernel_dirname, isslash(skern->kernel_dirname.back()) ? "" : "\\", kern.image_path.second);
		auto image_path =
		    fmt::format("{}/{}{}{}", cfg.esp.second, kern.image_path.first, isslash(kern.image_path.first.back()) ? "" : "\\", kern.image_path.second);
		image_path.erase(std::remove_if(std::begin(image_path), std::end(image_path),
		                                [prev = false](auto c) mutable {
			                                auto cur = isslash(c);


@@ 63,8 64,10 @@ 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);
		std::vector<std::uint8_t> devpath;
		do {
			devpath.resize(devpath.size() + 512);



@@ 74,10 77,17 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st
			// 	      int partition,
			// 	      const char *relpath,
			// 	      uint32_t options, ...)
			if(auto size = efi_generate_file_device_path_from_esp(devpath.data(), devpath.size(),              //
			                                                      esp_blockdev.c_str(), minor(cfg.esp.first),  //
			                                                      image_path.c_str(),                          //
			                                                      0);
			// if(auto size = efi_generate_file_device_path_from_esp(devpath.data(), devpath.size(),              //
			//                                                       esp_blockdev.c_str(), minor(cfg.esp.first),  //
			//                                                       image_path.c_str(),                          //
			//                                                       0);

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


@@ 85,6 95,31 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st
		} while(errno == ENOSPC);


		// TODO: put at end instead maybe?
		std::string templine{};
		std::string_view prev = kern.image_path.first;
		for(auto && ipath : kern.initrd_paths) {
			templine += "initrd=";
			if(ipath.first)
				prev = *ipath.first;
			templine += prev;
			if(prev.back() != '\\' && ipath.second.front() != '\\')
				templine += '\\';
			templine += ipath.second;
			templine += ' ';
		}
		templine += kern.cmdline;
		fmt::print("templine={}\n", templine);

		std::vector<std::uint16_t> cmdline(utf8len(reinterpret_cast<std::uint8_t *>(templine.data()), templine.size()));
		fmt::print("cmdline.size()={}\n", cmdline.size());
		if(utf8_to_ucs2(cmdline.data(), cmdline.size() * sizeof(std::uint16_t), false, reinterpret_cast<std::uint8_t *>(templine.data())) < 0)
			return fmt::format("formatting cmdline: {}", strerror(errno));
		fmt::print("cmdline=");
		for(int i = 0; i < cmdline.size() * sizeof(std::uint16_t); ++i)
			fmt::print("{:02X}", reinterpret_cast<std::uint8_t *>(cmdline.data())[i]);
		fmt::print("\n");

		// extern ssize_t efi_loadopt_create(uint8_t *buf, ssize_t size,
		//				  uint32_t attributes, efidp dp,
		//				  ssize_t dp_size, unsigned char *description,


@@ 94,14 129,14 @@ std::optional<std::string> klapki::context::context::save(const config & cfg, st
		                                                  bent->second.attributes,                                         //
		                                                  reinterpret_cast<efidp_data *>(devpath.data()), devpath.size(),  //
		                                                  reinterpret_cast<unsigned char *>(kern.description.data()),      //
		                                                  reinterpret_cast<std::uint8_t *>(kern.cmdline.data()), kern.cmdline.size());
		                                                  reinterpret_cast<std::uint8_t *>(cmdline.data()), cmdline.size() * sizeof(std::uint16_t));

		bent->second.load_option = std::shared_ptr<std::uint8_t[]>{new std::uint8_t[bent->second.load_option_len + 512]};
		bent->second.load_option = std::shared_ptr<std::uint8_t[]>{new std::uint8_t[bent->second.load_option_len]};
		if(efi_loadopt_create(bent->second.load_option.get(), bent->second.load_option_len,    //
		                      bent->second.attributes,                                         //
		                      reinterpret_cast<efidp_data *>(devpath.data()), devpath.size(),  //
		                      reinterpret_cast<unsigned char *>(kern.description.data()),      //
		                      reinterpret_cast<std::uint8_t *>(kern.cmdline.data()), kern.cmdline.size()) < 0)
		                      reinterpret_cast<std::uint8_t *>(cmdline.data()), cmdline.size() * sizeof(std::uint16_t)) < 0)
			return fmt::format("Making load option for {:04X}: {}", bootnum, strerror(errno));

		// write(open("oot", O_WRONLY | O_CREAT | O_DSYNC | O_TRUNC), bent->second.load_option.get(), bent->second.load_option_len);

M src/context_state.cpp => src/context_state.cpp +1 -1
@@ 89,7 89,7 @@ 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");  // verbose
				             fmt::print("{:04X} matches\n", went.bootnum_hint);  // verbose
				             return true;
			             } else
				             fmt::print(stderr, "Boot entry {:04X}: mismatched invalid hash, searching elsewhere\n", went.bootnum_hint);

M src/main.cpp => src/main.cpp +3 -5
@@ 38,7 38,7 @@
	})

#define TRY_OPT(ec, pref, ...)                         \
	if(auto err = context.age(cfg, output_state); err) { \
	if(auto err = __VA_ARGS__; err) { \
		fmt::print("{}: {}\n", pref, *err);                \
		return ec;                                         \
	}


@@ 59,7 59,6 @@ namespace klapki {

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


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


@@ 91,6 90,8 @@ namespace klapki {
				fmt::print("Committed:");
				op::execute(klapki::ops::dump{}, cfg, output_state, context);
			}

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

		return 0;


@@ 100,6 101,3 @@ namespace klapki {
int main(int, const char ** argv) {
	return klapki::main(argv);
}

// int err = efi_set_variable(klapki::efi_guid_klapki, us, (std::uint8_t *)us, strlen(us),
//                            EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, 0);

M src/ops_execute.cpp => src/ops_execute.cpp +2 -1
@@ 107,7 107,8 @@ std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & d
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::addvariant & av, const klapki::config &, klapki::state::state & state, context::context &) {
	if(av.variant[0] == '\0' || std::find(std::begin(state.statecfg.variants), std::end(state.statecfg.variants), av.variant) != std::end(state.statecfg.variants))
	if(av.variant[0] == '\0' ||
	   std::find(std::begin(state.statecfg.variants), std::end(state.statecfg.variants), av.variant) != std::end(state.statecfg.variants))
		fmt::print(stderr, "addvariant: boot variant {} already known\n", av.variant[0] == '\0' ? "(default)" : av.variant);
	else
		state.statecfg.variants.push_back(av.variant);

M src/state.hpp => src/state.hpp +26 -16
@@ 31,6 31,10 @@
#include <variant>
#include <vector>

namespace klapki {
	struct config;
}


namespace klapki::state {
	using sha_t             = std::uint8_t[20];


@@ 75,8 79,11 @@ namespace klapki::state {
		/// 0 on OK, errno on error
		static int parse(stated_config & into, const void * data, std::size_t size);

		std::vector<std::uint8_t> serialise() const;


		bool operator==(const stated_config & other) const noexcept;
		constexpr bool operator!=(const stated_config & other) const noexcept { return !(*this == other); }
	};

	struct state {


@@ 84,6 91,8 @@ 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);

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


@@ 137,24 146,25 @@ struct fmt::formatter<klapki::state::stated_config_entry> {
	auto format(const klapki::state::stated_config_entry & ent, FormatContext & ctx) {
		auto out = ctx.out();

		out = format_to(out, "{{ {:04X}, \"{}\", {}{}{}, \"{}\", [", ent.bootnum_hint,                                                                        //
		out = format_to(out, "{{ {:04X}, \"{}\", {}{}{}, \"{}\", [",                                                                                          //
		                ent.bootnum_hint,                                                                                                                     //
		                ent.version,                                                                                                                          //
		                ent.variant.empty() ? "" : "\"", ent.variant.empty() ? "(default)" : std::string_view{ent.variant}, ent.variant.empty() ? "" : "\"",  //
		                "ent.kernel_dirname");

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

		// 	if(el)
		// 		*out++ = '\"';
		// 	out = format_to(out, "{}", el ? std::string_view{*el} : "←");
		// 	if(el)
		// 		*out++ = '\"';
		// }
		                ent.kernel_dirname);

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

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

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


A src/state_commit.cpp => src/state_commit.cpp +100 -0
@@ 0,0 1,100 @@
// 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.


#include "config.hpp"
#include "efi.hpp"
#include "state.hpp"
#include <fmt/format.h>
extern "C" {
#include <efivar/efivar.h>
// Doesn't include efivar.h:
#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) {
	if(this->statecfg != original_state.statecfg) {
		if(cfg.verbose)
			fmt::print("Updating state config…\n");

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

		if(efi_set_variable(klapki::efi_guid_klapki, cfg.host(), 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));
	}


	TRY_OPT(std::visit(klapki::overload{
	                       [&](const klapki::state::boot_order_flat & now, const klapki::state::boot_order_flat & orig) -> std::optional<std::string> {
		                       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");

		                       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;
	                       },
	                       [&](const auto & a, const auto & b) -> std::optional<std::string> {
		                       fmt::print("{}  {}\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));


	const auto max_bootnum = std::max(this->entries.rbegin()->first, original_state.entries.rbegin()->first);
	for(std::size_t i = 0; i <= max_bootnum; ++i) {
		const auto now  = this->entries.find(i);
		const auto orig = original_state.entries.find(i);

		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
			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));
		} 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);
		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));
	}

	return {};
}

R src/state_parse.cpp => src/state_config.cpp +37 -0
@@ 21,6 21,7 @@


#include "state.hpp"
#include <algorithm>
#include <arpa/inet.h>
#include <fmt/format.h>



@@ 130,3 131,39 @@ end:

	return 0;
}


std::vector<std::uint8_t> klapki::state::stated_config::serialise() const {
	std::vector<std::uint8_t> ret;

	auto bp  = htons(this->boot_position);
	auto cur = std::copy(reinterpret_cast<std::uint8_t *>(&bp), reinterpret_cast<std::uint8_t *>(&bp) + sizeof(bp), std::back_inserter(ret));

	for(auto && var : this->variants)
		cur = std::copy(var.data(), var.data() + var.size() + 1, cur);
	*cur++ = '\0';

	for(auto && went : this->wanted_entries) {
		auto bh = htons(went.bootnum_hint);

		cur    = std::copy(reinterpret_cast<std::uint8_t *>(&bh), reinterpret_cast<std::uint8_t *>(&bh) + sizeof(bh), cur);
		cur    = std::copy(went.load_option_sha, went.load_option_sha + sizeof(sha_t), cur);
		cur    = std::copy(std::begin(went.version), std::end(went.version), cur);
		*cur++ = '\0';
		cur    = std::copy(std::begin(went.variant), std::end(went.variant), cur);
		*cur++ = '\0';
		cur    = std::copy(std::begin(went.kernel_dirname), std::end(went.kernel_dirname), cur);
		*cur++ = '\0';

		for(auto && idir : 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++ = '\0';
	}

	return ret;
}

A test-data/state_parse/exempli-gratia => test-data/state_parse/exempli-gratia +0 -0
M test/state_parse.cpp => test/state_parse.cpp +15 -0
@@ 65,6 65,21 @@ static std::unordered_map<const char *, klapki::state::stated_config> states = {
                                        "/boot/initrds",
                                        "/boot",
                                    }}}}},
    {"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",
                                    {{}}}}}},
};