From 34779155d5b69b51aa301e7c111f95ea9c840589 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Thu, 17 Aug 2023 15:56:11 -0500 Subject: init commit --- .ycm_extra_conf.py | 9 + makefile | 73 +++ src/main.cpp | 202 +++++++ src/wg2sd.cpp | 503 ++++++++++++++++ src/wg2sd.hpp | 134 +++++ test/utest.h | 1623 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/wg2sd_test.cpp | 219 +++++++ 7 files changed, 2763 insertions(+) create mode 100644 .ycm_extra_conf.py create mode 100644 makefile create mode 100644 src/main.cpp create mode 100644 src/wg2sd.cpp create mode 100644 src/wg2sd.hpp create mode 100644 test/utest.h create mode 100644 test/wg2sd_test.cpp diff --git a/.ycm_extra_conf.py b/.ycm_extra_conf.py new file mode 100644 index 0000000..da9fe01 --- /dev/null +++ b/.ycm_extra_conf.py @@ -0,0 +1,9 @@ +def Settings( **kwargs ): + return { + 'flags': [ + '-Wall', '-Wextra', '-Werror', + '-Isrc', '-Itest', + '-std=c++20', + '-Wno-unused-function', + ], + } diff --git a/makefile b/makefile new file mode 100644 index 0000000..c08daf5 --- /dev/null +++ b/makefile @@ -0,0 +1,73 @@ +# Compiler +CXX = g++ + +# Compiler flags +CXXFLAGS = -Wall -Wextra -Werror +CXXFLAGS += -Isrc -std=c++20 -Wno-unused-function + +# Release flags +RELEASE_FLAGS = -O3 -lrt + +# Debug flags +DEBUGFLAGS = -ggdb -O0 + +# Linking flags +LDFLAGS = -largon2 + +# Object files +OBJECTS := wg2sd.o +OBJECTS += main.o + +# Source directory +SRC_DIR = src +TEST_DIR = test + +TEST_FILES := $(wildcard $(TEST_DIR)/*.cpp) +TEST_TARGETS := $(patsubst $(TEST_DIR)/%.cpp, $(TEST_DIR)/%, $(TEST_FILES)) + +SRC_FILES := $(patsubst %.o,$(SRC_DIR)/%.cpp,$(OBJECTS)) + +# Object directory +OBJ_DIR = obj +DEBUG_OBJ_DIR = obj/debug + +# Target executable +TARGET = wg2sd + +# Build rules +all: CXXFLAGS += $(RELEASE_FLAGS) +all: targets + +targets: $(TARGET) + +tests: $(TEST_TARGETS) + +debug: CXXFLAGS += $(DEBUGFLAGS) +debug: OBJ_DIR = $(DEBUG_OBJ_DIR) +debug: tests targets + +$(TARGET): $(addprefix $(OBJ_DIR)/, $(OBJECTS)) + $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +$(TEST_DIR)/%: $(TEST_DIR)/%.cpp $(addprefix $(OBJ_DIR)/, wg2sd.o) | $(OBJ_DIR) + $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ + +$(OBJ_DIR) $(DEBUG_OBJ_DIR): + mkdir -p $@ + +# Clean rule +clean: + rm -rf $(OBJ_DIR) $(DEBUG_OBJ_DIR) $(TARGET) $(TEST_TARGETS) + +# Help rule +help: + @echo "Available targets:" + @echo " all (default) : Build the project" + @echo " release : Build the project with release flags" + @echo " tests : Build the tests" + @echo " debug : Build the project and tests with debug flags" + @echo " clean : Remove all build artifacts" + @echo " help : Display this help message" diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..724b479 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,202 @@ +// ===================================== +// ERROR HANDING - FROM FCUTILS +// ===================================== + +#include +#include +#include +#include +#include + +#define FORMAT_MAX_SIZE 1024 +extern int errno; + +void format_with_errno(char * buf, size_t n, char const * format) { + char const * errstr = strerror(errno); + char fmt_err[256]; + + size_t i = 0, + j = 0; + while(errstr[j] && i < sizeof(fmt_err) - 1) { + if((fmt_err[i++] = errstr[j++]) != '%') + continue; + + if(i < sizeof(fmt_err) - 1) { + fmt_err[i++] = '%'; + } else { + i--; + } + } + + fmt_err[i] = '\0'; + + snprintf(buf, n, "%s: %s", format, fmt_err); +} + + +[[noreturn]] void die_errno(char const * format, ...) { + va_list vargs; + va_start (vargs, format); + + char buf[FORMAT_MAX_SIZE]; + format_with_errno(buf, sizeof(buf), format); + vfprintf(stderr, buf, vargs); + fprintf(stderr, "\n"); + exit(1); + + va_end(vargs); +} + +[[noreturn]] void die(char const * format, ...) { + va_list vargs; + va_start (vargs, format); + + vfprintf(stderr, format, vargs); + fprintf(stderr, "\n"); + + va_end(vargs); + exit(1); +} + +void err(char const * format, ...) { + va_list vargs; + va_start (vargs, format); + + vfprintf(stderr, format, vargs); + fprintf(stderr, "\n"); + + va_end(vargs); +} + +// ============================================= +// COMMAND LINE UTILITY +// ============================================= + + +#include "wg2sd.hpp" + +#include +#include +#include +#include +#include +#include +#include + +[[noreturn]] void die_usage(char const * prog) { + err("Usage: %s [ -o OUTPUT_PATH ] CONFIG_FILE", prog); + die("Use -h for help"); +} + +void print_help(char const * prog) { + err("Usage: %s [ -o OUTPUT_PATH ] CONFIG_FILE", prog); + err("Options:"); + err("-o OUTPUT_PATH\tSet the output path (default is /etc/systemd/network)"); + err("-h\t\tDisplay this help message"); + exit(EXIT_SUCCESS); +} + +using namespace wg2sd; + +void write_systemd_file(SystemdFilespec const & filespec, std::string output_path, bool secure) { + std::string full_path = output_path + "/" + filespec.name; + std::ofstream ofs; + + if (secure) { + // Set permissions to 0640 before writing the file + umask(0027); + + // Open the file + ofs.open(full_path, std::ios::out | std::ios::trunc); + if (!ofs.is_open()) { + die("Failed to open file %s for writing", full_path.c_str()); + } + + // Change ownership to root:systemd-network + struct group *grp; + grp = getgrnam("systemd-network"); + if (grp == nullptr) { + die_errno("Failed to find the 'systemd-network' group"); + } + if (chown(full_path.c_str(), 0, grp->gr_gid) != 0) { + die_errno("Failed to change ownership of file %s", full_path.c_str()); + } + + // Set permissions + if (chmod(full_path.c_str(), S_IRUSR | S_IWUSR | S_IRGRP) != 0) { + die_errno("Failed to set permissions for file %s", full_path.c_str()); + } + } else { + ofs.open(full_path, std::ios::out | std::ios::trunc); + if (!ofs.is_open()) { + die_errno("Failed to open file %s for writing", full_path.c_str()); + } + } + + // Write the contents to the file + ofs << filespec.contents; + + if (ofs.fail()) { + die("Failed to write to file %s", full_path.c_str()); + } +} + +int main(int argc, char ** argv) { + int opt; + std::filesystem::path output_path = "/etc/systemd/network"; + + while ((opt = getopt(argc, argv, "o:h")) != -1) { + switch (opt) { + case 'o': + output_path = optarg; + break; + case 'h': + print_help(argv[0]); + break; + default: /* '?' */ + die_usage(argv[0]); + } + } + + if (optind >= argc) { + die_usage(argv[0]); + } + + std::filesystem::path config_path = argv[optind]; + std::fstream cfg_stream { config_path }; + + if(!cfg_stream.is_open()) { + die("Failed to open config file %s", config_path.string().c_str()); + } + + std::string interface_name = interface_name_from_filename(config_path); + + SystemdConfig cfg; + + try { + cfg = wg2sd::wg2sd(interface_name, cfg_stream, output_path); + } catch(ConfigurationException const & cex) { + + const ParsingException * pex = dynamic_cast(&cex); + if(pex && pex->line_no().has_value()) { + die("parsing error (line %llu): %s", pex->line_no().value(), pex->what()); + } else { + die("configuration error: %s", cex.what()); + } + + } + + if(!std::filesystem::path(output_path).is_absolute()) { + output_path = std::filesystem::absolute(output_path); + } + + write_systemd_file(cfg.netdev, output_path, false); + write_systemd_file(cfg.network, output_path, false); + write_systemd_file(cfg.private_keyfile, output_path, true); + + for(SystemdFilespec const & spec : cfg.symmetric_keyfiles) { + write_systemd_file(spec, output_path, true); + } + + return 0; +} diff --git a/src/wg2sd.cpp b/src/wg2sd.cpp new file mode 100644 index 0000000..2571908 --- /dev/null +++ b/src/wg2sd.cpp @@ -0,0 +1,503 @@ +#include "wg2sd.hpp" + +#include +#include +#include +#include + +#include + +std::string hashed_keyfile_name(std::string const & priv_key) { + constexpr uint8_t const SALT[] = { + 0x1, 0x6, 0x1, 0x5, 0x5, 0x8, 0x3, 0xd, 0x2, 0x7, + 0x5, 0xc, 0x8, 0x8, 0x7, 0x2, 0x7, 0xa, 0xf, 0x5, + 0xa, 0x6, 0xc, 0x5, 0xf, 0xe, 0x6, 0x7, 0xf, 0xd, + 0x1, 0x5 + }; + + uint8_t const * key = reinterpret_cast(priv_key.c_str()); + uint32_t keylen = priv_key.size(); + + uint8_t t_cost = 2; // 2-pass computation + uint32_t m_cost = 1 << 17; // 128 mebibytes memory + uint32_t parallelism = 1; // single thread + + constexpr size_t HASHLEN = 32; + + uint8_t hash[HASHLEN]; + + argon2i_hash_raw(t_cost, m_cost, parallelism, key, keylen, SALT, sizeof(SALT), hash, HASHLEN); + + constexpr char KEYFILE_EXT[] = ".keyfile"; + + char filename[HASHLEN + sizeof(KEYFILE_EXT)]; + + constexpr char const HEX[] = "0123456789abcdefghijklmnopqrstuv"; + + for(size_t i = 0; i < HASHLEN; i++) { + filename[i] = HEX[hash[i] & 0x1F]; + } + + // copy null terminator + for(size_t i = 0; i < sizeof(KEYFILE_EXT); i++) { + filename[HASHLEN + i] = KEYFILE_EXT[i]; + } + + return std::string { filename } ; +} + + +namespace wg2sd { + + std::string interface_name_from_filename(std::filesystem::path config_path) { + std::string interface_name = config_path.filename().string(); + interface_name = interface_name.substr( + 0, interface_name.size() - config_path.extension().string().size() + ); + return interface_name; + } + + bool _is_default_route(std::string const & cidr) { + static std::regex ipv4_wildcard("0(\\.0){0,3}\\/0"); + static std::regex ipv6_wildcard("(0{0,4}:){0,7}0{0,4}\\/0{1,4}"); + + return std::regex_match(cidr, ipv4_wildcard) or std::regex_match(cidr, ipv6_wildcard); + } + + bool _is_ipv4_route(std::string const & cidr) { + static std::regex ipv4("\\d{1,3}(\\.\\d{1,3}){0,3}(\\/\\d{1,2})?"); + + return std::regex_match(cidr, ipv4); + } + + constexpr uint32_t MAIN_TABLE = 254; + constexpr uint32_t LOCAL_TABLE = 255; + + // Parse the wireguard configuration from an input stream + // into a Config object. If an invalid key or section occurs, + // exists, a ParsingException is thrown. + Config parse_config(std::string const & interface_name, std::istream & stream) { + Config cfg; + + cfg.intf.name = interface_name; + cfg.has_default_route = false; + cfg.intf.should_create_routes = true; + + std::string line; + uint64_t line_no = 0; + + enum class Section { + Interface, + Peer, + None + }; + + Section section = Section::None; + + bool peer_has_default_route = false; + + while (std::getline(stream, line)) { + ++line_no; + + // Strip whitespace (\t) from line in-place + { + size_t i = 0, j = 0; + for(; i < line.size(); i++) { + if(line[i] != ' ' and line[i] != '\t') { + line[j] = line[i]; + j++; + } + } + line.erase(j); + } + + // Remove content exceeding a comment + size_t comment_start = line.find('#'); + if(comment_start != std::string::npos) { + line.erase(comment_start); + } + + // Ignore empty lines + if (line.empty()) { + continue; + } + + // Handle section: [Interface] or [Peer] specifies further + // configuration concerns an interface or peer respectively + + bool interface_sec_wanted = line == "[Interface]"; + bool peer_sec_wanted = line == "[Peer]"; + + if(interface_sec_wanted || peer_sec_wanted) { + cfg.has_default_route = cfg.has_default_route or peer_has_default_route; + peer_has_default_route = false; + } + + if (interface_sec_wanted) { + section = Section::Interface; + continue; + } else if (peer_sec_wanted) { + section = Section::Peer; + cfg.peers.emplace_back(); + continue; + } + + // Split key + size_t pos = line.find('='); + if(pos == std::string::npos) { + throw ParsingException("Expected key-value pair, got \"" + line + "\"", line_no); + } + + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + + // Read keys according to corresponding section + switch (section) { + case Section::Interface: { + if (key == "PrivateKey") { + cfg.intf.private_key = value; + } else if (key == "DNS") { + std::istringstream dnsStream(value); + std::string dnsIp; + while (std::getline(dnsStream, dnsIp, ',')) { + cfg.intf.DNS.push_back(dnsIp); + } + } else if (key == "Address") { + std::istringstream addressStream(value); + std::string address; + while (std::getline(addressStream, address, ',')) { + cfg.intf.addresses.push_back(address); + } + } else if (key == "Table") { + if(value == "off") { + cfg.intf.table = 0; + cfg.intf.should_create_routes = false; + } else { + cfg.intf.should_create_routes = true; + if(value == "auto") { + cfg.intf.table = 0; + } else if(value == "main") { + cfg.intf.table = MAIN_TABLE; + } else if(value == "local") { + cfg.intf.table = LOCAL_TABLE; + } else { + long long table; + try { + table = std::stoll(value); + } catch(std::exception const & e) { + table = -1; + } + + if(table < 1 || table > UINT32_MAX) { + throw ParsingException("Invalid option to \"Table\", must be one of \"off\", \"auto\" or a table number", line_no); + } + + cfg.intf.table = table; + } + } + } else if (key == "ListenPort") { + int port; + try { + port = std::stoi(value); + } catch(std::exception & e) { + port = -1; + } + + if(port < 0 || port > UINT16_MAX) { + throw ParsingException("Invalid port: " + key, line_no); + } + + cfg.intf.listen_port = port; + } else { + throw ParsingException("Invalid key in [Interface] section: " + key, line_no); + } + break; + } + case Section::Peer: { + if (key == "Endpoint") { + cfg.peers.back().endpoint = value; + } else if (key == "AllowedIPs") { + std::istringstream allowedIpsStream(value); + std::string allowedIp; + + while (std::getline(allowedIpsStream, allowedIp, ',')) { + bool is_default_route = _is_default_route(allowedIp); + + if(is_default_route and cfg.has_default_route) { + throw ParsingException("Default routes exist on multiple peers"); + } + + cfg.peers.back().allowed_ips.push_back(Cidr { + .route = allowedIp, + .is_default_route = is_default_route, + .is_ipv4 = _is_ipv4_route(allowedIp), + }); + + peer_has_default_route = peer_has_default_route or is_default_route; + } + + } else if (key == "PublicKey") { + cfg.peers.back().public_key = value; + } else if (key == "PersistentKeepalive") { + cfg.peers.back().persistent_keepalive = value; + } else if (key == "PresharedKey") { + cfg.peers.back().preshared_key = value; + } else { + throw ParsingException("Invalid key in [Peer] section: " + key, line_no); + } + break; + } + case Section::None: + throw ParsingException("Unexpected key outside of section: " + key, line_no); + } + } + + cfg.has_default_route = cfg.has_default_route or peer_has_default_route; + +#define MissingField(section, key) \ + ConfigurationException("[" section "] section missing essential field \"" key "\"") + + // Ensure PrivateKey, Address, PublicKey, and AllowedIPs are present + + if(cfg.intf.private_key.empty()) { + throw MissingField("Interface", "PrivateKey"); + } + + if(cfg.intf.addresses.empty()) { + throw MissingField("Interface", "Address"); + } + + for(Peer const & peer : cfg.peers) { + if(peer.public_key.empty()) { + throw MissingField("Peer", "PublicKey"); + } + + if(peer.allowed_ips.empty()) { + throw MissingField("Peer", "AllowedIPs"); + } + } + +#undef MissingField + + return cfg; + } + + static std::string _gen_netdev_cfg(Config const & cfg, uint32_t fwd_table, std::string const & private_keyfile, + std::vector & symmetric_keyfiles, std::string const & output_path) { + std::stringstream netdev; + + netdev << "# Autogenerated by wg2sd\n"; + netdev << "[NetDev]\n"; + netdev << "Name = " << cfg.intf.name << "\n"; + netdev << "Kind = wireguard\n"; + netdev << "Description = " << cfg.intf.name << " - wireguard tunnel\n"; + + if(!cfg.intf.mtu.empty()) { + netdev << "MTUBytes = " << cfg.intf.mtu << "\n"; + } + + netdev << "\n"; + + netdev << "[WireGuard]\n"; + netdev << "PrivateKeyFile = " << output_path; + + if(output_path.back() != '/') { + netdev << "/"; + } + + netdev << private_keyfile << "\n"; + + if(cfg.intf.should_create_routes and cfg.intf.table != 0) { + netdev << "RouteTable = "; + + switch(cfg.intf.table) { + case LOCAL_TABLE: + netdev << "local"; + break; + case MAIN_TABLE: + netdev << "main"; + break; + default: + netdev << cfg.intf.table; + break; + } + + netdev << "\n"; + } + + if(cfg.intf.should_create_routes and cfg.has_default_route) { + netdev << "FirewallMark = 0x" << std::hex << fwd_table << std::dec << "\n"; + } + + netdev << "\n"; + + for(Peer const & peer : cfg.peers) { + netdev << "[WireGuardPeer]\n"; + + netdev << "Endpoint = " << peer.endpoint << "\n"; + netdev << "PublicKey = " << peer.public_key << "\n"; + + if(!peer.preshared_key.empty()) { + std::string filename = hashed_keyfile_name(peer.preshared_key); + + symmetric_keyfiles.push_back(SystemdFilespec { + .name = filename, + .contents = peer.preshared_key + "\n", + }); + + netdev << "PresharedKeyFile = " << filename << "\n"; + } + + for(Cidr const & cidr : peer.allowed_ips) { + netdev << "AllowedIPs = " << cidr.route << "\n"; + } + + if(!peer.persistent_keepalive.empty()) { + netdev << "PersistentKeepalive = " << peer.persistent_keepalive << "\n"; + } + + netdev << "\n"; + } + + return netdev.str(); + } + + static std::string _gen_network_cfg(Config const & cfg, uint32_t fwd_table) { + std::stringstream network; + + network << "# Autogenerated by wg2sd\n"; + network << "[Match]\n"; + network << "Name = " << cfg.intf.name << "\n"; + network << "\n"; + + network << "[Network]\n"; + for(std::string const & addr : cfg.intf.addresses) { + network << "Address = " << addr << "\n"; + } + + for(std::string const & dns : cfg.intf.DNS) { + network << "DNS = " << dns << "\n"; + } + + if(cfg.has_default_route and cfg.intf.DNS.size() > 0) { + network << "Domains = ~." << "\n"; + } + + network << "\n"; + + if(!cfg.intf.should_create_routes) { + return network.str(); + } + + constexpr uint8_t POLICY_ROUTE_NONE = 0; + constexpr uint8_t POLICY_ROUTE_V4 = 1 << 0; + constexpr uint8_t POLICY_ROUTE_V6 = 1 << 1; + constexpr uint8_t POLICY_ROUTE_BOTH = POLICY_ROUTE_V4 | POLICY_ROUTE_V6; + + uint8_t policy_route = POLICY_ROUTE_NONE; + + for(Peer const & peer : cfg.peers) { + for(Cidr const & cidr : peer.allowed_ips) { + if(cidr.is_default_route) { + policy_route |= cidr.is_ipv4 ? POLICY_ROUTE_V4 : POLICY_ROUTE_V6; + } + + network << "[Route]\n"; + network << "Destination = " << cidr.route << "\n"; + uint32_t table = cfg.has_default_route ? fwd_table : cfg.intf.table; + if(table) { + network << "Table = " << table << "\n"; + } + network << "\n"; + } + } + + if(policy_route != POLICY_ROUTE_NONE) { + + char const * family = nullptr; + + switch(policy_route) { + case POLICY_ROUTE_V4: + family = "ipv6"; + break; + case POLICY_ROUTE_V6: + family = "ipv4"; + break; + case POLICY_ROUTE_BOTH: + family = "both"; + break; + } + + network << "[RoutingPolicyRule]\n"; + network << "SuppressPrefixLength = 0\n"; + network << "Family = " << family << "\n"; + network << "Priority = 32764\n"; + network << "\n"; + + network << "[RoutingPolicyRule]\n"; + network << "FirewallMark = 0x" << std::hex << fwd_table << std::dec << "\n"; + network << "InvertRule = true\n"; + network << "Table = " << fwd_table << "\n"; + network << "Family = " << family << "\n"; + network << "Priority = 32765\n"; + network << "\n"; + + } + + return network.str(); + } + + static uint32_t _random_table() { + std::random_device rd; + std::mt19937 rng { rd() }; + + uint32_t table = 0; + while(table == 0 or table == MAIN_TABLE or table == LOCAL_TABLE) { + table = rng() % UINT32_MAX; + } + + return table; + } + + SystemdConfig gen_systemd_config(Config const & cfg, std::string const & output_path) { + + // If the table is explicitly specified with Table=, + // all routes are added to this table. + // + // If Table=auto and a default route exists, this + // table is used by the default route to supersede + // non-encrypted traffic traveling to /0 routes in the + // main routing table by using suppress_prefix policy rules. + // These routes match a fwmark which is identical to the + // table name. All other routes are placed in the main routing + // table. + // + // If Table=off, no routes are added. + uint32_t fwd_table = _random_table(); + + std::vector symmetric_keyfiles; + + std::string private_keyfile = hashed_keyfile_name(cfg.intf.private_key); + + return SystemdConfig { + .netdev = { + .name = cfg.intf.name + ".netdev", + .contents = _gen_netdev_cfg(cfg, fwd_table, private_keyfile, symmetric_keyfiles, output_path), + }, + .network = { + .name = cfg.intf.name + ".network", + .contents = _gen_network_cfg(cfg, fwd_table) + }, + .private_keyfile = { + .name = private_keyfile, + .contents = cfg.intf.private_key + "\n", + }, + .symmetric_keyfiles = std::move(symmetric_keyfiles) + }; + } + + SystemdConfig wg2sd(std::string const & interface_name, std::istream & stream, std::string const & output_path) { + return gen_systemd_config(parse_config(interface_name, stream), output_path); + } + +} diff --git a/src/wg2sd.hpp b/src/wg2sd.hpp new file mode 100644 index 0000000..00028b4 --- /dev/null +++ b/src/wg2sd.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace wg2sd { + + struct Interface { + // File name, or defaults to "wg" + std::string name; + // Address=... + // List of ip addresses to be assigned to the interface + std::vector addresses; + // PrivateKey=... + // Base64-encoded private key string + std::string private_key; + // MTu=.. + std::string mtu; + // DNS=... + // DNS consists of a comma-separated list of IP addresses of DNS servers + std::vector DNS; + // Table=... + // By default, wireguard creates routes. This is disabled, when Table=off + bool should_create_routes; + // Table number (if specific), 0 if auto + uint32_t table; + // ListenPort=... + // The port number on which the interface will listen + std::optional listen_port; + + Interface() + : should_create_routes { false } + , table { 0 } + , listen_port { } + { } + }; + + struct Cidr { + std::string route; + bool is_default_route; + bool is_ipv4; + }; + + struct Peer { + // Endpoint=... + // IP and port of the peer + std::string endpoint; + // PublicKey=... + std::string public_key; + // AllowedIPs=... + // Comma separated list of allowed ips + // Each allowed ip is a CIDR block + std::vector allowed_ips; + // PersistentAlive=... + std::string persistent_keepalive; + // PresharedKey=... + std::string preshared_key; + }; + + struct Config { + // [Interface] + Interface intf; + // [Peer] + std::vector peers; + // If one of the peers has a default route + bool has_default_route; + + Config() + : has_default_route { false } + { } + }; + + class ConfigurationException : public std::exception { + + public: + + ConfigurationException(std::string const & message) + : _message { message } + {} + + char const * what() const noexcept override { + return _message.c_str(); + } + + private: + std::string _message; + }; + + class ParsingException : public ConfigurationException { + + public: + + ParsingException(std::string const & message, std::optional line_no = {}) + : ConfigurationException(message) + , _line_no { line_no } + {} + + + std::optional line_no() const noexcept { + return _line_no; + } + + private: + std::string _message; + std::optional _line_no; + }; + + struct SystemdFilespec { + std::string name; + std::string contents; + }; + + struct SystemdConfig { + SystemdFilespec netdev; + SystemdFilespec network; + SystemdFilespec private_keyfile; + std::vector symmetric_keyfiles; + }; + + std::string interface_name_from_filename(std::filesystem::path config_path); + + Config parse_config(std::string const & interface_name, std::istream & stream); + + SystemdConfig gen_systemd_config(Config const & cfg, std::string const & output_path); + + SystemdConfig wg2sd(std::string const & interface_name, std::istream & stream, std::string const & output_path); + +}; diff --git a/test/utest.h b/test/utest.h new file mode 100644 index 0000000..ca996ea --- /dev/null +++ b/test/utest.h @@ -0,0 +1,1623 @@ +/* + The latest version of this library is available on GitHub; + https://github.com/sheredom/utest.h +*/ + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + 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 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. + + For more information, please refer to +*/ + +#ifndef SHEREDOM_UTEST_H_INCLUDED +#define SHEREDOM_UTEST_H_INCLUDED + +#ifdef _MSC_VER +/* + Disable warning about not inlining 'inline' functions. +*/ +#pragma warning(disable : 4710) + +/* + Disable warning about inlining functions that are not marked 'inline'. +*/ +#pragma warning(disable : 4711) + +/* + Disable warning for alignment padding added +*/ +#pragma warning(disable : 4820) + +#if _MSC_VER > 1900 +/* + Disable warning about preprocessor macros not being defined in MSVC headers. +*/ +#pragma warning(disable : 4668) + +/* + Disable warning about no function prototype given in MSVC headers. +*/ +#pragma warning(disable : 4255) + +/* + Disable warning about pointer or reference to potentially throwing function. +*/ +#pragma warning(disable : 5039) + +/* + Disable warning about macro expansion producing 'defined' has undefined + behavior. +*/ +#pragma warning(disable : 5105) +#endif + +#if _MSC_VER > 1930 +/* + Disable warning about 'const' variable is not used. +*/ +#pragma warning(disable : 5264) +#endif + +#pragma warning(push, 1) +#endif + +#if defined(_MSC_VER) && (_MSC_VER < 1920) +typedef __int64 utest_int64_t; +typedef unsigned __int64 utest_uint64_t; +typedef unsigned __int32 utest_uint32_t; +#else +#include +typedef int64_t utest_int64_t; +typedef uint64_t utest_uint64_t; +typedef uint32_t utest_uint32_t; +#endif + +#include +#include +#include +#include +#include + +#if defined(__cplusplus) +#if defined(_MSC_VER) && !defined(_CPPUNWIND) +/* We're on MSVC and the compiler is compiling without exception support! */ +#elif !defined(_MSC_VER) && !defined(__EXCEPTIONS) +/* We're on a GCC/Clang compiler that doesn't have exception support! */ +#else +#define UTEST_HAS_EXCEPTIONS 1 +#endif +#endif + +#if defined(UTEST_HAS_EXCEPTIONS) +#include +#endif + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#if defined(__cplusplus) +#define UTEST_C_FUNC extern "C" +#else +#define UTEST_C_FUNC +#endif + +#define UTEST_TEST_PASSED (0) +#define UTEST_TEST_FAILURE (1) +#define UTEST_TEST_SKIPPED (2) + +#if defined(__TINYC__) +#define UTEST_ATTRIBUTE(a) __attribute((a)) +#else +#define UTEST_ATTRIBUTE(a) __attribute__((a)) +#endif + +#if defined(_MSC_VER) || defined(__MINGW64__) || defined(__MINGW32__) + +#if defined(__MINGW64__) || defined(__MINGW32__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunknown-pragmas" +#endif + +#if defined(_WINDOWS_) || defined(_WINDOWS_H) +typedef LARGE_INTEGER utest_large_integer; +#else +// use old QueryPerformanceCounter definitions (not sure is this needed in some +// edge cases or not) on Win7 with VS2015 these extern declaration cause "second +// C linkage of overloaded function not allowed" error +typedef union { + struct { + unsigned long LowPart; + long HighPart; + } DUMMYSTRUCTNAME; + struct { + unsigned long LowPart; + long HighPart; + } u; + utest_int64_t QuadPart; +} utest_large_integer; + +UTEST_C_FUNC __declspec(dllimport) int __stdcall QueryPerformanceCounter( + utest_large_integer *); +UTEST_C_FUNC __declspec(dllimport) int __stdcall QueryPerformanceFrequency( + utest_large_integer *); + +#if defined(__MINGW64__) || defined(__MINGW32__) +#pragma GCC diagnostic pop +#endif +#endif + +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__DragonFly__) || defined(__sun__) || \ + defined(__HAIKU__) +/* + slightly obscure include here - we need to include glibc's features.h, but + we don't want to just include a header that might not be defined for other + c libraries like musl. Instead we include limits.h, which we know on all + glibc distributions includes features.h +*/ +#include + +#if defined(__GLIBC__) && defined(__GLIBC_MINOR__) +#include + +#if ((2 < __GLIBC__) || ((2 == __GLIBC__) && (17 <= __GLIBC_MINOR__))) +/* glibc is version 2.17 or above, so we can just use clock_gettime */ +#define UTEST_USE_CLOCKGETTIME +#else +#include +#include +#endif +#else // Other libc implementations +#include +#define UTEST_USE_CLOCKGETTIME +#endif + +#elif defined(__APPLE__) +#include +#endif + +#if defined(_MSC_VER) && (_MSC_VER < 1920) +#define UTEST_PRId64 "I64d" +#define UTEST_PRIu64 "I64u" +#else +#include + +#define UTEST_PRId64 PRId64 +#define UTEST_PRIu64 PRIu64 +#endif + +#if defined(__cplusplus) +#define UTEST_INLINE inline + +#if defined(__clang__) +#define UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wglobal-constructors\"") + +#define UTEST_INITIALIZER_END_DISABLE_WARNINGS _Pragma("clang diagnostic pop") +#else +#define UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS +#define UTEST_INITIALIZER_END_DISABLE_WARNINGS +#endif + +#define UTEST_INITIALIZER(f) \ + struct f##_cpp_struct { \ + f##_cpp_struct(); \ + }; \ + UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS static f##_cpp_struct \ + f##_cpp_global UTEST_INITIALIZER_END_DISABLE_WARNINGS; \ + f##_cpp_struct::f##_cpp_struct() +#elif defined(_MSC_VER) +#define UTEST_INLINE __forceinline + +#if defined(_WIN64) +#define UTEST_SYMBOL_PREFIX +#else +#define UTEST_SYMBOL_PREFIX "_" +#endif + +#if defined(__clang__) +#define UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wmissing-variable-declarations\"") + +#define UTEST_INITIALIZER_END_DISABLE_WARNINGS _Pragma("clang diagnostic pop") +#else +#define UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS +#define UTEST_INITIALIZER_END_DISABLE_WARNINGS +#endif + +#pragma section(".CRT$XCU", read) +#define UTEST_INITIALIZER(f) \ + static void __cdecl f(void); \ + UTEST_INITIALIZER_BEGIN_DISABLE_WARNINGS \ + __pragma(comment(linker, "/include:" UTEST_SYMBOL_PREFIX #f "_")) \ + UTEST_C_FUNC __declspec(allocate(".CRT$XCU")) void(__cdecl * \ + f##_)(void) = f; \ + UTEST_INITIALIZER_END_DISABLE_WARNINGS \ + static void __cdecl f(void) +#else +#if defined(__linux__) +#if defined(__clang__) +#if __has_warning("-Wreserved-id-macro") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif +#endif + +#define __STDC_FORMAT_MACROS 1 + +#if defined(__clang__) +#if __has_warning("-Wreserved-id-macro") +#pragma clang diagnostic pop +#endif +#endif +#endif + +#define UTEST_INLINE inline + +#define UTEST_INITIALIZER(f) \ + static void f(void) UTEST_ATTRIBUTE(constructor); \ + static void f(void) +#endif + +#if defined(__cplusplus) +#define UTEST_CAST(type, x) static_cast(x) +#define UTEST_PTR_CAST(type, x) reinterpret_cast(x) +#define UTEST_EXTERN extern "C" +#define UTEST_NULL NULL +#else +#define UTEST_CAST(type, x) ((type)(x)) +#define UTEST_PTR_CAST(type, x) ((type)(x)) +#define UTEST_EXTERN extern +#define UTEST_NULL 0 +#endif + +#ifdef _MSC_VER +/* + io.h contains definitions for some structures with natural padding. This is + uninteresting, but for some reason MSVC's behaviour is to warn about + including this system header. That *is* interesting +*/ +#pragma warning(disable : 4820) +#pragma warning(push, 1) +#include +#pragma warning(pop) +#define UTEST_COLOUR_OUTPUT() (_isatty(_fileno(stdout))) +#else +#if defined(__EMSCRIPTEN__) +#include +#define UTEST_COLOUR_OUTPUT() false +#else +#include +#define UTEST_COLOUR_OUTPUT() (isatty(STDOUT_FILENO)) +#endif +#endif + +static UTEST_INLINE void *utest_realloc(void *const pointer, size_t new_size) { + void *const new_pointer = realloc(pointer, new_size); + + if (UTEST_NULL == new_pointer) { + free(new_pointer); + } + + return new_pointer; +} + +static UTEST_INLINE utest_int64_t utest_ns(void) { +#if defined(_MSC_VER) || defined(__MINGW64__) || defined(__MINGW32__) + utest_large_integer counter; + utest_large_integer frequency; + QueryPerformanceCounter(&counter); + QueryPerformanceFrequency(&frequency); + return UTEST_CAST(utest_int64_t, + (counter.QuadPart * 1000000000) / frequency.QuadPart); +#elif defined(__linux__) && defined(__STRICT_ANSI__) + return UTEST_CAST(utest_int64_t, clock()) * 1000000000 / CLOCKS_PER_SEC; +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__DragonFly__) || defined(__sun__) || \ + defined(__HAIKU__) + struct timespec ts; +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(__HAIKU__) + timespec_get(&ts, TIME_UTC); +#else + const clockid_t cid = CLOCK_REALTIME; +#if defined(UTEST_USE_CLOCKGETTIME) + clock_gettime(cid, &ts); +#else + syscall(SYS_clock_gettime, cid, &ts); +#endif +#endif + return UTEST_CAST(utest_int64_t, ts.tv_sec) * 1000 * 1000 * 1000 + ts.tv_nsec; +#elif __APPLE__ + return UTEST_CAST(utest_int64_t, clock_gettime_nsec_np(CLOCK_UPTIME_RAW)); +#elif __EMSCRIPTEN__ + return emscripten_performance_now() * 1000000.0; +#else +#error Unsupported platform! +#endif +} + +typedef void (*utest_testcase_t)(int *, size_t); + +struct utest_test_state_s { + utest_testcase_t func; + size_t index; + char *name; +}; + +struct utest_state_s { + struct utest_test_state_s *tests; + size_t tests_length; + FILE *output; +}; + +/* extern to the global state utest needs to execute */ +UTEST_EXTERN struct utest_state_s utest_state; + +#if defined(_MSC_VER) +#define UTEST_WEAK __forceinline +#elif defined(__MINGW32__) || defined(__MINGW64__) +#define UTEST_WEAK static UTEST_ATTRIBUTE(used) +#elif defined(__clang__) || defined(__GNUC__) || defined(__TINYC__) +#define UTEST_WEAK UTEST_ATTRIBUTE(weak) +#else +#error Non clang, non gcc, non MSVC, non tcc compiler found! +#endif + +#if defined(_MSC_VER) +#define UTEST_UNUSED +#else +#define UTEST_UNUSED UTEST_ATTRIBUTE(unused) +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvariadic-macros" +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#define UTEST_PRINTF(...) \ + if (utest_state.output) { \ + fprintf(utest_state.output, __VA_ARGS__); \ + } \ + printf(__VA_ARGS__) +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvariadic-macros" +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif + +#ifdef _MSC_VER +#define UTEST_SNPRINTF(BUFFER, N, ...) _snprintf_s(BUFFER, N, N, __VA_ARGS__) +#else +#define UTEST_SNPRINTF(...) snprintf(__VA_ARGS__) +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#if defined(__cplusplus) +/* if we are using c++ we can use overloaded methods (its in the language) */ +#define UTEST_OVERLOADABLE +#elif defined(__clang__) +/* otherwise, if we are using clang with c - use the overloadable attribute */ +#define UTEST_OVERLOADABLE UTEST_ATTRIBUTE(overloadable) +#endif + +#if defined(__cplusplus) && (__cplusplus >= 201103L) + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif + +#include + +template ::value> +struct utest_type_deducer final { + static void _(const T t); +}; + +template <> struct utest_type_deducer { + static void _(const signed char c) { + UTEST_PRINTF("%d", static_cast(c)); + } +}; + +template <> struct utest_type_deducer { + static void _(const unsigned char c) { + UTEST_PRINTF("%u", static_cast(c)); + } +}; + +template <> struct utest_type_deducer { + static void _(const short s) { UTEST_PRINTF("%d", static_cast(s)); } +}; + +template <> struct utest_type_deducer { + static void _(const unsigned short s) { + UTEST_PRINTF("%u", static_cast(s)); + } +}; + +template <> struct utest_type_deducer { + static void _(const float f) { UTEST_PRINTF("%f", static_cast(f)); } +}; + +template <> struct utest_type_deducer { + static void _(const double d) { UTEST_PRINTF("%f", d); } +}; + +template <> struct utest_type_deducer { + static void _(const long double d) { +#if defined(__MINGW32__) || defined(__MINGW64__) + /* MINGW is weird - doesn't like LF at all?! */ + UTEST_PRINTF("%f", (double)d); +#else + UTEST_PRINTF("%Lf", d); +#endif + } +}; + +template <> struct utest_type_deducer { + static void _(const int i) { UTEST_PRINTF("%d", i); } +}; + +template <> struct utest_type_deducer { + static void _(const unsigned int i) { UTEST_PRINTF("%u", i); } +}; + +template <> struct utest_type_deducer { + static void _(const long i) { UTEST_PRINTF("%ld", i); } +}; + +template <> struct utest_type_deducer { + static void _(const unsigned long i) { UTEST_PRINTF("%lu", i); } +}; + +template <> struct utest_type_deducer { + static void _(const long long i) { UTEST_PRINTF("%lld", i); } +}; + +template <> struct utest_type_deducer { + static void _(const unsigned long long i) { UTEST_PRINTF("%llu", i); } +}; + +template struct utest_type_deducer { + static void _(const T *t) { + UTEST_PRINTF("%p", static_cast(const_cast(t))); + } +}; + +template struct utest_type_deducer { + static void _(T *t) { UTEST_PRINTF("%p", static_cast(t)); } +}; + +template struct utest_type_deducer { + static void _(const T t) { + UTEST_PRINTF("%llu", static_cast(t)); + } +}; + +template +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(const T t) { + utest_type_deducer::_(t); +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#elif defined(UTEST_OVERLOADABLE) + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(signed char c); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(signed char c) { + UTEST_PRINTF("%d", UTEST_CAST(int, c)); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(unsigned char c); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(unsigned char c) { + UTEST_PRINTF("%u", UTEST_CAST(unsigned int, c)); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(float f); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(float f) { + UTEST_PRINTF("%f", UTEST_CAST(double, f)); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(double d); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(double d) { + UTEST_PRINTF("%f", d); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long double d); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long double d) { +#if defined(__MINGW32__) || defined(__MINGW64__) + /* MINGW is weird - doesn't like LF at all?! */ + UTEST_PRINTF("%f", (double)d); +#else + UTEST_PRINTF("%Lf", d); +#endif +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(int i); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(int i) { + UTEST_PRINTF("%d", i); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(unsigned int i); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(unsigned int i) { + UTEST_PRINTF("%u", i); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long int i); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long int i) { + UTEST_PRINTF("%ld", i); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long unsigned int i); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long unsigned int i) { + UTEST_PRINTF("%lu", i); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(const void *p); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(const void *p) { + UTEST_PRINTF("%p", p); +} + +/* + long long is a c++11 extension +*/ +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) || \ + defined(__cplusplus) && (__cplusplus >= 201103L) || \ + (defined(__MINGW32__) || defined(__MINGW64__)) + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long long int i); +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long long int i) { + UTEST_PRINTF("%lld", i); +} + +UTEST_WEAK UTEST_OVERLOADABLE void utest_type_printer(long long unsigned int i); +UTEST_WEAK UTEST_OVERLOADABLE void +utest_type_printer(long long unsigned int i) { + UTEST_PRINTF("%llu", i); +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !(defined(__MINGW32__) || defined(__MINGW64__)) || \ + defined(__TINYC__) +#define utest_type_printer(val) \ + UTEST_PRINTF(_Generic((val), signed char \ + : "%d", unsigned char \ + : "%u", short \ + : "%d", unsigned short \ + : "%u", int \ + : "%d", long \ + : "%ld", long long \ + : "%lld", unsigned \ + : "%u", unsigned long \ + : "%lu", unsigned long long \ + : "%llu", float \ + : "%f", double \ + : "%f", long double \ + : "%Lf", default \ + : _Generic((val - val), ptrdiff_t \ + : "%p", default \ + : "undef")), \ + (val)) +#else +/* + we don't have the ability to print the values we got, so we create a macro + to tell our users we can't do anything fancy +*/ +#define utest_type_printer(...) UTEST_PRINTF("undef") +#endif + +#ifdef _MSC_VER +#define UTEST_SURPRESS_WARNING_BEGIN \ + __pragma(warning(push)) __pragma(warning(disable : 4127)) \ + __pragma(warning(disable : 4571)) __pragma(warning(disable : 4130)) +#define UTEST_SURPRESS_WARNING_END __pragma(warning(pop)) +#else +#define UTEST_SURPRESS_WARNING_BEGIN +#define UTEST_SURPRESS_WARNING_END +#endif + +#if defined(__cplusplus) && (__cplusplus >= 201103L) +#define UTEST_AUTO(x) auto +#elif !defined(__cplusplus) + +#if defined(__clang__) +/* clang-format off */ +/* had to disable clang-format here because it malforms the pragmas */ +#define UTEST_AUTO(x) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wgnu-auto-type\"") __auto_type \ + _Pragma("clang diagnostic pop") +/* clang-format on */ +#else +#define UTEST_AUTO(x) __typeof__(x + 0) +#endif + +#else +#define UTEST_AUTO(x) typeof(x + 0) +#endif + +#if defined(__clang__) +#define UTEST_STRNCMP(x, y, size) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wdisabled-macro-expansion\"") \ + strncmp(x, y, size) _Pragma("clang diagnostic pop") +#else +#define UTEST_STRNCMP(x, y, size) strncmp(x, y, size) +#endif + +#define UTEST_SKIP(msg) \ + do { \ + UTEST_PRINTF(" Skipped : '%s'\n", (msg)); \ + *utest_result = UTEST_TEST_SKIPPED; \ + return; \ + } while (0) + +#if defined(__clang__) +#define UTEST_EXPECT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wlanguage-extension-token\"") \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat-pedantic\"") \ + _Pragma("clang diagnostic ignored \"-Wfloat-equal\"") \ + UTEST_AUTO(x) xEval = (x); \ + UTEST_AUTO(y) yEval = (y); \ + if (!((xEval)cond(yEval))) { \ + _Pragma("clang diagnostic pop") \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : ("); \ + UTEST_PRINTF(#x ") " #cond " (" #y); \ + UTEST_PRINTF(")\n"); \ + UTEST_PRINTF(" Actual : "); \ + utest_type_printer(xEval); \ + UTEST_PRINTF(" vs "); \ + utest_type_printer(yEval); \ + UTEST_PRINTF("\n"); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#elif defined(__GNUC__) || defined(__TINYC__) +#define UTEST_EXPECT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + UTEST_AUTO(x) xEval = (x); \ + UTEST_AUTO(y) yEval = (y); \ + if (!((xEval)cond(yEval))) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : ("); \ + UTEST_PRINTF(#x ") " #cond " (" #y); \ + UTEST_PRINTF(")\n"); \ + UTEST_PRINTF(" Actual : "); \ + utest_type_printer(xEval); \ + UTEST_PRINTF(" vs "); \ + utest_type_printer(yEval); \ + UTEST_PRINTF("\n"); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#else +#define UTEST_EXPECT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + if (!((x)cond(y))) { \ + UTEST_PRINTF("%s:%i: Failure (Expected " #cond " Actual)\n", __FILE__, \ + __LINE__); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#endif + +#define EXPECT_TRUE(x) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const int xEval = !!(x); \ + if (!(xEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : true\n"); \ + UTEST_PRINTF(" Actual : %s\n", (xEval) ? "true" : "false"); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_FALSE(x) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const int xEval = !!(x); \ + if (xEval) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : false\n"); \ + UTEST_PRINTF(" Actual : %s\n", (xEval) ? "true" : "false"); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_EQ(x, y) UTEST_EXPECT(x, y, ==) +#define EXPECT_NE(x, y) UTEST_EXPECT(x, y, !=) +#define EXPECT_LT(x, y) UTEST_EXPECT(x, y, <) +#define EXPECT_LE(x, y) UTEST_EXPECT(x, y, <=) +#define EXPECT_GT(x, y) UTEST_EXPECT(x, y, >) +#define EXPECT_GE(x, y) UTEST_EXPECT(x, y, >=) + +#define EXPECT_STREQ(x, y) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 != strcmp(xEval, yEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STRNE(x, y) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 == strcmp(xEval, yEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STRNEQ(x, y, n) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + const size_t nEval = UTEST_CAST(size_t, n); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 != UTEST_STRNCMP(xEval, yEval, nEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%.*s\"\n", UTEST_CAST(int, nEval), xEval); \ + UTEST_PRINTF(" Actual : \"%.*s\"\n", UTEST_CAST(int, nEval), yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_STRNNE(x, y, n) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + const size_t nEval = UTEST_CAST(size_t, n); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 == UTEST_STRNCMP(xEval, yEval, nEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%.*s\"\n", UTEST_CAST(int, nEval), xEval); \ + UTEST_PRINTF(" Actual : \"%.*s\"\n", UTEST_CAST(int, nEval), yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define EXPECT_NEAR(x, y, epsilon) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const double diff = \ + utest_fabs(UTEST_CAST(double, x) - UTEST_CAST(double, y)); \ + if (diff > UTEST_CAST(double, epsilon) || utest_isnan(diff)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : %f\n", UTEST_CAST(double, x)); \ + UTEST_PRINTF(" Actual : %f\n", UTEST_CAST(double, y)); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#if defined(UTEST_HAS_EXCEPTIONS) +#define EXPECT_EXCEPTION(x, exception_type) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + int exception_caught = 0; \ + try { \ + x; \ + } catch (const exception_type &) { \ + exception_caught = 1; \ + } catch (...) { \ + exception_caught = 2; \ + } \ + if (exception_caught != 1) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : %s exception\n", #exception_type); \ + UTEST_PRINTF(" Actual : %s\n", (exception_caught == 2) \ + ? "Unexpected exception" \ + : "No exception"); \ + *utest_result = UTEST_TEST_FAILURE; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#endif + +#if defined(__clang__) +#define UTEST_ASSERT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wlanguage-extension-token\"") \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat-pedantic\"") \ + _Pragma("clang diagnostic ignored \"-Wfloat-equal\"") \ + UTEST_AUTO(x) xEval = (x); \ + UTEST_AUTO(y) yEval = (y); \ + if (!((xEval)cond(yEval))) { \ + _Pragma("clang diagnostic pop") \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : ("); \ + UTEST_PRINTF(#x ") " #cond " (" #y); \ + UTEST_PRINTF(")\n"); \ + UTEST_PRINTF(" Actual : "); \ + utest_type_printer(xEval); \ + UTEST_PRINTF(" vs "); \ + utest_type_printer(yEval); \ + UTEST_PRINTF("\n"); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#elif defined(__GNUC__) || defined(__TINYC__) +#define UTEST_ASSERT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + UTEST_AUTO(x) xEval = (x); \ + UTEST_AUTO(y) yEval = (y); \ + if (!((xEval)cond(yEval))) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : ("); \ + UTEST_PRINTF(#x ") " #cond " (" #y); \ + UTEST_PRINTF(")\n"); \ + UTEST_PRINTF(" Actual : "); \ + utest_type_printer(xEval); \ + UTEST_PRINTF(" vs "); \ + utest_type_printer(yEval); \ + UTEST_PRINTF("\n"); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#else +#define UTEST_ASSERT(x, y, cond) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + if (!((x)cond(y))) { \ + UTEST_PRINTF("%s:%i: Failure (Expected " #cond " Actual)\n", __FILE__, \ + __LINE__); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#endif + +#define ASSERT_TRUE(x) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const int xEval = !!(x); \ + if (!(xEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : true\n"); \ + UTEST_PRINTF(" Actual : %s\n", (xEval) ? "true" : "false"); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_FALSE(x) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const int xEval = !!(x); \ + if (xEval) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : false\n"); \ + UTEST_PRINTF(" Actual : %s\n", (xEval) ? "true" : "false"); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_EQ(x, y) UTEST_ASSERT(x, y, ==) +#define ASSERT_NE(x, y) UTEST_ASSERT(x, y, !=) +#define ASSERT_LT(x, y) UTEST_ASSERT(x, y, <) +#define ASSERT_LE(x, y) UTEST_ASSERT(x, y, <=) +#define ASSERT_GT(x, y) UTEST_ASSERT(x, y, >) +#define ASSERT_GE(x, y) UTEST_ASSERT(x, y, >=) + +#define ASSERT_STREQ(x, y) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 != strcmp(xEval, yEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_STRNE(x, y) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 == strcmp(xEval, yEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%s\"\n", xEval); \ + UTEST_PRINTF(" Actual : \"%s\"\n", yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_STRNEQ(x, y, n) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + const size_t nEval = UTEST_CAST(size_t, n); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 != UTEST_STRNCMP(xEval, yEval, nEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%.*s\"\n", UTEST_CAST(int, nEval), xEval); \ + UTEST_PRINTF(" Actual : \"%.*s\"\n", UTEST_CAST(int, nEval), yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_STRNNE(x, y, n) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const char *xEval = (x); \ + const char *yEval = (y); \ + const size_t nEval = UTEST_CAST(size_t, n); \ + if (UTEST_NULL == xEval || UTEST_NULL == yEval || \ + 0 == UTEST_STRNCMP(xEval, yEval, nEval)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : \"%.*s\"\n", UTEST_CAST(int, nEval), xEval); \ + UTEST_PRINTF(" Actual : \"%.*s\"\n", UTEST_CAST(int, nEval), yEval); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#define ASSERT_NEAR(x, y, epsilon) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + const double diff = \ + utest_fabs(UTEST_CAST(double, x) - UTEST_CAST(double, y)); \ + if (diff > UTEST_CAST(double, epsilon) || utest_isnan(diff)) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : %f\n", UTEST_CAST(double, x)); \ + UTEST_PRINTF(" Actual : %f\n", UTEST_CAST(double, y)); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END + +#if defined(UTEST_HAS_EXCEPTIONS) +#define ASSERT_EXCEPTION(x, exception_type) \ + UTEST_SURPRESS_WARNING_BEGIN do { \ + int exception_caught = 0; \ + try { \ + x; \ + } catch (const exception_type &) { \ + exception_caught = 1; \ + } catch (...) { \ + exception_caught = 2; \ + } \ + if (exception_caught != 1) { \ + UTEST_PRINTF("%s:%i: Failure\n", __FILE__, __LINE__); \ + UTEST_PRINTF(" Expected : %s exception\n", #exception_type); \ + UTEST_PRINTF(" Actual : %s\n", (exception_caught == 2) \ + ? "Unexpected exception" \ + : "No exception"); \ + *utest_result = UTEST_TEST_FAILURE; \ + return; \ + } \ + } \ + while (0) \ + UTEST_SURPRESS_WARNING_END +#endif + +#define UTEST(SET, NAME) \ + UTEST_EXTERN struct utest_state_s utest_state; \ + static void utest_run_##SET##_##NAME(int *utest_result); \ + static void utest_##SET##_##NAME(int *utest_result, size_t utest_index) { \ + (void)utest_index; \ + utest_run_##SET##_##NAME(utest_result); \ + } \ + UTEST_INITIALIZER(utest_register_##SET##_##NAME) { \ + const size_t index = utest_state.tests_length++; \ + const char *name_part = #SET "." #NAME; \ + const size_t name_size = strlen(name_part) + 1; \ + char *name = UTEST_PTR_CAST(char *, malloc(name_size)); \ + utest_state.tests = UTEST_PTR_CAST( \ + struct utest_test_state_s *, \ + utest_realloc(UTEST_PTR_CAST(void *, utest_state.tests), \ + sizeof(struct utest_test_state_s) * \ + utest_state.tests_length)); \ + if (utest_state.tests) { \ + utest_state.tests[index].func = &utest_##SET##_##NAME; \ + utest_state.tests[index].name = name; \ + utest_state.tests[index].index = 0; \ + UTEST_SNPRINTF(name, name_size, "%s", name_part); \ + } else if (name) { \ + free(name); \ + } \ + } \ + void utest_run_##SET##_##NAME(int *utest_result) + +#define UTEST_F_SETUP(FIXTURE) \ + static void utest_f_setup_##FIXTURE(int *utest_result, \ + struct FIXTURE *utest_fixture) + +#define UTEST_F_TEARDOWN(FIXTURE) \ + static void utest_f_teardown_##FIXTURE(int *utest_result, \ + struct FIXTURE *utest_fixture) + +#if defined(__GNUC__) && __GNUC__ >= 8 && defined(__cplusplus) +#define UTEST_FIXTURE_SURPRESS_WARNINGS_BEGIN \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wclass-memaccess\"") +#define UTEST_FIXTURE_SURPRESS_WARNINGS_END _Pragma("GCC diagnostic pop") +#else +#define UTEST_FIXTURE_SURPRESS_WARNINGS_BEGIN +#define UTEST_FIXTURE_SURPRESS_WARNINGS_END +#endif + +#define UTEST_F(FIXTURE, NAME) \ + UTEST_FIXTURE_SURPRESS_WARNINGS_BEGIN \ + UTEST_EXTERN struct utest_state_s utest_state; \ + static void utest_f_setup_##FIXTURE(int *, struct FIXTURE *); \ + static void utest_f_teardown_##FIXTURE(int *, struct FIXTURE *); \ + static void utest_run_##FIXTURE##_##NAME(int *, struct FIXTURE *); \ + static void utest_f_##FIXTURE##_##NAME(int *utest_result, \ + size_t utest_index) { \ + struct FIXTURE fixture; \ + (void)utest_index; \ + memset(&fixture, 0, sizeof(fixture)); \ + utest_f_setup_##FIXTURE(utest_result, &fixture); \ + if (UTEST_TEST_PASSED != *utest_result) { \ + return; \ + } \ + utest_run_##FIXTURE##_##NAME(utest_result, &fixture); \ + utest_f_teardown_##FIXTURE(utest_result, &fixture); \ + } \ + UTEST_INITIALIZER(utest_register_##FIXTURE##_##NAME) { \ + const size_t index = utest_state.tests_length++; \ + const char *name_part = #FIXTURE "." #NAME; \ + const size_t name_size = strlen(name_part) + 1; \ + char *name = UTEST_PTR_CAST(char *, malloc(name_size)); \ + utest_state.tests = UTEST_PTR_CAST( \ + struct utest_test_state_s *, \ + utest_realloc(UTEST_PTR_CAST(void *, utest_state.tests), \ + sizeof(struct utest_test_state_s) * \ + utest_state.tests_length)); \ + if (utest_state.tests) { \ + utest_state.tests[index].func = &utest_f_##FIXTURE##_##NAME; \ + utest_state.tests[index].name = name; \ + UTEST_SNPRINTF(name, name_size, "%s", name_part); \ + } else if (name) { \ + free(name); \ + } \ + } \ + UTEST_FIXTURE_SURPRESS_WARNINGS_END \ + void utest_run_##FIXTURE##_##NAME(int *utest_result, \ + struct FIXTURE *utest_fixture) + +#define UTEST_I_SETUP(FIXTURE) \ + static void utest_i_setup_##FIXTURE( \ + int *utest_result, struct FIXTURE *utest_fixture, size_t utest_index) + +#define UTEST_I_TEARDOWN(FIXTURE) \ + static void utest_i_teardown_##FIXTURE( \ + int *utest_result, struct FIXTURE *utest_fixture, size_t utest_index) + +#define UTEST_I(FIXTURE, NAME, INDEX) \ + UTEST_EXTERN struct utest_state_s utest_state; \ + static void utest_run_##FIXTURE##_##NAME##_##INDEX(int *, struct FIXTURE *); \ + static void utest_i_##FIXTURE##_##NAME##_##INDEX(int *utest_result, \ + size_t index) { \ + struct FIXTURE fixture; \ + memset(&fixture, 0, sizeof(fixture)); \ + utest_i_setup_##FIXTURE(utest_result, &fixture, index); \ + if (UTEST_TEST_PASSED != *utest_result) { \ + return; \ + } \ + utest_run_##FIXTURE##_##NAME##_##INDEX(utest_result, &fixture); \ + utest_i_teardown_##FIXTURE(utest_result, &fixture, index); \ + } \ + UTEST_INITIALIZER(utest_register_##FIXTURE##_##NAME##_##INDEX) { \ + size_t i; \ + utest_uint64_t iUp; \ + for (i = 0; i < (INDEX); i++) { \ + const size_t index = utest_state.tests_length++; \ + const char *name_part = #FIXTURE "." #NAME; \ + const size_t name_size = strlen(name_part) + 32; \ + char *name = UTEST_PTR_CAST(char *, malloc(name_size)); \ + utest_state.tests = UTEST_PTR_CAST( \ + struct utest_test_state_s *, \ + utest_realloc(UTEST_PTR_CAST(void *, utest_state.tests), \ + sizeof(struct utest_test_state_s) * \ + utest_state.tests_length)); \ + if (utest_state.tests) { \ + utest_state.tests[index].func = &utest_i_##FIXTURE##_##NAME##_##INDEX; \ + utest_state.tests[index].index = i; \ + utest_state.tests[index].name = name; \ + iUp = UTEST_CAST(utest_uint64_t, i); \ + UTEST_SNPRINTF(name, name_size, "%s/%" UTEST_PRIu64, name_part, iUp); \ + } else if (name) { \ + free(name); \ + } \ + } \ + } \ + void utest_run_##FIXTURE##_##NAME##_##INDEX(int *utest_result, \ + struct FIXTURE *utest_fixture) + +UTEST_WEAK +double utest_fabs(double d); +UTEST_WEAK +double utest_fabs(double d) { + union { + double d; + utest_uint64_t u; + } both; + both.d = d; + both.u &= 0x7fffffffffffffffu; + return both.d; +} + +UTEST_WEAK +int utest_isnan(double d); +UTEST_WEAK +int utest_isnan(double d) { + union { + double d; + utest_uint64_t u; + } both; + both.d = d; + both.u &= 0x7fffffffffffffffu; + return both.u > 0x7ff0000000000000u; +} + +UTEST_WEAK +int utest_should_filter_test(const char *filter, const char *testcase); +UTEST_WEAK int utest_should_filter_test(const char *filter, + const char *testcase) { + if (filter) { + const char *filter_cur = filter; + const char *testcase_cur = testcase; + const char *filter_wildcard = UTEST_NULL; + + while (('\0' != *filter_cur) && ('\0' != *testcase_cur)) { + if ('*' == *filter_cur) { + /* store the position of the wildcard */ + filter_wildcard = filter_cur; + + /* skip the wildcard character */ + filter_cur++; + + while (('\0' != *filter_cur) && ('\0' != *testcase_cur)) { + if ('*' == *filter_cur) { + /* + we found another wildcard (filter is something like *foo*) so we + exit the current loop, and return to the parent loop to handle + the wildcard case + */ + break; + } else if (*filter_cur != *testcase_cur) { + /* otherwise our filter didn't match, so reset it */ + filter_cur = filter_wildcard; + } + + /* move testcase along */ + testcase_cur++; + + /* move filter along */ + filter_cur++; + } + + if (('\0' == *filter_cur) && ('\0' == *testcase_cur)) { + return 0; + } + + /* if the testcase has been exhausted, we don't have a match! */ + if ('\0' == *testcase_cur) { + return 1; + } + } else { + if (*testcase_cur != *filter_cur) { + /* test case doesn't match filter */ + return 1; + } else { + /* move our filter and testcase forward */ + testcase_cur++; + filter_cur++; + } + } + } + + if (('\0' != *filter_cur) || + (('\0' != *testcase_cur) && + ((filter == filter_cur) || ('*' != filter_cur[-1])))) { + /* we have a mismatch! */ + return 1; + } + } + + return 0; +} + +static UTEST_INLINE FILE *utest_fopen(const char *filename, const char *mode) { +#ifdef _MSC_VER + FILE *file; + if (0 == fopen_s(&file, filename, mode)) { + return file; + } else { + return UTEST_NULL; + } +#else + return fopen(filename, mode); +#endif +} + +static UTEST_INLINE int utest_main(int argc, const char *const argv[]); +int utest_main(int argc, const char *const argv[]) { + utest_uint64_t failed = 0; + utest_uint64_t skipped = 0; + size_t index = 0; + size_t *failed_testcases = UTEST_NULL; + size_t failed_testcases_length = 0; + size_t *skipped_testcases = UTEST_NULL; + size_t skipped_testcases_length = 0; + const char *filter = UTEST_NULL; + utest_uint64_t ran_tests = 0; + int enable_mixed_units = 0; + int random_order = 0; + utest_uint32_t seed = 0; + + enum colours { RESET, GREEN, RED, YELLOW }; + + const int use_colours = UTEST_COLOUR_OUTPUT(); + const char *colours[] = {"\033[0m", "\033[32m", "\033[31m", "\033[33m"}; + + if (!use_colours) { + for (index = 0; index < sizeof colours / sizeof colours[0]; index++) { + colours[index] = ""; + } + } + /* loop through all arguments looking for our options */ + for (index = 1; index < UTEST_CAST(size_t, argc); index++) { + /* Informational switches */ + const char help_str[] = "--help"; + const char list_str[] = "--list-tests"; + /* Test config switches */ + const char filter_str[] = "--filter="; + const char output_str[] = "--output="; + const char enable_mixed_units_str[] = "--enable-mixed-units"; + const char random_order_str[] = "--random-order"; + const char random_order_with_seed_str[] = "--random-order="; + + if (0 == UTEST_STRNCMP(argv[index], help_str, strlen(help_str))) { + printf("utest.h - the single file unit testing solution for C/C++!\n" + "Command line Options:\n" + " --help Show this message and exit.\n" + " --filter= Filter the test cases to run (EG. " + "MyTest*.a would run MyTestCase.a but not MyTestCase.b).\n" + " --list-tests List testnames, one per line. Output " + "names can be passed to --filter.\n"); + printf(" --output= Output an xunit XML file to the file " + "specified in .\n" + " --enable-mixed-units Enable the per-test output to contain " + "mixed units (s/ms/us/ns).\n" + " --random-order[=] Randomize the order that the tests are " + "ran in. If the optional argument is not provided, then a " + "random starting seed is used.\n"); + goto cleanup; + } else if (0 == + UTEST_STRNCMP(argv[index], filter_str, strlen(filter_str))) { + /* user wants to filter what test cases run! */ + filter = argv[index] + strlen(filter_str); + } else if (0 == + UTEST_STRNCMP(argv[index], output_str, strlen(output_str))) { + utest_state.output = utest_fopen(argv[index] + strlen(output_str), "w+"); + } else if (0 == UTEST_STRNCMP(argv[index], list_str, strlen(list_str))) { + for (index = 0; index < utest_state.tests_length; index++) { + UTEST_PRINTF("%s\n", utest_state.tests[index].name); + } + /* when printing the test list, don't actually run the tests */ + return 0; + } else if (0 == UTEST_STRNCMP(argv[index], enable_mixed_units_str, + strlen(enable_mixed_units_str))) { + enable_mixed_units = 1; + } else if (0 == UTEST_STRNCMP(argv[index], random_order_with_seed_str, + strlen(random_order_with_seed_str))) { + seed = + UTEST_CAST(utest_uint32_t, + strtoul(argv[index] + strlen(random_order_with_seed_str), + UTEST_NULL, 10)); + random_order = 1; + } else if (0 == UTEST_STRNCMP(argv[index], random_order_str, + strlen(random_order_str))) { + const utest_int64_t ns = utest_ns(); + + // Some really poor pseudo-random using the current time. I do this + // because I really want to avoid using C's rand() because that'd mean our + // random would be affected by any srand() usage by the user (which I + // don't want). + seed = UTEST_CAST(utest_uint32_t, ns >> 32) * 31 + + UTEST_CAST(utest_uint32_t, ns & 0xffffffff); + random_order = 1; + } + } + + if (random_order) { + // Use Fisher-Yates with the Durstenfield's version to randomly re-order the + // tests. + for (index = utest_state.tests_length; index > 1; index--) { + // For the random order we'll use PCG. + const utest_uint32_t state = seed; + const utest_uint32_t word = + ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + const utest_uint32_t next = + ((word >> 22u) ^ word) % UTEST_CAST(utest_uint32_t, index); + + // Swap the randomly chosen element into the last location. + const struct utest_test_state_s copy = utest_state.tests[index - 1]; + utest_state.tests[index - 1] = utest_state.tests[next]; + utest_state.tests[next] = copy; + + // Move the seed onwards. + seed = seed * 747796405u + 2891336453u; + } + } + + for (index = 0; index < utest_state.tests_length; index++) { + if (utest_should_filter_test(filter, utest_state.tests[index].name)) { + continue; + } + + ran_tests++; + } + + printf("%s[==========]%s Running %" UTEST_PRIu64 " test cases.\n", + colours[GREEN], colours[RESET], UTEST_CAST(utest_uint64_t, ran_tests)); + + if (utest_state.output) { + fprintf(utest_state.output, "\n"); + fprintf(utest_state.output, + "\n", + UTEST_CAST(utest_uint64_t, ran_tests)); + fprintf(utest_state.output, + "\n", + UTEST_CAST(utest_uint64_t, ran_tests)); + } + + for (index = 0; index < utest_state.tests_length; index++) { + int result = UTEST_TEST_PASSED; + utest_int64_t ns = 0; + + if (utest_should_filter_test(filter, utest_state.tests[index].name)) { + continue; + } + + printf("%s[ RUN ]%s %s\n", colours[GREEN], colours[RESET], + utest_state.tests[index].name); + + if (utest_state.output) { + fprintf(utest_state.output, "", + utest_state.tests[index].name); + } + + ns = utest_ns(); + errno = 0; +#if defined(UTEST_HAS_EXCEPTIONS) + UTEST_SURPRESS_WARNING_BEGIN + try { + utest_state.tests[index].func(&result, utest_state.tests[index].index); + } catch (const std::exception &err) { + printf(" Exception : %s\n", err.what()); + result = UTEST_TEST_FAILURE; + } catch (...) { + printf(" Exception : Unknown\n"); + result = UTEST_TEST_FAILURE; + } + UTEST_SURPRESS_WARNING_END +#else + utest_state.tests[index].func(&result, utest_state.tests[index].index); +#endif + ns = utest_ns() - ns; + + if (utest_state.output) { + fprintf(utest_state.output, "\n"); + } + + // Record the failing test. + if (UTEST_TEST_FAILURE == result) { + const size_t failed_testcase_index = failed_testcases_length++; + failed_testcases = UTEST_PTR_CAST( + size_t *, utest_realloc(UTEST_PTR_CAST(void *, failed_testcases), + sizeof(size_t) * failed_testcases_length)); + if (UTEST_NULL != failed_testcases) { + failed_testcases[failed_testcase_index] = index; + } + failed++; + } else if (UTEST_TEST_SKIPPED == result) { + const size_t skipped_testcase_index = skipped_testcases_length++; + skipped_testcases = UTEST_PTR_CAST( + size_t *, utest_realloc(UTEST_PTR_CAST(void *, skipped_testcases), + sizeof(size_t) * skipped_testcases_length)); + if (UTEST_NULL != skipped_testcases) { + skipped_testcases[skipped_testcase_index] = index; + } + skipped++; + } + + { + const char *const units[] = {"ns", "us", "ms", "s", UTEST_NULL}; + unsigned int unit_index = 0; + utest_int64_t time = ns; + + if (enable_mixed_units) { + for (unit_index = 0; UTEST_NULL != units[unit_index]; unit_index++) { + if (10000 > time) { + break; + } + + time /= 1000; + } + } + + if (UTEST_TEST_FAILURE == result) { + printf("%s[ FAILED ]%s %s (%" UTEST_PRId64 "%s)\n", colours[RED], + colours[RESET], utest_state.tests[index].name, time, + units[unit_index]); + } else if (UTEST_TEST_SKIPPED == result) { + printf("%s[ SKIPPED ]%s %s (%" UTEST_PRId64 "%s)\n", colours[YELLOW], + colours[RESET], utest_state.tests[index].name, time, + units[unit_index]); + } else { + printf("%s[ OK ]%s %s (%" UTEST_PRId64 "%s)\n", colours[GREEN], + colours[RESET], utest_state.tests[index].name, time, + units[unit_index]); + } + } + } + + printf("%s[==========]%s %" UTEST_PRIu64 " test cases ran.\n", colours[GREEN], + colours[RESET], ran_tests); + printf("%s[ PASSED ]%s %" UTEST_PRIu64 " tests.\n", colours[GREEN], + colours[RESET], ran_tests - failed - skipped); + + if (0 != skipped) { + printf("%s[ SKIPPED ]%s %" UTEST_PRIu64 " tests, listed below:\n", + colours[YELLOW], colours[RESET], skipped); + for (index = 0; index < skipped_testcases_length; index++) { + printf("%s[ SKIPPED ]%s %s\n", colours[YELLOW], colours[RESET], + utest_state.tests[skipped_testcases[index]].name); + } + } + + if (0 != failed) { + printf("%s[ FAILED ]%s %" UTEST_PRIu64 " tests, listed below:\n", + colours[RED], colours[RESET], failed); + for (index = 0; index < failed_testcases_length; index++) { + printf("%s[ FAILED ]%s %s\n", colours[RED], colours[RESET], + utest_state.tests[failed_testcases[index]].name); + } + } + + if (utest_state.output) { + fprintf(utest_state.output, "\n\n"); + } + +cleanup: + for (index = 0; index < utest_state.tests_length; index++) { + free(UTEST_PTR_CAST(void *, utest_state.tests[index].name)); + } + + free(UTEST_PTR_CAST(void *, skipped_testcases)); + free(UTEST_PTR_CAST(void *, failed_testcases)); + free(UTEST_PTR_CAST(void *, utest_state.tests)); + + if (utest_state.output) { + fclose(utest_state.output); + } + + return UTEST_CAST(int, failed); +} + +/* + we need, in exactly one source file, define the global struct that will hold + the data we need to run utest. This macro allows the user to declare the + data without having to use the UTEST_MAIN macro, thus allowing them to write + their own main() function. +*/ +#define UTEST_STATE() struct utest_state_s utest_state = {0, 0, 0} + +/* + define a main() function to call into utest.h and start executing tests! A + user can optionally not use this macro, and instead define their own main() + function and manually call utest_main. The user must, in exactly one source + file, use the UTEST_STATE macro to declare a global struct variable that + utest requires. +*/ +#define UTEST_MAIN() \ + UTEST_STATE(); \ + int main(int argc, const char *const argv[]) { \ + return utest_main(argc, argv); \ + } + +#endif /* SHEREDOM_UTEST_H_INCLUDED */ diff --git a/test/wg2sd_test.cpp b/test/wg2sd_test.cpp new file mode 100644 index 0000000..5d57ac9 --- /dev/null +++ b/test/wg2sd_test.cpp @@ -0,0 +1,219 @@ +#include "utest.h" + +#include "wg2sd.hpp" +#include +#include + +namespace wg2sd { + extern bool _is_default_route(std::string const & cidr); + extern bool _is_ipv4_route(std::string const & cidr); +}; + +using namespace wg2sd; + +UTEST(wg2sd, ip_helpers) { + + std::array default_routes = { + "0/0", + "0.0/0", + "0.0.0/0", + "0.0.0.0/0", + "::/0", + "::0/0", + "::0/0", + "0000:0000:0000:0000::/0", + }; + + for(std::string const & s : default_routes) { + ASSERT_TRUE(_is_default_route(s)); + } + + std::array non_default_routes = { + "192.168.0.1/24", + "0.0.0.1/0", + "0.0.0.0/32", + "1/1", + "2001:db8::1/64", + "fe80::1/128", + "::1/128", + "2001:0db8:0000:0000::/64", + }; + + for(std::string const & s : non_default_routes) { + ASSERT_FALSE(_is_default_route(s)); + } + + std::array ipv4_route = { + "192.168.1.1", + "0.0.0.0/0", + "255.255.255.255/32", + "1.2.3.4/0" + }; + + for(std::string const & s : ipv4_route) { + ASSERT_TRUE(_is_ipv4_route(s)); + } + + std::array ipv6_route = { + "2001:db8::1/64", + "fe80::1/128", + "::1/128", + "2001:0db8:0000:0000::/64", + }; + + for(std::string const & s : ipv6_route) { + ASSERT_FALSE(_is_ipv4_route(s)); + } +} + +// Typical configuration, similar to that provided by mullvad.net +char const * CONFIG1 = ( + "[Interface]\n" + " # Device: Fast Mink\n" + " PrivateKey = APmSX97Yww7WyHrQGG3u7oUJAKRazSyXVu9lD+A3aW8=\n" + "Address = 10.14.123.142/32,fc00:aaaa:aaaa:aa01::6:ad78/128\n" + "DNS =10.0.0.2\n" + "[Peer]\n" + "PublicKey = kMIIVxitU3/1AnAGwdL5KazDQ97MnkuEVz2sWihALnQ= \n" + "AllowedIPs = 0.0.0.0/0,::0/0 # comment\n" + "Endpoint = 194.36.25.33:51820\n" +); + +// Configuration with multiple peers, interface peer tables are reversed +const char * CONFIG2 = ( + "[Peer]\n" + "PublicKey = sMYYPASxJslAuszh5PgUPysrzZHHBOzawJ8PFbRQrHI=\n" + "AllowedIPs = 192.168.1.2/32\n" + "Endpoint = 203.0.113.1:51820\n" + "\n" + "[Peer]\n" + "PublicKey = kB9CSPsPS5irR0ZpVAHZKPNHLQKjIFjmgc6MSCAiWUs=\n" + "AllowedIPs = 192.168.1.3/32\n" + "Endpoint = 203.0.113.2:51820\n" + "\n" + "[Interface]\n" + "PrivateKey = ED3TF8deMhmXHa7Jrp024uv5T7jKl7611vFV3C1P+EY=\n" + "Address = 192.168.1.1/24\n" +); + +// Configuration with persistant keepalive, psk, and multiple DNS entries +const char * CONFIG3 = ( + "[Interface]\n" + "PrivateKey = cJgeEfHUay0aKpV+k1lFK9nq9JJcqzKm8+Wh3EGtg1c=\n" + " Table=5\n" + "\t\tAddress = 192.168.1.1/24\n" + "DNS = 8.8.8.8,\t 8.8.4.4\n" + "ListenPort = 4444\n" + "Table = 42\n" + "\n" + "[Peer]\n" + " PublicKey = kB9CSPsPS5irR0ZpVAHZKPNHLQKjIFjmgc6MSCAiWUs=\n" + " AllowedIPs = 192.168.1.2/32\n" + "Endpoint = 203.0.113.1:51820\n" + "PersistentKeepalive = 25\n" + " PresharedKey = KIst3pK+YVHmM5k7NbNULKd2px9vaRsFi/y4E7NDWDQ=\n" +); + +const char * INVALID_CONFIG = ( + "PrivateKey = kPvfTBQxgHpaXI9wVj6JrtYKIJLVXrf0zg6ON7qUxl8=\n" + "Address = 192.168.1.1/24\n" + "[Interface]\n" +); + + +UTEST(wg2sd, parses_config) { + + // CONFIG1 + std::istringstream ss { CONFIG1 }; + + Config cfg = parse_config("wg", ss); + + ASSERT_STREQ(cfg.intf.name.c_str(), "wg"); + ASSERT_STREQ(cfg.intf.private_key.c_str(), "APmSX97Yww7WyHrQGG3u7oUJAKRazSyXVu9lD+A3aW8="); + ASSERT_EQ(cfg.intf.table, 0ul); + ASSERT_FALSE(cfg.intf.listen_port.has_value()); + ASSERT_EQ(cfg.intf.DNS.size(), 1ull); + ASSERT_STREQ(cfg.intf.DNS.back().c_str(), "10.0.0.2"); + ASSERT_EQ(cfg.peers.size(), 1ull); + + const Peer & peer = cfg.peers[0]; + + ASSERT_STREQ(peer.endpoint.c_str(), "194.36.25.33:51820"); + ASSERT_STREQ(peer.allowed_ips[0].route.c_str(), "0.0.0.0/0"); + ASSERT_TRUE(peer.allowed_ips[0].is_ipv4); + ASSERT_TRUE(peer.allowed_ips[0].is_default_route); + ASSERT_STREQ(peer.allowed_ips[1].route.c_str(), "::0/0"); + ASSERT_FALSE(peer.allowed_ips[1].is_ipv4); + ASSERT_TRUE(peer.allowed_ips[1].is_default_route); + + ASSERT_STREQ(peer.public_key.c_str(), "kMIIVxitU3/1AnAGwdL5KazDQ97MnkuEVz2sWihALnQ="); + ASSERT_STREQ(peer.preshared_key.c_str(), ""); + ASSERT_STREQ(peer.persistent_keepalive.c_str(), ""); + + // CONFIG2 + std::istringstream ss2 { CONFIG2 }; + + Config cfg2 = parse_config("wg", ss2); + + ASSERT_STREQ(cfg2.intf.name.c_str(), "wg"); + ASSERT_FALSE(cfg2.intf.listen_port.has_value()); + ASSERT_STREQ(cfg2.intf.private_key.c_str(), "ED3TF8deMhmXHa7Jrp024uv5T7jKl7611vFV3C1P+EY="); + ASSERT_EQ(cfg2.intf.table, 0ul); + ASSERT_EQ(cfg2.intf.DNS.size(), 0ull); + + ASSERT_EQ(cfg2.peers.size(), 2ull); + + const Peer &peer2_1 = cfg2.peers[0]; + + ASSERT_STREQ(peer2_1.endpoint.c_str(), "203.0.113.1:51820"); + ASSERT_STREQ(peer2_1.allowed_ips[0].route.c_str(), "192.168.1.2/32"); + ASSERT_TRUE(peer2_1.allowed_ips[0].is_ipv4); + ASSERT_FALSE(peer2_1.allowed_ips[0].is_default_route); + ASSERT_STREQ(peer2_1.public_key.c_str(), "sMYYPASxJslAuszh5PgUPysrzZHHBOzawJ8PFbRQrHI="); + ASSERT_STREQ(peer2_1.preshared_key.c_str(), ""); + ASSERT_STREQ(peer2_1.persistent_keepalive.c_str(), ""); + + const Peer &peer2_2 = cfg2.peers[1]; + + ASSERT_STREQ(peer2_2.endpoint.c_str(), "203.0.113.2:51820"); + ASSERT_STREQ(peer2_2.allowed_ips[0].route.c_str(), "192.168.1.3/32"); + ASSERT_TRUE(peer2_2.allowed_ips[0].is_ipv4); + ASSERT_FALSE(peer2_2.allowed_ips[0].is_default_route); + ASSERT_STREQ(peer2_2.public_key.c_str(), "kB9CSPsPS5irR0ZpVAHZKPNHLQKjIFjmgc6MSCAiWUs="); + ASSERT_STREQ(peer2_2.preshared_key.c_str(), ""); + ASSERT_STREQ(peer2_2.persistent_keepalive.c_str(), ""); + + // CONFIG3 + std::istringstream ss3 { CONFIG3 }; + + Config cfg3 = parse_config("wg", ss3); + + ASSERT_STREQ(cfg3.intf.name.c_str(), "wg"); + ASSERT_STREQ(cfg3.intf.private_key.c_str(), "cJgeEfHUay0aKpV+k1lFK9nq9JJcqzKm8+Wh3EGtg1c="); + ASSERT_EQ(cfg3.intf.table, 42ul); + uint16_t port = cfg3.intf.listen_port.value(); + ASSERT_EQ(port, 4444); + ASSERT_EQ(cfg3.intf.DNS.size(), 2ull); + ASSERT_STREQ(cfg3.intf.DNS[0].c_str(), "8.8.8.8"); + ASSERT_STREQ(cfg3.intf.DNS[1].c_str(), "8.8.4.4"); + + ASSERT_EQ(cfg3.peers.size(), 1ull); + + const Peer &peer3 = cfg3.peers[0]; + + ASSERT_STREQ(peer3.endpoint.c_str(), "203.0.113.1:51820"); + ASSERT_STREQ(peer3.allowed_ips[0].route.c_str(), "192.168.1.2/32"); + ASSERT_TRUE(peer3.allowed_ips[0].is_ipv4); + ASSERT_FALSE(peer3.allowed_ips[0].is_default_route); + ASSERT_STREQ(peer3.public_key.c_str(), "kB9CSPsPS5irR0ZpVAHZKPNHLQKjIFjmgc6MSCAiWUs="); + ASSERT_STREQ(peer3.preshared_key.c_str(), "KIst3pK+YVHmM5k7NbNULKd2px9vaRsFi/y4E7NDWDQ="); + ASSERT_STREQ(peer3.persistent_keepalive.c_str(), "25"); + + + // INVALID_CONFIG + std::istringstream ss4 { INVALID_CONFIG }; + + ASSERT_EXCEPTION(parse_config("wg", ss4), ParsingException); +} + +UTEST_MAIN() -- cgit v1.2.3