~nabijaczleweli/klapki

912280bd269e0fd96a5e0591784412fa38cec17e — наб 3 months ago 913953a
variants get properly propagated now
M Makefile => Makefile +1 -1
@@ 30,7 30,7 @@ VERAR := $(foreach l,KLAPKI,-D$(l)_VERSION='$($(l)_VERSION)')
SOURCES := $(sort $(wildcard $(SRCDIR)*.cpp $(SRCDIR)**/*.cpp $(SRCDIR)**/**/*.cpp $(SRCDIR)**/**/**/*.cpp))
TEST_SOURCES := $(sort $(wildcard $(TSTDIR)*.cpp $(TSTDIR)**/*.cpp $(TSTDIR)**/**/*.cpp $(TSTDIR)**/**/**/*.cpp))

# Building with -flto=full means we can't make useful $(ARCH)es, so don't
# Building with -flto on Clang means we can't make useful $(ARCH)es, so don't
LIBFMT := $(patsubst ext/fmt/src/%.cc,$(BLDDIR)fmt/obj/%$(OBJ),$(wildcard ext/fmt/src/*.cc))



M src/config.cpp => src/config.cpp +0 -3
@@ 157,8 157,5 @@ std::variant<klapki::config, std::string> klapki::config::read(const char ** arg
		}
	}

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

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

M src/context.hpp => src/context.hpp +23 -18
@@ 56,17 56,23 @@ namespace klapki::context {
	};

	struct context {
		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;

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

		// TODO: add/remove variants to/from existing kernels
		std::optional<std::string> allocate_kernel_variant(const config & cfg, state::state & state, std::string version, std::string var, std::string image_dir,
		                                                   state::sha_t & image_sha, std::vector<std::pair<state::nonbase_dirname_t, state::shaa_t>> initrd_dirs,
		                                                   std::string image_basename, std::vector<std::pair<state::nonbase_dirname_t, std::string>> initrd_paths);
		template <class F>
		void purge_allocations(state::state & state, F && pred);
		void purge_allocations_impl(state::state & state, const state::stated_config_entry & skern);

		std::optional<std::string> propagate(const config & cfg, state::state & state);
		std::optional<std::string> age(const config & cfg, state::state & state);
		std::optional<std::string> wisen(const config & cfg, state::state & state);
		std::optional<std::string> save(const config & cfg, state::state & state);
		std::optional<std::string> commit(const config & cfg, state::state & state);  // Only updates file SHAs in state
		std::optional<std::string> commit(const config & cfg, state::state & state) const;  // Only updates file SHAs in state

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


@@ 75,6 81,20 @@ namespace klapki::context {
}


template <class F>
void klapki::context::context::purge_allocations(state::state & state, F && pred) {
	state.statecfg.wanted_entries.erase(std::remove_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
	                                                   [&](auto && skern) {
		                                                   if(!pred(skern))
			                                                   return false;

		                                                   this->purge_allocations_impl(state, skern);
		                                                   return true;
	                                                   }),
	                                    std::end(state.statecfg.wanted_entries));
}


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


@@ 83,21 103,6 @@ struct fmt::formatter<klapki::context::context> {
	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: [",

M src/context_age.cpp => src/context_age.cpp +77 -26
@@ 39,6 39,10 @@ extern "C" {
		std::move(std::get<0>(ret));                       \
	})

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


struct fresh_kernel {
	std::string_view version;


@@ 117,6 121,74 @@ namespace {
}


#define COPY_SHA(s) \
	{ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15], s[16], s[17], s[18], s[19] }

std::optional<std::string> klapki::context::context::allocate_kernel_variant(const config & cfg, state::state & state, std::string version, std::string var,
                                                                             std::string kernel_dirname, state::sha_t & kernel_image_sha,
                                                                             std::vector<std::pair<state::nonbase_dirname_t, state::shaa_t>> initrd_dirnames,
                                                                             std::string image_basename,
                                                                             std::vector<std::pair<state::nonbase_dirname_t, std::string>> initrd_paths) {
	// \base\version
	auto efi_base = fmt::format("{}{}\\", cfg.news_efi_dir(), version);

	const auto new_bootnum = TRY(allocate_bootnum(state));
	if(cfg.verbose) {
		if(var.empty())
			fmt::print("  Default variant");
		else
			fmt::print("  Variant {}", var);
		fmt::print(" assigned bootnum {:04X}\n", new_bootnum);
	}


	append_bootorder_entry(state.order, new_bootnum);
	state.statecfg.wanted_entries.emplace_back(state::stated_config_entry{
	    new_bootnum, {0xFF}, std::move(version), std::move(var), std::move(kernel_dirname), COPY_SHA(kernel_image_sha), std::move(initrd_dirnames)});

	state.entries.emplace(new_bootnum,
	                      state::boot_entry{{}, 0, {0x00}, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS});
	this->our_kernels.emplace(new_bootnum, our_kernel{"", "", {std::move(efi_base), std::move(image_basename)}, std::move(initrd_paths)});

	return {};
}

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

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

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

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

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

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


std::optional<std::string> klapki::context::context::age(const config & cfg, state::state & state) {
	std::vector<fresh_kernel> fkerns;
	fkerns.swap(this->fresh_kernels);


@@ 137,33 209,12 @@ std::optional<std::string> klapki::context::context::age(const config & cfg, sta
		for(auto && initrd : fkern.initrds)
			our_initrd_paths.emplace_back(std::nullopt, initrd.second);

		// \base\version
		const auto efi_base    = fmt::format("{}{}\\", cfg.news_efi_dir(), fkern.version);
		const auto variant_cbk = [&](const char * var) -> std::variant<std::monostate, std::string> {
			const auto new_bootnum = TRY(allocate_bootnum(state));
			if(cfg.verbose) {
				if(var[0] == '\0')
					fmt::print("  Default variant");
				else
					fmt::print("  Variant {}", var);
				fmt::print(" assigned bootnum {:04X}\n", new_bootnum);
			}


			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()}, {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});
			this->our_kernels.emplace(new_bootnum, our_kernel{"", "", {efi_base, std::string{fkern.image.second}}, our_initrd_paths});
			// state::stated_config_entry{new_bootnum, {0xFF}, std::string{fkern.version}, var, efi_base, });

			return std::monostate{};
		};
		TRY(variant_cbk(""));
		state::sha_t nilsha{0x00};
		TRY_OPT(this->allocate_kernel_variant(cfg, state, std::string{fkern.version}, "", std::string{image_dir.get()}, nilsha, initrd_dirs,
		                                      std::string{fkern.image.second}, our_initrd_paths));
		for(auto && var : state.statecfg.variants)
			TRY(variant_cbk(var.c_str()));
			TRY_OPT(this->allocate_kernel_variant(cfg, state, std::string{fkern.version}, var, std::string{image_dir.get()}, nilsha, initrd_dirs,
			                                      std::string{fkern.image.second}, our_initrd_paths));
	}

	return {};

M src/context_commit.cpp => src/context_commit.cpp +11 -6
@@ 82,7 82,13 @@ static std::variant<bool, std::string> copy_file(int srcdir, int destdir, const 
	if(!std::memcmp(cursha, insha, sizeof(sha_t))) {
		if(verbose)
			fmt::print("{} unchanged ({})\n", basename, klapki::context::detail::sha_f{cursha});
		return false;

		struct stat dest_sb;  // Still copy if missning
		if(fstatat(destdir, basename, &dest_sb, 0) < 0) {
			if(errno != ENOENT)
				return fmt::format("Couldn't stat() source {}: {}\n", basename, strerror(errno));
		} else
			return false;
	}

	if(verbose)


@@ 109,7 115,7 @@ static std::variant<bool, std::string> copy_file(int srcdir, int destdir, const 
}


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


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


	std::map<std::pair<int, std::string_view>, const std::uint8_t *> already_copied;
	std::map<std::pair<int, std::string>, 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))


@@ 240,14 246,13 @@ std::optional<std::string> klapki::context::context::commit(const config & cfg, 
		if(destfd == std::end(esp_dirs))
			return fmt::format("ESP dir {} not in cache?", ddir);

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

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

M src/context_derive.cpp => src/context_derive.cpp +0 -6
@@ 142,11 142,5 @@ klapki::context::context klapki::context::context::derive(const config & cfg, st
		} 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.insert(went.version);
		else
			throw __func__;  // unreachable, by this point all wanted entries match up to boot entries

	return ret;
}

A src/context_propagate.cpp => src/context_propagate.cpp +75 -0
@@ 0,0 1,75 @@
// 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 "context.hpp"


std::optional<std::string> klapki::context::context::propagate(const config & cfg, state::state & state) {
	std::map<std::string, std::map<std::uint16_t, std::string>> version_variants;  // Can't be string_views because we write over wanted_entries later
	for(auto && went : state.statecfg.wanted_entries)
		version_variants[went.version][went.bootnum_hint] = went.variant;

	for(auto && [ver, vars] : version_variants) {
		const auto baseline_bootnum = vars.begin()->first;

		std::set<std::string_view> missing_variants;

		auto var_cbk = [&, &vars = vars](std::string_view var) {
			if(const auto vitr = std::find_if(std::begin(vars), std::end(vars), [&](auto && vv) { return vv.second == var; }); vitr != std::end(vars))
				vars.extract(vitr);
			else
				missing_variants.emplace(var);
		};
		var_cbk("");
		for(auto && var : state.statecfg.variants)
			var_cbk(var);

		for(auto && mvar : missing_variants)
			if(!vars.empty()) {  // At this point vars has entries which didn't match any currently stated variant. Try to relocate missing variants onto them.
				const auto bootnum = vars.extract(vars.begin()).key();

				const auto went = std::find_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
				                               [&](auto && went) { return went.bootnum_hint == bootnum; });
				if(went == std::end(state.statecfg.wanted_entries))
					throw __func__;

				went->variant = mvar;
			} else {
				const auto baseline_went = std::find_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
				                                        [&](auto && went) { return went.bootnum_hint == baseline_bootnum; });
				if(baseline_went == std::end(state.statecfg.wanted_entries))
					throw __func__;
				const auto baseline_kern = this->our_kernels.find(baseline_bootnum);
				if(baseline_kern == std::end(this->our_kernels))
					throw __func__;

				this->allocate_kernel_variant(cfg, state, std::string{ver}, std::string{mvar}, baseline_went->kernel_dirname, baseline_went->kernel_image_sha,
				                              baseline_went->initrd_dirnames, baseline_kern->second.image_path.second, baseline_kern->second.initrd_paths);
			}

		if(!vars.empty())
			this->purge_allocations(state, [&, &vars = vars](auto && skern) { return vars.find(skern.bootnum_hint) != std::end(vars); });
	}

	return {};
}

M src/main.cpp => src/main.cpp +13 -13
@@ 68,35 68,33 @@ namespace klapki {
		}


		// TODO: reap, propagate variants

		TRY_OPT(5, "context.propagate()", context.propagate(cfg, output_state));
		if(cfg.verbose) {
			fmt::print("Propagated:");
			op::execute(klapki::ops::dump{}, cfg, output_state, context);
		}

		TRY_OPT(5, "context.age()", context.age(cfg, output_state));
		TRY_OPT(6, "context.age()", context.age(cfg, output_state));
		if(cfg.verbose) {
			fmt::print("Aged:");
			op::execute(klapki::ops::dump{}, cfg, output_state, context);
		}

		TRY_OPT(6, "context.wisen()", context.wisen(cfg, output_state));
		TRY_OPT(7, "context.wisen()", context.wisen(cfg, output_state));
		if(cfg.verbose) {
			fmt::print("Wisened:");
			op::execute(klapki::ops::dump{}, cfg, output_state, context);
		}

		TRY_OPT(7, "context.save()", context.save(cfg, output_state));
		TRY_OPT(8, "context.save()", context.save(cfg, output_state));
		if(cfg.verbose) {
			fmt::print("Saved:");
			op::execute(klapki::ops::dump{}, cfg, output_state, context);
		}

		if(cfg.commit) {
			TRY_OPT(8, "context.commit()", context.commit(cfg, output_state));
			if(cfg.verbose) {
				fmt::print("Committed:");
				op::execute(klapki::ops::dump{}, cfg, output_state, context);
			}

			TRY_OPT(8, "output_state.commit()", output_state.commit(cfg.host, input_state));
		if(cfg.commit) {  // Both const, context only updates SHAs which we don't print anyway
			TRY_OPT(9, "context.commit()", context.commit(cfg, output_state));
			TRY_OPT(10, "output_state.commit()", output_state.commit(cfg.host, input_state));
		}

		return 0;


@@ 106,4 104,6 @@ namespace klapki {
int main(int, const char ** argv) try { return klapki::main(argv); } catch(const char * thrown) {
	fmt::print(stderr, "{}\n", thrown);
	throw;
} catch(...) {
	throw;
}

M src/ops_execute.cpp => src/ops_execute.cpp +5 -47
@@ 85,8 85,10 @@ std::optional<std::string> klapki::ops::execute(const klapki::ops::bootpos & bp,
	return {};
}

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)) {
std::optional<std::string> klapki::ops::execute(const klapki::ops::addkernel & ak, const klapki::config &, klapki::state::state & state,
                                                context::context & context) {
	if(std::find_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
	                [&](auto && went) { return went.version == ak.version; }) != std::end(state.statecfg.wanted_entries)) {
		fmt::print(stderr, "addkernel: kernel version {} already known\n", ak.version);
		return {};
	}


@@ 100,62 102,18 @@ 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 {};
}

std::optional<std::string> klapki::ops::execute(const klapki::ops::delkernel & dk, const klapki::config &, klapki::state::state & state,
                                                context::context & context) {
	state.statecfg.wanted_entries.erase(std::remove_if(std::begin(state.statecfg.wanted_entries), std::end(state.statecfg.wanted_entries),
	                                                   [&](auto && skern) {
		                                                   if(skern.version != dk.version)
			                                                   return false;

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

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

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

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

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

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

		                                                   return true;
	                                                   }),
	                                    std::end(state.statecfg.wanted_entries));
	context.purge_allocations(state, [&](auto && skern) { return skern.version == dk.version; });

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

	context.kernel_versions.erase(dk.version);

	return {};
}


M src/state.hpp => src/state.hpp +2 -2
@@ 76,7 76,7 @@ namespace klapki::state {

	struct stated_config {
		std::uint16_t boot_position;        // big-endian
		std::vector<std::string> variants;  // entries NUL-terminated; list empty-terminated
		std::vector<std::string> variants;  // entries NUL-terminated; list empty-terminated; kept unique, order-preserving
		std::vector<stated_config_entry> wanted_entries;

		/// 0 on OK, errno on error


@@ 94,7 94,7 @@ namespace klapki::state {
		std::map<std::uint16_t, boot_entry> entries;
		stated_config statecfg;

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

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

M src/state_commit.cpp => src/state_commit.cpp +3 -2
@@ 35,15 35,16 @@ extern "C" {
		return err;


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

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

		// This does fail if the state is too big. Dunno what to do about it
		if(efi_set_variable(klapki::efi_guid_klapki, us.data(), statecfg_raw.data(), statecfg_raw.size(),
		                    EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, 0600) < 0)
			return fmt::format("Couldn't write new state config: {}", strerror(errno));
			return fmt::format("Couldn't write new state config ({} bytes): {}", statecfg_raw.size(), strerror(errno));
	}



M src/state_config.cpp => src/state_config.cpp +5 -1
@@ 53,7 53,11 @@ int klapki::state::stated_config::parse(klapki::state::stated_config & into, con
		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(std::find(std::begin(into.variants), std::end(into.variants), variant) != std::end(into.variants)) {
			fmt::print(stderr, "Duplicate variant {}?", variant);
			continue;
		}

		if(variant.empty())
			break;