From 69d66ac41ab66a5a5da007ccfacc1d5a9d45d819 Mon Sep 17 00:00:00 2001
From: Bent Bisballe Nyeng <deva@aasimon.org>
Date: Sat, 1 Feb 2025 16:24:07 +0100
Subject: Add PointerList and EnvMap classes for working with, and propagating,
 argc/argv and env strings

---
 src/pointerlist.cc       | 123 ++++++++++++++++++
 src/pointerlist.h        |  74 +++++++++++
 test/ctor.cc             |  17 +++
 test/pointerlist_test.cc | 320 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 534 insertions(+)
 create mode 100644 src/pointerlist.cc
 create mode 100644 src/pointerlist.h
 create mode 100644 test/pointerlist_test.cc

diff --git a/src/pointerlist.cc b/src/pointerlist.cc
new file mode 100644
index 0000000..c0242f7
--- /dev/null
+++ b/src/pointerlist.cc
@@ -0,0 +1,123 @@
+// -*- c++ -*-
+// Distributed under the BSD 2-Clause License.
+// See accompanying file LICENSE for details.
+#include "pointerlist.h"
+
+#include <cstring>
+
+PointerList::PointerList(int argc, const char* const argv[])
+{
+	for(int i = 0; i < argc; ++i)
+	{
+		push_back(argv[i]);
+	}
+}
+
+std::pair<int, const char* const*> PointerList::get()
+{
+	argptrs.clear();
+	for(const auto& arg : *this)
+	{
+		argptrs.push_back(arg.data());
+	}
+	argptrs.push_back(nullptr);
+
+	return {argptrs.size() - 1, // size not counting the nullptr at the end
+	        argptrs.data()};
+}
+
+EnvMap::EnvMap(const char* const env[])
+{
+	if(env == nullptr)
+	{
+		return;
+	}
+
+	auto ptr = env;
+	while(*ptr)
+	{
+		insert(std::string_view(*ptr));
+		++ptr;
+	}
+}
+
+EnvMap::EnvMap(const char* env)
+{
+	if(env == nullptr)
+	{
+		return;
+	}
+
+	auto ptr = env;
+	while(*ptr)
+	{
+		insert(ptr);
+		ptr += strlen(ptr) + 1;
+	}
+}
+
+std::string EnvMap::operator[](const std::string& key) const
+{
+	try
+	{
+		return data.at(key);
+	}
+	catch(...)
+	{
+		return {};
+	}
+}
+
+void EnvMap::clear()
+{
+	data.clear();
+}
+
+void EnvMap::insert(std::string_view key_value)
+{
+	auto equals_sign = key_value.find('=');
+	if(equals_sign == std::string::npos)
+	{
+		insert({key_value, ""});
+		return;
+	}
+	std::string key{key_value.substr(0, equals_sign)};
+	std::string value{key_value.substr(equals_sign + 1)}; // skip '='
+	insert({key, value});
+}
+
+void EnvMap::insert(const std::pair<std::string_view, std::string_view>& item)
+{
+	data[std::string(item.first)] = item.second;
+}
+
+bool EnvMap::contains(std::string_view key) const
+{
+	return data.contains(std::string{key});
+}
+
+std::size_t EnvMap::size() const
+{
+	return data.size();
+}
+
+std::string EnvMap::stringify() const
+{
+	std::string str;
+	for(const auto& [key, value] : data)
+	{
+		str += key + "=" + value + '\0';
+	}
+	str += '\0';
+	return str;
+}
+
+std::pair<int, const char* const*> EnvMap::get()
+{
+	pointerlist.clear();
+	for(const auto& [key, value] : data)
+	{
+		pointerlist.push_back(key + "=" + value + '\0');
+	}
+	return pointerlist.get();
+}
diff --git a/src/pointerlist.h b/src/pointerlist.h
new file mode 100644
index 0000000..988bb26
--- /dev/null
+++ b/src/pointerlist.h
@@ -0,0 +1,74 @@
+// -*- c++ -*-
+// Distributed under the BSD 2-Clause License.
+// See accompanying file LICENSE for details.
+#pragma once
+
+#include <string>
+#include <vector>
+#include <deque>
+#include <utility>
+#include <map>
+
+//! Maintains an (owning) list of string args and converts them to argc/argv
+//! compatible arguments on request.
+//! The returned pointers are guaranteed to be valid as long as the PointerList
+//! object lifetime is not exceeded.
+class PointerList
+	: public std::deque<std::string>
+{
+public:
+	PointerList() = default;
+	PointerList(int argc, const char* const argv[]);
+
+	//! Returns argc/argv pair from the current list of args
+	//! The argv entry after the last is a nullptr (not included in the argc)
+	std::pair<int, const char* const*> get();
+
+private:
+	std::vector<const char*> argptrs;
+};
+
+
+//! Maintains an owning map of strings representing the env.
+class EnvMap
+{
+public:
+	EnvMap() = default;
+
+	//! Initialize from an array of pointers to key=value\0 strings terminated
+	//! by \0
+	EnvMap(const char* const env[]);
+
+	//! Initialize from a string of the format
+	//!  key1=val\0key2=val\0...keyN=val\0\0
+	EnvMap(const char* env);
+
+	std::string operator[](const std::string& key) const;
+
+	//! Clear all items in the map
+	void clear();
+
+	//! Insert string from format: key=value
+	void insert(std::string_view key_value);
+
+	//! Regular map insert
+	void insert(const std::pair<std::string_view, std::string_view>& item);
+
+	//! Checks if the container contains element with specific key
+	bool contains(std::string_view key) const;
+
+	std::size_t size() const;
+
+	//! Return string with the following format:
+	//!  key1=val\0key2=val\0...keyN=val\0\0
+	std::string stringify() const;
+
+	//! Returns the map as argc/argv pair where each pointer points to a string
+	//! of the format key=value\0 and is terminated with a nullptr
+	std::pair<int, const char* const*> get();
+
+private:
+	std::map<std::string, std::string> data{};
+
+	PointerList pointerlist;
+};
diff --git a/test/ctor.cc b/test/ctor.cc
index d69b1cf..acb232f 100644
--- a/test/ctor.cc
+++ b/test/ctor.cc
@@ -9,6 +9,23 @@ ctor::build_configurations ctorTestConfigs(const ctor::settings& settings)
 {
 	return
 	{
+		{
+			.type = ctor::target_type::unit_test,
+			.system = ctor::output_system::build,
+			.target = "pointerlist_test",
+			.sources = {
+				"pointerlist_test.cc",
+				"testmain.cc",
+				"../src/pointerlist.cc",
+			},
+			.flags = {
+				.cxxflags = {
+					"-std=c++20", "-O3", "-Wall", "-Werror",
+					"-I../src", "-Iuunit",
+					"-DOUTPUT=\"pointerlist\"",
+				},
+			},
+		},
 		{
 			.type = ctor::target_type::unit_test,
 			.system = ctor::output_system::build,
diff --git a/test/pointerlist_test.cc b/test/pointerlist_test.cc
new file mode 100644
index 0000000..4274473
--- /dev/null
+++ b/test/pointerlist_test.cc
@@ -0,0 +1,320 @@
+// -*- c++ -*-
+// Distributed under the BSD 2-Clause License.
+// See accompanying file LICENSE for details.
+#include <uunit.h>
+
+#include <set>
+#include <algorithm>
+
+#include "pointerlist.h"
+
+class PointerListTest
+	: public uUnit
+{
+public:
+	PointerListTest()
+	{
+		uTEST(PointerListTest::test_zom_pointerlist_push);
+		uTEST(PointerListTest::test_zom_pointerlist_from_args);
+		uTEST(PointerListTest::test_zom_envmap_insert);
+		uTEST(PointerListTest::test_zom_envmap_from_env);
+		uTEST(PointerListTest::test_exceptional_env);
+	}
+
+	void test_zom_pointerlist_push()
+	{
+		using namespace std::string_literals;
+
+		{ // Zero
+			PointerList args;
+			uASSERT_EQUAL(0u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(0, argc);
+			uASSERT(nullptr != argv);
+			uASSERT_EQUAL(nullptr, argv[0]);
+		}
+
+		{ // One
+			PointerList args;
+			args.push_back("hello");
+			uASSERT_EQUAL(1u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello"s, std::string(argv[0]));
+		}
+
+		{ // Many
+			PointerList args;
+			args.push_back("hello");
+			args.push_back("dear");
+			args.push_back("world");
+			uASSERT_EQUAL(3u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(3, argc);
+			uASSERT_EQUAL("hello"s, std::string(argv[0]));
+			uASSERT_EQUAL("dear"s, std::string(argv[1]));
+			uASSERT_EQUAL("world"s, std::string(argv[2]));
+		}
+	}
+
+	void test_zom_pointerlist_from_args()
+	{
+		using namespace std::string_literals;
+
+		{ // Zero
+			PointerList args(0, nullptr);
+			uASSERT_EQUAL(0u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(0, argc);
+			uASSERT(nullptr != argv);
+			uASSERT_EQUAL(nullptr, argv[0]);
+		}
+
+		{ // One
+			int _argc{1};
+			const char* _argv[] = { "hello" };
+			PointerList args(_argc, _argv);
+			uASSERT_EQUAL(1u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello"s, std::string(argv[0]));
+		}
+
+		{ // Many
+			int _argc{3};
+			const char* _argv[] = { "hello", "dear", "world" };
+			PointerList args(_argc, _argv);
+			uASSERT_EQUAL(3u, args.size());
+			auto [argc, argv] = args.get();
+			uASSERT_EQUAL(3, argc);
+			// order must be preserved
+			uASSERT_EQUAL("hello"s, std::string(argv[0]));
+			uASSERT_EQUAL("dear"s, std::string(argv[1]));
+			uASSERT_EQUAL("world"s, std::string(argv[2]));
+		}
+	}
+
+	void test_zom_envmap_insert()
+	{
+		using namespace std::string_literals;
+
+		{ // Zero
+			EnvMap env;
+			uASSERT_EQUAL(0u, env.size());
+
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(0, argc);
+			uASSERT(nullptr != argv);
+			uASSERT_EQUAL(nullptr, argv[0]);
+
+			auto str = env.stringify();
+			uASSERT_EQUAL(1u, str.size());
+			uASSERT_EQUAL('\0', str[0]);
+		}
+
+		{ // One (key only)
+			EnvMap env;
+			env.insert("hello");
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello="s, std::string(argv[0]));
+			uASSERT_EQUAL(""s, env["hello"]);
+		}
+		{ // One (with value)
+			EnvMap env;
+			env.insert("hello=world");
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello=world"s, std::string(argv[0]));
+			uASSERT_EQUAL("world"s, env["hello"]);
+
+			uASSERT_EQUAL("hello=world\0\0"s, env.stringify());
+		}
+
+		{ // Overwrite one
+			EnvMap env;
+			env.insert("hello=world");
+			uASSERT_EQUAL(1u, env.size());
+			uASSERT_EQUAL("world"s, env["hello"s]);
+
+			env.insert("hello=foo");
+			uASSERT_EQUAL(1u, env.size());
+			uASSERT_EQUAL("foo"s, env["hello"s]);
+		}
+
+		{ // Many
+			EnvMap env;
+			env.insert("hello=world");
+			env.insert("world=leader");
+			env.insert("dear=boar");
+			uASSERT_EQUAL(3u, env.size());
+
+			uASSERT_EQUAL("boar"s, env["dear"s]);
+			uASSERT_EQUAL("world"s, env["hello"s]);
+			uASSERT_EQUAL("leader"s, env["world"s]);
+
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(3, argc);
+			// store and sort to verify unordered
+			std::vector<std::string> vals{argv[0], argv[1], argv[2]};
+			std::ranges::sort(vals);
+			uASSERT_EQUAL("dear=boar"s, vals[0]);
+			uASSERT_EQUAL("hello=world"s, vals[1]);
+			uASSERT_EQUAL("world=leader"s, vals[2]);
+
+			// test all combinations since ordering is not a requirement
+			// exactly one of the must be true (boolean XOR)
+			auto str = env.stringify();
+			uASSERT(((((("dear=boar\0hello=world\0world=leader\0\0"s == str) !=
+			            ("dear=boar\0world=leader\0hello=world\0\0"s == str)) !=
+			           ("hello=world\0dear=boar\0world=leader\0\0"s == str)) !=
+			          ("hello=world\0world=leader\0dear=boar\0\0"s == str)) !=
+			         ("world=leader\0dear=boar\0hello=world\0\0"s == str)) !=
+			        ("world=leader\0hello=world\0dear=boar\0\0"s == str));
+		}
+	}
+
+	void test_zom_envmap_from_env()
+	{
+		using namespace std::string_literals;
+
+		{ // Zero
+			const char* penv = nullptr;
+			EnvMap env(penv);
+			uASSERT_EQUAL(0u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(0, argc);
+			uASSERT(nullptr != argv);
+			uASSERT_EQUAL(nullptr, argv[0]);
+		}
+
+		{ // Zero
+			const char** ppenv = nullptr;
+			EnvMap env(ppenv);
+			uASSERT_EQUAL(0u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(0, argc);
+			uASSERT(nullptr != argv);
+			uASSERT_EQUAL(nullptr, argv[0]);
+		}
+
+		{ // One (key only)
+			const char* ptr = "hello\0\0";
+			EnvMap env(ptr);
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello="s, std::string(argv[0]));
+			uASSERT_EQUAL(""s, env["hello"]);
+		}
+
+		{ // One (key only)
+			const char* ptr[] = {"hello", nullptr};
+			EnvMap env(ptr);
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello="s, std::string(argv[0]));
+			uASSERT_EQUAL(""s, env["hello"]);
+		}
+
+		{ // One (with value)
+			const char* ptr = "hello=world\0\0";
+			EnvMap env(ptr);
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello=world"s, std::string(argv[0]));
+			uASSERT_EQUAL("world"s, env["hello"]);
+
+			uASSERT_EQUAL("hello=world\0\0"s, env.stringify());
+		}
+
+		{ // One (with value)
+			const char* ptr[] = {"hello=world\0", nullptr};
+			EnvMap env(ptr);
+			uASSERT_EQUAL(1u, env.size());
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(1, argc);
+			uASSERT_EQUAL("hello=world"s, std::string(argv[0]));
+			uASSERT_EQUAL("world"s, env["hello"]);
+
+			uASSERT_EQUAL("hello=world\0\0"s, env.stringify());
+		}
+
+		{ // Many
+			const char* ptr = "hello=world\0world=leader\0dear=boar\0\0";
+			EnvMap env(ptr);
+			uASSERT_EQUAL(3u, env.size());
+
+			uASSERT_EQUAL("boar"s, env["dear"s]);
+			uASSERT_EQUAL("world"s, env["hello"s]);
+			uASSERT_EQUAL("leader"s, env["world"s]);
+
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(3, argc);
+			// store and sort to verify unordered
+			std::vector<std::string> vals{argv[0], argv[1], argv[2]};
+			std::ranges::sort(vals);
+			uASSERT_EQUAL("dear=boar"s, vals[0]);
+			uASSERT_EQUAL("hello=world"s, vals[1]);
+			uASSERT_EQUAL("world=leader"s, vals[2]);
+
+			// test all combinations since ordering is not a requirement
+			// exactly one of the must be true (boolean XOR)
+			auto str = env.stringify();
+			uASSERT(((((("dear=boar\0hello=world\0world=leader\0\0"s == str) !=
+			            ("dear=boar\0world=leader\0hello=world\0\0"s == str)) !=
+			           ("hello=world\0dear=boar\0world=leader\0\0"s == str)) !=
+			          ("hello=world\0world=leader\0dear=boar\0\0"s == str)) !=
+			         ("world=leader\0dear=boar\0hello=world\0\0"s == str)) !=
+			        ("world=leader\0hello=world\0dear=boar\0\0"s == str));
+		}
+
+		{ // Many
+			const char* ptr[] =
+				{"hello=world\0", "world=leader\0", "dear=boar\0", nullptr};
+			EnvMap env(ptr);
+			uASSERT_EQUAL(3u, env.size());
+
+			uASSERT_EQUAL("boar"s, env["dear"s]);
+			uASSERT_EQUAL("world"s, env["hello"s]);
+			uASSERT_EQUAL("leader"s, env["world"s]);
+
+			auto [argc, argv] = env.get();
+			uASSERT_EQUAL(3, argc);
+			// store and sort to verify unordered
+			std::vector<std::string> vals{argv[0], argv[1], argv[2]};
+			std::ranges::sort(vals);
+			uASSERT_EQUAL("dear=boar"s, vals[0]);
+			uASSERT_EQUAL("hello=world"s, vals[1]);
+			uASSERT_EQUAL("world=leader"s, vals[2]);
+
+			// test all combinations since ordering is not a requirement
+			// exactly one of the must be true (boolean XOR)
+			auto str = env.stringify();
+			uASSERT(((((("dear=boar\0hello=world\0world=leader\0\0"s == str) !=
+			            ("dear=boar\0world=leader\0hello=world\0\0"s == str)) !=
+			           ("hello=world\0dear=boar\0world=leader\0\0"s == str)) !=
+			          ("hello=world\0world=leader\0dear=boar\0\0"s == str)) !=
+			         ("world=leader\0dear=boar\0hello=world\0\0"s == str)) !=
+			        ("world=leader\0hello=world\0dear=boar\0\0"s == str));
+		}
+	}
+
+	void test_exceptional_env()
+	{
+		using namespace std::string_literals;
+
+		{ // Zero
+			EnvMap env;
+			uASSERT_EQUAL(0u, env.size());
+			uASSERT_EQUAL(""s, env["foo"]); // lookup of non-existing key
+		}
+	}
+};
+
+// Registers the fixture into the 'registry'
+static PointerListTest test;
-- 
cgit v1.2.3