From ee7539155df97f2627d0d83b959e695ce405c896 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Thu, 15 Oct 2026 10:17:23 +0800 Subject: [PATCH] C++20 port 16/16: config parsing on arenas, unit tests in C++ (#3080) * Rename test_jconf.c to test_jconf_cpp.cpp * Parse the config file on arenas read_jconf now reads the file and builds the JSON tree in a scratch arena that is released before it returns, and copies the parsed strings into one process-lifetime arena instead of a malloc per string. parse_addr and the config parser share one address splitter. Port test_jconf to C++ and cover every field copied out of the parse tree, a re-read, and the densest file the size limit accepts. * tests: rename remaining C unit tests to .cpp (no content changes) * tests: port remaining C unit tests to the C++ test harness Convert acl, acl_literal, aead2022, base64, buffer, cache (legacy reference), crypto, ipset, ipset_reference, json, netutils, ppbloom, process, ref, ssurl and utils tests to test_helpers.hpp. ctest names are unchanged; every assertion is kept. Build the ported sources with the strict C++ warning set. * Run clang-tidy on every ported test The test regex skipped file names with digits (test_aead2022_cpp.cpp, test_base64_cpp.cpp); include them, fix the two optional accesses it then reports, and build test_jconf with the strict warning set. * docs: per-connection RSS on Linux against the C relay * test_jconf: check read_jconf results before using them --- .github/workflows/tests.yml | 2 +- CLAUDE.md | 2 +- docs/cpp-core.md | 10 +- docs/performance.md | 19 +- scripts/no_heap_baseline.txt | 2 +- src/jconf.cpp | 208 ++++++++----- tests/CMakeLists.txt | 40 ++- tests/test_acl.c | 87 ------ tests/test_acl_cpp.cpp | 92 ++++++ tests/test_acl_literal.c | 52 ---- tests/test_acl_literal_cpp.cpp | 71 +++++ ...{test_aead2022.c => test_aead2022_cpp.cpp} | 261 ++++++++-------- tests/test_base64.c | 116 ------- tests/test_base64_cpp.cpp | 108 +++++++ tests/test_buffer.c | 103 ------ tests/test_buffer_cpp.cpp | 94 ++++++ tests/test_cache.c | 129 -------- tests/test_cache_cpp.cpp | 2 +- tests/test_cache_reference_cpp.cpp | 160 ++++++++++ tests/test_crypto_aead2022_cpp.cpp | 5 +- tests/{test_crypto.c => test_crypto_cpp.cpp} | 216 +++++++------ tests/test_ipset.c | 59 ---- tests/test_ipset_cpp.cpp | 77 +++++ tests/test_ipset_reference.c | 59 ---- tests/test_ipset_reference_cpp.cpp | 84 +++++ tests/test_jconf.c | 253 --------------- tests/test_jconf_cpp.cpp | 294 ++++++++++++++++++ tests/test_json.c | 161 ---------- tests/test_json_cpp.cpp | 158 ++++++++++ tests/test_netutils.c | 138 -------- tests/test_netutils_cpp.cpp | 140 +++++++++ tests/test_ppbloom.c | 74 ----- tests/test_ppbloom_cpp.cpp | 66 ++++ tests/test_process.c | 85 ----- tests/test_process_cpp.cpp | 139 +++++++++ tests/test_ref.c | 63 ---- tests/test_ref_cpp.cpp | 72 +++++ tests/test_ssurl.c | 232 -------------- tests/test_ssurl_cpp.cpp | 224 +++++++++++++ tests/test_utils.c | 113 ------- tests/test_utils_cpp.cpp | 100 ++++++ 41 files changed, 2300 insertions(+), 2070 deletions(-) delete mode 100644 tests/test_acl.c create mode 100644 tests/test_acl_cpp.cpp delete mode 100644 tests/test_acl_literal.c create mode 100644 tests/test_acl_literal_cpp.cpp rename tests/{test_aead2022.c => test_aead2022_cpp.cpp} (62%) delete mode 100644 tests/test_base64.c create mode 100644 tests/test_base64_cpp.cpp delete mode 100644 tests/test_buffer.c create mode 100644 tests/test_buffer_cpp.cpp delete mode 100644 tests/test_cache.c create mode 100644 tests/test_cache_reference_cpp.cpp rename tests/{test_crypto.c => test_crypto_cpp.cpp} (54%) delete mode 100644 tests/test_ipset.c create mode 100644 tests/test_ipset_cpp.cpp delete mode 100644 tests/test_ipset_reference.c create mode 100644 tests/test_ipset_reference_cpp.cpp delete mode 100644 tests/test_jconf.c create mode 100644 tests/test_jconf_cpp.cpp delete mode 100644 tests/test_json.c create mode 100644 tests/test_json_cpp.cpp delete mode 100644 tests/test_netutils.c create mode 100644 tests/test_netutils_cpp.cpp delete mode 100644 tests/test_ppbloom.c create mode 100644 tests/test_ppbloom_cpp.cpp delete mode 100644 tests/test_process.c create mode 100644 tests/test_process_cpp.cpp delete mode 100644 tests/test_ref.c create mode 100644 tests/test_ref_cpp.cpp delete mode 100644 tests/test_ssurl.c create mode 100644 tests/test_ssurl_cpp.cpp delete mode 100644 tests/test_utils.c create mode 100644 tests/test_utils_cpp.cpp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1fcea67f..45624986 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -125,7 +125,7 @@ jobs: run: | set -o pipefail tidy_status=0 - run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$? + run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z0-9_]+_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$? grep 'warning:' tidy.log | sort -u > warnings.txt || true count="$(wc -l < warnings.txt)" echo "clang-tidy warnings: $count (max allowed: $MAX_WARNINGS)" diff --git a/CLAUDE.md b/CLAUDE.md index 5e6852e1..0a322ff0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ Uses **uncrustify** with the config at `.uncrustify.cfg`. Key settings: 4-space - **clang-tidy**: config in `.clang-tidy`. The build exports `compile_commands.json` automatically. Run locally: ```bash - run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$' + run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z0-9_]+_cpp)\.cpp)$' ``` On macOS with Homebrew LLVM, add `-extra-arg="-isysroot$(xcrun --show-sdk-path)"`. CI (`clang-tidy` job in `tests.yml`) enforces a warning-count ratchet via `MAX_WARNINGS` — lower it when fixing findings; never raise it without justification. - **No-heap lint**: `scripts/check_no_heap.py --self-test` (also a ctest) rejects heap allocation in C++ sources under `src/`. diff --git a/docs/cpp-core.md b/docs/cpp-core.md index c3faaeda..65dd4ffb 100644 --- a/docs/cpp-core.md +++ b/docs/cpp-core.md @@ -113,7 +113,12 @@ Pools are reserved at startup and only touched slots become resident. 3. Event layer over libuv (`src/event/`). 4. Crypto codecs held by value: AEAD, stream and AEAD-2022. 5. Services: the cache on a fixed slab, `IntrusiveList` for ACL rules and - the manager, the pooled resolver. + the manager, the pooled resolver, and `read_jconf` on arenas (the file and + JSON tree in a scratch arena released before it returns, the parsed + strings in one process-lifetime arena). ACL rules and IP-set nodes still + come from `malloc` at startup: the IP-set trie frees nodes as it merges + prefixes, and its size depends on the ACL file, so a bump arena would + have to reserve for the worst case. 6. One TCP engine for ss-tunnel, ss-local, ss-redir and ss-server. 7. One UDP engine. 8. Library heaps for libuv, c-ares and Mbed TLS, `heap_seal()`, and the @@ -122,7 +127,8 @@ Pools are reserved at startup and only touched slots become resident. (`ss_local_start`/`stop`/`wait`/`destroy`) and the RAII `shadowsocks/shadowsocks.hpp`. 10. Removal of `ss_event`, the C resolver, the C codecs (now test oracles), - uthash and `ss_list`. + uthash and `ss_list`, and the unit tests ported to C++ + (`tests/test_helpers.hpp`). ## Allocation audit diff --git a/docs/performance.md b/docs/performance.md index adaafba8..0ad38cab 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -63,8 +63,23 @@ RSS with 1,000 established, idle connections through ss-local and ss-server Each connection slot carries about 104 KiB of inline buffers, against roughly 58 KiB that the C relay allocated on demand. The pool is reserved virtually at -startup and only touched slots become resident. A shared buffer pool with read -backpressure would cut this; it is the main open trade-off of the rewrite. +startup and only the touched pages of a slot become resident. + +The table above was taken on Apple Silicon, whose 16 KiB pages round every +touched buffer up to a whole page. The same test on Linux (4 KiB pages, GCC, +Alpine container, aes-256-gcm, one small request per connection) gives: + +| | C server | C++ server | C local | C++ local | +|---|---:|---:|---:|---:| +| idle | 1,380 KiB | 1,812 KiB | 1,248 KiB | 1,684 KiB | +| 1,000 connections | 27,604 KiB | 37,248 KiB | 24,744 KiB | 34,236 KiB | +| per connection | 26.2 KiB | 35.4 KiB | 23.5 KiB | 32.6 KiB | + +So the fixed slots cost about 9 KiB per connection more than C on Linux. +Construction does not zero-fill the buffers (a user-provided `FixedBuffer` +constructor changed nothing), so the difference is page granularity across +the separate inline buffers. A shared buffer pool with read backpressure +would cut it; it is the main open trade-off of the rewrite. ### UDP diff --git a/scripts/no_heap_baseline.txt b/scripts/no_heap_baseline.txt index 588f387b..8ea629b5 100644 --- a/scripts/no_heap_baseline.txt +++ b/scripts/no_heap_baseline.txt @@ -3,7 +3,7 @@ # violations, and also when it loses some, so the number here always goes # down. Remove a file once it is rewritten on the src/core primitives. src/ipset.cpp 3 -src/jconf.cpp 16 +src/jconf.cpp 6 src/json.cpp 2 src/local.cpp 1 src/manager.cpp 14 diff --git a/src/jconf.cpp b/src/jconf.cpp index a92c0640..8e679786 100644 --- a/src/jconf.cpp +++ b/src/jconf.cpp @@ -34,6 +34,11 @@ #include "string.h" #include "core.h" +#include "core/arena.hpp" + +#include +#include +#include #define check_json_value_type(value, expected_type, message) \ do { \ @@ -41,15 +46,115 @@ FATAL((message)); \ } while (0) +/* + * The parsed configuration lives as long as the process, so its strings come + * from one arena reserved when the file is read. The file text and the JSON + * tree use a second, scratch arena that is released before read_jconf + * returns. Nothing here goes back to malloc per string. + */ +namespace { + +alignas(ss::MonotonicArena) unsigned char conf_arena_storage[sizeof(ss::MonotonicArena)]; +ss::MonotonicArena *conf_arena; + +// The arena is never destroyed: callers keep jconf_t pointers until exit. +ss::MonotonicArena & +config_arena(std::size_t bytes) +{ + if (conf_arena == nullptr) { + conf_arena = ::new (conf_arena_storage) ss::MonotonicArena(); // heap-ok: placement into static storage + } else { + // A second read_jconf replaces the previous result (conf is static). + *conf_arena = ss::MonotonicArena(); + } + if (!conf_arena->reserve(bytes)) { + FATAL("No enough memory."); + } + return *conf_arena; +} + +char * +conf_strndup(std::string_view text) +{ + auto copy = conf_arena->intern(text); + if (!copy) { + FATAL("No enough memory."); + } + return const_cast(copy->data()); +} + +// json-parser's allocator hooks. The scratch Region is calloc'ed and never +// reused within one parse, so `zero` already holds. +void * +scratch_alloc(size_t size, int, void *user) +{ + auto bytes = static_cast(user)->allocate(size); + return bytes ? bytes->data() : nullptr; +} + +void +scratch_free(void *, void *) +{ +} + +struct AddrParts { + std::string_view host; + std::string_view port; // empty when there is none +}; + +// Split "host", "host:port", "[v6]" or "[v6]:port". IPv6 literals without +// brackets and anything else with several colons stay whole. +AddrParts +split_addr(const char *str) +{ + // Plain pointer arithmetic: string_view::substr can throw out_of_range, + // which would pull in the C++ runtime. + std::string_view text(str); + auto slice = [&](std::size_t from, std::size_t to) { return std::string_view(str + from, to - from); }; + struct ss_ip ip; + if (ss_ip_init(&ip, str) != -1) { + return { text, {} }; + } + if (text.front() == '[') { + auto end = text.find(']'); + if (end == std::string_view::npos) { + return { text, {} }; + } + AddrParts parts{ slice(1, end), {} }; + if (end + 1 < text.size() && text[end + 1] == ':') { + parts.port = slice(end + 2, text.size()); + } + return parts; + } + auto colon = text.find(':'); + if (colon == std::string_view::npos || text.find(':', colon + 1) != std::string_view::npos) { + return { text, {} }; + } + return { slice(0, colon), slice(colon + 1, text.size()) }; +} + +void +parse_conf_addr(const char *str, ss_addr_t *addr) +{ + if (str == NULL) { + return; + } + AddrParts parts = *str == '\0' ? AddrParts{} : split_addr(str); + addr->host = conf_strndup(parts.host); + addr->port = parts.port.empty() ? NULL : conf_strndup(parts.port); +} + +} // namespace + static char * to_string(const json_value *value) { if (value->type == json_string) { - return ss_strndup(value->u.string.ptr, value->u.string.length); + return conf_strndup(std::string_view(value->u.string.ptr, value->u.string.length)); } else if (value->type == json_integer) { char buf[32]; snprintf(buf, sizeof(buf), "%" PRId64, value->u.integer); - return strdup(buf); + return conf_strndup(buf); } else if (value->type == json_null) { return NULL; } else { @@ -83,56 +188,14 @@ parse_addr(const char *str_in, ss_addr_t *addr) { if (str_in == NULL) return; - - int ret = -1, n = 0, len; - char *str = strdup(str_in); - len = strlen(str_in); - - struct ss_ip ip; - if (ss_ip_init(&ip, str) != -1) { - addr->host = str; + if (*str_in == '\0') { + addr->host = strdup(str_in); addr->port = NULL; return; } - - if (str[0] == '[') { - char *end = strchr(str, ']'); - if (end != NULL) { - addr->host = ss_strndup(str + 1, end - str - 1); - if (end[1] == ':' && end[2] != '\0') { - addr->port = strdup(end + 2); - } else { - addr->port = NULL; - } - free(str); - return; - } - - addr->host = str; - addr->port = NULL; - return; - } - - char *pch = strchr(str, ':'); - while (pch != NULL) { - n++; - ret = pch - str; - pch = strchr(pch + 1, ':'); - } - - if (ret == -1 || n != 1) { - addr->host = strdup(str); - addr->port = NULL; - } else { - addr->host = ss_strndup(str, ret); - if (ret < len - 1) { - addr->port = strdup(str + ret + 1); - } else { - addr->port = NULL; - } - } - - free(str); + AddrParts parts = split_addr(str_in); + addr->host = ss_strndup(parts.host.data(), parts.host.size()); + addr->port = parts.port.empty() ? NULL : ss_strndup(parts.port.data(), parts.port.size()); } /* CLI endpoints are strict; keep the legacy JSON address parser unchanged. */ @@ -263,7 +326,6 @@ read_jconf(const char *file) memset(&conf, 0, sizeof(jconf_t)); - char *buf; json_value *obj; FILE *f = fopen(file, "rb"); @@ -283,22 +345,31 @@ read_jconf(const char *file) FATAL("Too large config file."); } - buf = (char *)ss_malloc(pos + 1); - if (buf == NULL) { + // Every string kept in conf copies part of the file plus a NUL: a JSON + // string or key, an integer printed back in decimal, or the two halves of + // an address (whose unsplit copy is kept too). 16x the file size is a + // comfortable bound. + std::size_t len = static_cast(pos); + config_arena(16 * len + 4096); + + // json-parser needs a json_value (and an array or object slot) for every + // value, each costing at least one byte of input. + ss::MonotonicArena scratch; + if (!scratch.reserve(len + 1 + 128 * (len + 1))) { FATAL("No enough memory."); } + char *buf = reinterpret_cast(scratch.allocate(len + 1, 1)->data()); - int nread = fread(buf, pos, 1, f); - if (!nread) { + if (len > 0 && fread(buf, len, 1, f) != 1) { FATAL("Failed to read the config file."); } fclose(f); - buf[pos] = '\0'; // end of string + buf[len] = '\0'; // end of string - json_settings settings = { 0UL, 0, NULL, NULL, NULL }; + json_settings settings = { 0UL, 0, scratch_alloc, scratch_free, &scratch, 0 }; char error_buf[512]; - obj = json_parse_ex(&settings, buf, pos, error_buf); + obj = json_parse_ex(&settings, buf, len, error_buf); if (obj == NULL) { FATAL(error_buf); @@ -316,15 +387,11 @@ read_jconf(const char *file) break; } json_value *v = value->u.array.values[j]; - char *addr_str = to_string(v); - parse_addr(addr_str, conf.remote_addr + j); - ss_free(addr_str); + parse_conf_addr(to_string(v), conf.remote_addr + j); conf.remote_num = j + 1; } } else if (value->type == json_string) { - char* tmp_str = to_string(value); - parse_addr(tmp_str, conf.remote_addr); - ss_free(tmp_str); + parse_conf_addr(to_string(value), conf.remote_addr); conf.remote_num = 1; } } else if (strcmp(name, "port_password") == 0) { @@ -335,8 +402,8 @@ read_jconf(const char *file) } json_value *v = value->u.object.values[j].value; if (v->type == json_string) { - conf.port_password[j].port = ss_strndup(value->u.object.values[j].name, - value->u.object.values[j].name_length); + conf.port_password[j].port = conf_strndup( + std::string_view(value->u.object.values[j].name, value->u.object.values[j].name_length)); conf.port_password[j].password = to_string(v); conf.port_password_num = j + 1; } @@ -364,9 +431,7 @@ read_jconf(const char *file) conf.user = to_string(value); } else if (strcmp(name, "plugin") == 0) { conf.plugin = to_string(value); - // NOLINTNEXTLINE(clang-analyzer-unix.Malloc): config strings live for the process lifetime if (conf.plugin && strlen(conf.plugin) == 0) { - ss_free(conf.plugin); conf.plugin = NULL; } } else if (strcmp(name, "plugin_opts") == 0) { @@ -409,12 +474,11 @@ read_jconf(const char *file) if (v->type == json_string) { char *dscp_str = to_string(v); int dscp = parse_dscp(dscp_str); - char *port = ss_strndup(value->u.object.values[j].name, - value->u.object.values[j].name_length); + char *port = conf_strndup(std::string_view(value->u.object.values[j].name, + value->u.object.values[j].name_length)); conf.dscp[j].port = port; conf.dscp[j].dscp = dscp; conf.dscp_num = j + 1; - ss_free(dscp_str); } } } @@ -435,7 +499,6 @@ read_jconf(const char *file) LOGI("ignore unknown mode: %s, use tcp_only as fallback", mode_str); - ss_free(mode_str); } else if (strcmp(name, "mtu") == 0) { conf.mtu = to_nonnegative_int(value, name); } else if (strcmp(name, "mptcp") == 0) { @@ -479,7 +542,6 @@ read_jconf(const char *file) FATAL("Invalid config file"); } - ss_free(buf); - json_value_free(obj); + // The tree and the file text go with the scratch arena. return &conf; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a5213973..ae604c09 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -117,26 +117,26 @@ if(Python3_Interpreter_FOUND) endif() # Shared-reference ownership and consuming pointer moves. -ss_add_test(test_ref "test_ref.c" "") +ss_add_test(test_ref "test_ref_cpp.cpp" "") # test_base64 - no external deps ss_add_test(test_base64 - "test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.cpp" + "test_base64_cpp.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp" "") # test_utils - needs sodium (for ss_malloc -> uses sodium in utils.cpp) ss_add_test(test_utils - "test_utils.c;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_utils_cpp.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "ss_sodium") # test_json - standalone json parser ss_add_test(test_json - "test_json.c;${PROJECT_SOURCE_DIR}/src/json.cpp" + "test_json_cpp.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp" "${SS_MATH_LIBRARY}") # test_ssurl - ss:// URI parsing (SIP002 and legacy) ss_add_test(test_ssurl - "test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_ssurl_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ssurl.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "ss_sodium") # test_netutils - address parsing and resolver integration @@ -146,12 +146,12 @@ set(TEST_NETUTILS_LIBS ss_cares ) ss_add_test(test_netutils - "test_netutils.c;${PROJECT_SOURCE_DIR}/src/netutils.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_netutils_cpp.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "${TEST_NETUTILS_LIBS}") # test_cache - expiration uses a platform monotonic clock ss_add_test(test_cache - "test_cache.c;${CMAKE_CURRENT_SOURCE_DIR}/reference/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_cache_reference_cpp.cpp;${CMAKE_CURRENT_SOURCE_DIR}/reference/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "ss_sodium") # test_cache_cpp - fixed-capacity cache edge cases @@ -164,7 +164,7 @@ ss_strict_warnings(test_cache_cpp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/reference/cach set(TEST_PPBLOOM_LIBS "") list(APPEND TEST_PPBLOOM_LIBS bloom) ss_add_test(test_ppbloom - "test_ppbloom.c;${PROJECT_SOURCE_DIR}/src/ppbloom.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_ppbloom_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ppbloom.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "${TEST_PPBLOOM_LIBS};ss_sodium;${SS_MATH_LIBRARY}") if(SS_ENABLE_REGEX) @@ -180,7 +180,7 @@ ss_add_test(test_rule_cpp # test_acl - ACL precedence and prefix matching set(TEST_ACL_LIBS ${TEST_RULE_LIBS}) ss_add_test(test_acl - "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_acl_cpp.cpp;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "${TEST_ACL_LIBS}") endif() @@ -192,7 +192,7 @@ set(TEST_JCONF_LIBS ss_cares ) ss_add_test(test_jconf - "test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp" + "test_jconf_cpp.cpp;${PROJECT_SOURCE_DIR}/src/jconf.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp" "${TEST_JCONF_LIBS};${SS_MATH_LIBRARY}") # The original C cipher implementation, kept under tests/reference as an @@ -213,7 +213,7 @@ set(SS_CRYPTO_TEST_SOURCES # test_buffer - needs sodium and mbedtls for crypto.cpp buffer functions ss_add_test(test_buffer - "test_buffer.c;${SS_CRYPTO_TEST_SOURCES}" + "test_buffer_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}" "ss_sodium;;ss_mbedcrypto;ss_ev;${TEST_PPBLOOM_LIBS};blake3;${SS_MATH_LIBRARY}") # test_crypto - full crypto test @@ -227,12 +227,12 @@ list(APPEND TEST_CRYPTO_LIBS bloom) # BLAKE3 backs the 2022 ciphers' key derivation list(APPEND TEST_CRYPTO_LIBS blake3) ss_add_test(test_crypto - "test_crypto.c;${SS_CRYPTO_TEST_SOURCES}" + "test_crypto_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}" "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") # test_aead2022 - Shadowsocks 2022 Edition (SIP022) ciphers ss_add_test(test_aead2022 - "test_aead2022.c;${SS_CRYPTO_TEST_SOURCES}" + "test_aead2022_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}" "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") # test_crypto_aead_cpp - C++ AEAD codecs, differentially against aead.cpp @@ -266,11 +266,11 @@ add_test(NAME test_ss_setup set_tests_properties(test_ss_setup PROPERTIES LABELS "integration") ss_add_test(test_ipset - "test_ipset.c;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_ipset_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "ss_sodium") ss_add_test(test_process - "test_process.c;${PROJECT_SOURCE_DIR}/src/process.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_process_cpp.cpp;${PROJECT_SOURCE_DIR}/src/process.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "ss_sodium") set_tests_properties(test_process PROPERTIES TIMEOUT 20) @@ -288,9 +288,17 @@ if(SS_ENABLE_REGEX) list(APPEND TEST_LITERAL_LIBS ss_pcre2) endif() ss_add_test(test_acl_literal - "test_acl_literal.c;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" + "test_acl_literal_cpp.cpp;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp" "${TEST_LITERAL_LIBS}") +# Tests ported from C build with the full C++ warning set. test_ipset_reference +# is intentionally absent: it needs the pre-migration libipset archives. +ss_strict_warnings( + test_acl_cpp.cpp test_acl_literal_cpp.cpp test_aead2022_cpp.cpp test_base64_cpp.cpp + test_buffer_cpp.cpp test_cache_reference_cpp.cpp test_crypto_cpp.cpp test_ipset_cpp.cpp + test_jconf_cpp.cpp test_json_cpp.cpp test_netutils_cpp.cpp test_ppbloom_cpp.cpp test_process_cpp.cpp + test_ref_cpp.cpp test_ssurl_cpp.cpp test_utils_cpp.cpp) + # Preloadable malloc counter for the allocation audit. It interposes the # system allocator, so it is only built where that is supported (glibc, diff --git a/tests/test_acl.c b/tests/test_acl.c deleted file mode 100644 index cdf718af..00000000 --- a/tests/test_acl.c +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include -#include -#include "test_helpers.h" - -#include "acl.h" - -int verbose = 0; - -static void -test_invalid_regex_is_not_installed(void) -{ - char path[4096]; - FILE *f = test_tempfile(path, sizeof(path)); - assert(f != NULL); - fprintf(f, "[black_list]\n"); - fprintf(f, "[invalid\n"); - fprintf(f, "^blocked\\.example$\n"); - fprintf(f, "[outbound_block_list]\n"); - fprintf(f, "[also-invalid\n"); - fprintf(f, "^outbound\\.example$\n"); - fclose(f); - - assert(init_acl(path) == 0); - assert(acl_match_host("does-not-crash.example") == 0); - assert(acl_match_host("blocked.example") == 1); - assert(outbound_block_match_host("does-not-crash.example") == 0); - assert(outbound_block_match_host("outbound.example") == 1); - free_acl(); - - remove(path); -} - -static void -test_long_acl_line_is_discarded(void) -{ - char path[4096]; - FILE *f = test_tempfile(path, sizeof(path)); - assert(f != NULL); - fprintf(f, "[black_list]\n"); - for (int i = 0; i < 400; i++) { - fputc('a', f); - } - fprintf(f, "\n"); - fprintf(f, "^kept\\.example$\n"); - fclose(f); - - assert(init_acl(path) == 0); - assert(acl_match_host("kept.example") == 1); - free_acl(); - - remove(path); -} - -static void -test_invalid_cidr_is_discarded(void) -{ - char path[4096]; - FILE *f = test_tempfile(path, sizeof(path)); - assert(f != NULL); - fprintf(f, "[black_list]\n"); - fprintf(f, "1.2.3.0/999\n"); - fprintf(f, "2.2.2.2/-1\n"); - fprintf(f, "3.3.3.3/abc\n"); - fprintf(f, "4.4.4.0/24\n"); - fprintf(f, "^path/with/slash\\.example$\n"); - fclose(f); - - assert(init_acl(path) == 0); - assert(acl_match_host("1.2.3.1") == 0); - assert(acl_match_host("2.2.2.2") == 0); - assert(acl_match_host("3.3.3.3") == 0); - assert(acl_match_host("4.4.4.8") == 1); - assert(acl_match_host("path/with/slash.example") == 1); - free_acl(); - - remove(path); -} - -int -main(void) -{ - test_invalid_regex_is_not_installed(); - test_long_acl_line_is_discarded(); - test_invalid_cidr_is_discarded(); - return 0; -} diff --git a/tests/test_acl_cpp.cpp b/tests/test_acl_cpp.cpp new file mode 100644 index 00000000..0058d1d6 --- /dev/null +++ b/tests/test_acl_cpp.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// ACL file parsing (src/acl.cpp): invalid rules are discarded, valid ones +// still match. +#include +#include + +#include "acl.h" +#include "test_helpers.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +void test_invalid_regex_is_not_installed() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fprintf(f, "[black_list]\n"); + std::fprintf(f, "[invalid\n"); + std::fprintf(f, "^blocked\\.example$\n"); + std::fprintf(f, "[outbound_block_list]\n"); + std::fprintf(f, "[also-invalid\n"); + std::fprintf(f, "^outbound\\.example$\n"); + std::fclose(f); + + SS_CHECK(init_acl(path) == 0); + SS_CHECK(acl_match_host("does-not-crash.example") == 0); + SS_CHECK(acl_match_host("blocked.example") == 1); + SS_CHECK(outbound_block_match_host("does-not-crash.example") == 0); + SS_CHECK(outbound_block_match_host("outbound.example") == 1); + free_acl(); + + std::remove(path); +} + +void test_long_acl_line_is_discarded() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fprintf(f, "[black_list]\n"); + for (int i = 0; i < 400; i++) { + std::fputc('a', f); + } + std::fprintf(f, "\n"); + std::fprintf(f, "^kept\\.example$\n"); + std::fclose(f); + + SS_CHECK(init_acl(path) == 0); + SS_CHECK(acl_match_host("kept.example") == 1); + free_acl(); + + std::remove(path); +} + +void test_invalid_cidr_is_discarded() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fprintf(f, "[black_list]\n"); + std::fprintf(f, "1.2.3.0/999\n"); + std::fprintf(f, "2.2.2.2/-1\n"); + std::fprintf(f, "3.3.3.3/abc\n"); + std::fprintf(f, "4.4.4.0/24\n"); + std::fprintf(f, "^path/with/slash\\.example$\n"); + std::fclose(f); + + SS_CHECK(init_acl(path) == 0); + SS_CHECK(acl_match_host("1.2.3.1") == 0); + SS_CHECK(acl_match_host("2.2.2.2") == 0); + SS_CHECK(acl_match_host("3.3.3.3") == 0); + SS_CHECK(acl_match_host("4.4.4.8") == 1); + SS_CHECK(acl_match_host("path/with/slash.example") == 1); + free_acl(); + + std::remove(path); +} + +} // namespace + +int main() +{ + SS_RUN(test_invalid_regex_is_not_installed); + SS_RUN(test_long_acl_line_is_discarded); + SS_RUN(test_invalid_cidr_is_discarded); + return 0; +} diff --git a/tests/test_acl_literal.c b/tests/test_acl_literal.c deleted file mode 100644 index 55dd0a8f..00000000 --- a/tests/test_acl_literal.c +++ /dev/null @@ -1,52 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif -#include -#include -#include -#include "acl.h" -int verbose = 0; - -int -main(int argc, char **argv) -{ - (void)argc; - char path[4096]; - assert(snprintf(path, sizeof(path), "%s.acl", argv[0]) < (int)sizeof(path)); - FILE *file = fopen(path, "w"); - assert(file != NULL); - fputs("[black_list]\nfull:Exact.example\nsuffix:blocked.example\n10.0.0.0/8\n" - "2001:db8::/32\n[white_list]\nsuffix:allowed.example\n" - "[outbound_block_list]\nfull:outbound.example\n", file); - fclose(file); - assert(init_acl(path) == 0); - assert(acl_match_host("EXACT.example") == 1); - assert(acl_match_host("sub.exact.example") == 0); - assert(acl_match_host("blocked.example") == 1); - assert(acl_match_host("a.blocked.example") == 1); - assert(acl_match_host("notblocked.example") == 0); - assert(acl_match_host("blocked.example.evil") == 0); - assert(acl_match_host("a.allowed.example") == -1); - assert(outbound_block_match_host("outbound.example") == 1); - assert(outbound_block_match_host("a.outbound.example") == 0); - assert(acl_match_host("10.42.1.2") == 1); - assert(acl_remove_ip("10.42.1.2") == 0); - assert(acl_match_host("10.42.1.2") == 0); - assert(acl_match_host("10.42.1.3") == 1); - assert(acl_add_ip("10.42.1.2") == 0); - assert(acl_match_host("10.42.1.2") == 1); - assert(acl_remove_ip("2001:db8::42") == 0); - assert(acl_match_host("2001:db8::42") == 0); - assert(acl_match_host("2001:db8::43") == 1); - free_acl(); -#if !SS_ENABLE_REGEX - file = fopen(path, "w"); - assert(file != NULL); - fputs("[black_list]\n10.0.0.0/8\n^old\\.regex$\n", file); - fclose(file); - assert(init_acl(path) == -1); - assert(acl_match_host("10.1.1.1") == 0); -#endif - remove(path); - return 0; -} diff --git a/tests/test_acl_literal_cpp.cpp b/tests/test_acl_literal_cpp.cpp new file mode 100644 index 00000000..cc1d9f99 --- /dev/null +++ b/tests/test_acl_literal_cpp.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// ACL literal rules (full:/suffix:), CIDR sets and runtime IP edits +// (src/acl.cpp). +#include + +#ifdef HAVE_CONFIG_H +#include "config.h" // SS_ENABLE_REGEX +#endif + +#include "acl.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +void write_file(const char *path, const char *content) +{ + FILE *file = std::fopen(path, "w"); + SS_CHECK(file != nullptr); + std::fputs(content, file); + std::fclose(file); +} + +void test_acl_literal(const char *argv0) +{ + char path[4096]; + SS_CHECK(std::snprintf(path, sizeof(path), "%s.acl", argv0) < static_cast(sizeof(path))); + write_file(path, + "[black_list]\nfull:Exact.example\nsuffix:blocked.example\n10.0.0.0/8\n" + "2001:db8::/32\n[white_list]\nsuffix:allowed.example\n" + "[outbound_block_list]\nfull:outbound.example\n"); + SS_CHECK(init_acl(path) == 0); + SS_CHECK(acl_match_host("EXACT.example") == 1); + SS_CHECK(acl_match_host("sub.exact.example") == 0); + SS_CHECK(acl_match_host("blocked.example") == 1); + SS_CHECK(acl_match_host("a.blocked.example") == 1); + SS_CHECK(acl_match_host("notblocked.example") == 0); + SS_CHECK(acl_match_host("blocked.example.evil") == 0); + SS_CHECK(acl_match_host("a.allowed.example") == -1); + SS_CHECK(outbound_block_match_host("outbound.example") == 1); + SS_CHECK(outbound_block_match_host("a.outbound.example") == 0); + SS_CHECK(acl_match_host("10.42.1.2") == 1); + SS_CHECK(acl_remove_ip("10.42.1.2") == 0); + SS_CHECK(acl_match_host("10.42.1.2") == 0); + SS_CHECK(acl_match_host("10.42.1.3") == 1); + SS_CHECK(acl_add_ip("10.42.1.2") == 0); + SS_CHECK(acl_match_host("10.42.1.2") == 1); + SS_CHECK(acl_remove_ip("2001:db8::42") == 0); + SS_CHECK(acl_match_host("2001:db8::42") == 0); + SS_CHECK(acl_match_host("2001:db8::43") == 1); + free_acl(); +#if !SS_ENABLE_REGEX + write_file(path, "[black_list]\n10.0.0.0/8\n^old\\.regex$\n"); + SS_CHECK(init_acl(path) == -1); + SS_CHECK(acl_match_host("10.1.1.1") == 0); +#endif + std::remove(path); +} + +} // namespace + +int main(int argc, char **argv) +{ + SS_CHECK(argc >= 1); + test_acl_literal(argv[0]); + std::printf("ok test_acl_literal\n"); + return 0; +} diff --git a/tests/test_aead2022.c b/tests/test_aead2022_cpp.cpp similarity index 62% rename from tests/test_aead2022.c rename to tests/test_aead2022_cpp.cpp index b1e16bf3..9183ca67 100644 --- a/tests/test_aead2022.c +++ b/tests/test_aead2022_cpp.cpp @@ -1,53 +1,53 @@ -/* - * Unit tests for the Shadowsocks 2022 Edition (SIP022) cipher suite. - */ +// SPDX-License-Identifier: GPL-3.0-or-later +// Shadowsocks 2022 Edition (SIP022) cipher suite in the legacy crypto +// reference (tests/reference/aead.cpp): BLAKE3 vectors, TCP and UDP round +// trips, tamper and replay rejection. +#include +#include +#include +#include -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include #include -int verbose = 0; +#include "test_helpers.hpp" -#include "crypto.h" #include "aead.h" +#include "blake3/blake3.h" +#include "crypto.h" #include "ppbloom.h" #include "utils.h" -#include "blake3/blake3.h" -/* Provide nonce_cache symbol needed by crypto.cpp */ -struct cache *nonce_cache = NULL; +extern "C" { +int verbose = 0; +// Provide the nonce_cache symbol needed by the crypto reference. +struct cache *nonce_cache = nullptr; +} + +namespace { /* 32-byte and 16-byte PSKs in base64 */ #define PSK32 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" #define PSK16 "AAAAAAAAAAAAAAAAAAAAAA==" -static const char *methods_2022[] = { +const char *const methods_2022[] = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305", }; -static const char * -psk_for(const char *method) +const char *psk_for(const char *method) { - return strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32; + return std::strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32; } -static crypto_t * -make_crypto(const char *method) +crypto_t *make_crypto(const char *method) { - crypto_t *c = crypto_init(psk_for(method), NULL, method); - assert(c != NULL); + crypto_t *c = crypto_init(psk_for(method), nullptr, method); + SS_CHECK(c != nullptr); return c; } -static void -free_crypto(crypto_t *c) +void free_crypto(crypto_t *c) { /* * crypto_init() sets up the nonce bloom filter each time it is called, @@ -64,14 +64,13 @@ free_crypto(crypto_t *c) * nothing interoperates. Vector generated from the BLAKE3 reference for * context "shadowsocks 2022 session subkey" over 32 zero bytes of material. */ -static void -test_blake3_derive_key_known_vector(void) +void test_blake3_derive_key_known_vector() { blake3_hasher h; uint8_t material[32]; uint8_t out[32]; - memset(material, 0, sizeof(material)); + std::memset(material, 0, sizeof(material)); blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX); blake3_hasher_update(&h, material, sizeof(material)); blake3_hasher_finalize(&h, out, sizeof(out)); @@ -81,20 +80,18 @@ test_blake3_derive_key_known_vector(void) blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX); blake3_hasher_update(&h, material, sizeof(material)); blake3_hasher_finalize(&h, out2, sizeof(out2)); - assert(memcmp(out, out2, 32) == 0); + SS_CHECK(std::memcmp(out, out2, 32) == 0); /* A different context string must give a different subkey. */ uint8_t out3[32]; blake3_hasher_init_derive_key(&h, "some other context"); blake3_hasher_update(&h, material, sizeof(material)); blake3_hasher_finalize(&h, out3, sizeof(out3)); - assert(memcmp(out, out3, 32) != 0); - (void)out3; + SS_CHECK(std::memcmp(out, out3, 32) != 0); } /* BLAKE3 hash of the empty input, from the reference test vectors. */ -static void -test_blake3_empty_hash_vector(void) +void test_blake3_empty_hash_vector() { blake3_hasher h; uint8_t out[32]; @@ -108,16 +105,14 @@ test_blake3_empty_hash_vector(void) blake3_hasher_init(&h); blake3_hasher_update(&h, "", 0); blake3_hasher_finalize(&h, out, sizeof(out)); - assert(memcmp(out, expected, 32) == 0); - (void)expected; + SS_CHECK(std::memcmp(out, expected, 32) == 0); } /* * A client request stream must round-trip through a server decrypt context: * salt, fixed header, variable header with the SOCKS address, then payload. */ -static void -test_tcp_request_roundtrip(const char *method) +void test_tcp_request_roundtrip(const char *method) { /* SOCKS address: IPv4 1.2.3.4:80 */ static const uint8_t addr[] = { 0x01, 1, 2, 3, 4, 0x00, 0x50 }; @@ -137,35 +132,35 @@ test_tcp_request_roundtrip(const char *method) /* First write: the address header. */ buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 4096); - memcpy(buf.data, addr, sizeof(addr)); + std::memcpy(buf.data, addr, sizeof(addr)); buf.len = sizeof(addr); - assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); + SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); /* Second write: stream payload. */ buffer_t buf2; - memset(&buf2, 0, sizeof(buf2)); + std::memset(&buf2, 0, sizeof(buf2)); balloc(&buf2, 4096); - size_t plen = strlen(payload); - memcpy(buf2.data, payload, plen); + size_t plen = std::strlen(payload); + std::memcpy(buf2.data, payload, plen); buf2.len = plen; - assert(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK); + SS_CHECK(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK); /* Feed both writes to the server in one go, as they arrive on the wire. */ buffer_t wire; - memset(&wire, 0, sizeof(wire)); + std::memset(&wire, 0, sizeof(wire)); balloc(&wire, 8192); - memcpy(wire.data, buf.data, buf.len); - memcpy(wire.data + buf.len, buf2.data, buf2.len); + std::memcpy(wire.data, buf.data, buf.len); + std::memcpy(wire.data + buf.len, buf2.data, buf2.len); wire.len = buf.len + buf2.len; - assert(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK); + SS_CHECK(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK); /* The server sees the SOCKS address followed by the payload. */ - assert(wire.len == sizeof(addr) + plen); - assert(memcmp(wire.data, addr, sizeof(addr)) == 0); - assert(memcmp(wire.data + sizeof(addr), payload, plen) == 0); + SS_CHECK(wire.len == sizeof(addr) + plen); + SS_CHECK(std::memcmp(wire.data, addr, sizeof(addr)) == 0); + SS_CHECK(std::memcmp(wire.data + sizeof(addr), payload, plen) == 0); bfree(&buf); bfree(&buf2); @@ -179,8 +174,7 @@ test_tcp_request_roundtrip(const char *method) * A stream split across arbitrary boundaries must still decrypt: the * decryptor buffers partial chunks and returns CRYPTO_NEED_MORE. */ -static void -test_tcp_partial_delivery(const char *method) +void test_tcp_partial_delivery(const char *method) { static const uint8_t addr[] = { 0x01, 10, 0, 0, 1, 0x1f, 0x90 }; crypto_t *c = make_crypto(method); @@ -194,33 +188,32 @@ test_tcp_partial_delivery(const char *method) dec.role = CRYPTO_ROLE_SERVER; buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 4096); - memcpy(buf.data, addr, sizeof(addr)); + std::memcpy(buf.data, addr, sizeof(addr)); buf.len = sizeof(addr); - assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); + SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); /* Deliver the ciphertext one byte at a time. */ size_t total = buf.len; int got_ok = 0; for (size_t i = 0; i < total; i++) { buffer_t piece; - memset(&piece, 0, sizeof(piece)); + std::memset(&piece, 0, sizeof(piece)); balloc(&piece, 4096); piece.data[0] = buf.data[i]; piece.len = 1; int rc = c->decrypt(&piece, &dec, 4096); - assert(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE); + SS_CHECK(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE); if (rc == CRYPTO_OK) { - assert(piece.len == sizeof(addr)); - assert(memcmp(piece.data, addr, sizeof(addr)) == 0); + SS_CHECK(piece.len == sizeof(addr)); + SS_CHECK(std::memcmp(piece.data, addr, sizeof(addr)) == 0); got_ok = 1; } bfree(&piece); } - assert(got_ok); - (void)got_ok; + SS_CHECK(got_ok); bfree(&buf); c->ctx_release(&enc); @@ -229,8 +222,7 @@ test_tcp_partial_delivery(const char *method) } /* A tampered ciphertext must fail authentication rather than pass through. */ -static void -test_tcp_tamper_detection(const char *method) +void test_tcp_tamper_detection(const char *method) { static const uint8_t addr[] = { 0x01, 127, 0, 0, 1, 0x00, 0x50 }; crypto_t *c = make_crypto(method); @@ -244,16 +236,16 @@ test_tcp_tamper_detection(const char *method) dec.role = CRYPTO_ROLE_SERVER; buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 4096); - memcpy(buf.data, addr, sizeof(addr)); + std::memcpy(buf.data, addr, sizeof(addr)); buf.len = sizeof(addr); - assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); + SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK); /* Flip a bit inside the first header chunk (past the salt). */ buf.data[c->cipher->key_len + 2] ^= 0x01; - assert(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR); + SS_CHECK(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR); bfree(&buf); c->ctx_release(&enc); @@ -262,8 +254,7 @@ test_tcp_tamper_detection(const char *method) } /* UDP packets must round-trip through the session interface. */ -static void -test_udp_roundtrip(const char *method) +void test_udp_roundtrip(const char *method) { static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, /* 8.8.8.8:53 */ @@ -271,26 +262,26 @@ test_udp_roundtrip(const char *method) }; crypto_t *c = make_crypto(method); - assert(c->encrypt_udp != NULL); - assert(c->decrypt_udp != NULL); + SS_CHECK(c->encrypt_udp != nullptr); + SS_CHECK(c->decrypt_udp != nullptr); - void *client_session = NULL; - void *server_session = NULL; + void *client_session = nullptr; + void *server_session = nullptr; buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 2048); - memcpy(buf.data, pkt, sizeof(pkt)); + std::memcpy(buf.data, pkt, sizeof(pkt)); buf.len = sizeof(pkt); aead_set_role(CRYPTO_ROLE_CLIENT); - assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); - assert(buf.len > sizeof(pkt)); + SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); + SS_CHECK(buf.len > sizeof(pkt)); aead_set_role(CRYPTO_ROLE_SERVER); - assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); - assert(buf.len == sizeof(pkt)); - assert(memcmp(buf.data, pkt, sizeof(pkt)) == 0); + SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); + SS_CHECK(buf.len == sizeof(pkt)); + SS_CHECK(std::memcmp(buf.data, pkt, sizeof(pkt)) == 0); bfree(&buf); c->udp_session_release(client_session); @@ -300,39 +291,38 @@ test_udp_roundtrip(const char *method) } /* A replayed UDP packet must be rejected by the sliding window filter. */ -static void -test_udp_replay_rejected(const char *method) +void test_udp_replay_rejected(const char *method) { static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 }; crypto_t *c = make_crypto(method); - void *client_session = NULL; - void *server_session = NULL; + void *client_session = nullptr; + void *server_session = nullptr; buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 2048); - memcpy(buf.data, pkt, sizeof(pkt)); + std::memcpy(buf.data, pkt, sizeof(pkt)); buf.len = sizeof(pkt); aead_set_role(CRYPTO_ROLE_CLIENT); - assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); + SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); /* Keep a copy of the wire packet so we can replay it verbatim. */ buffer_t copy; - memset(©, 0, sizeof(copy)); + std::memset(©, 0, sizeof(copy)); balloc(©, 2048); - memcpy(copy.data, buf.data, buf.len); + std::memcpy(copy.data, buf.data, buf.len); copy.len = buf.len; aead_set_role(CRYPTO_ROLE_SERVER); - assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); + SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); /* A rejected packet must not publish a session into a fresh relay slot. */ - void *rejected_session = NULL; - assert(c->decrypt_udp(©, c->cipher, 2048, &rejected_session) == CRYPTO_ERROR); - assert(rejected_session == NULL); + void *rejected_session = nullptr; + SS_CHECK(c->decrypt_udp(©, c->cipher, 2048, &rejected_session) == CRYPTO_ERROR); + SS_CHECK(rejected_session == nullptr); /* the very same packet again must be refused */ - assert(c->decrypt_udp(©, c->cipher, 2048, &server_session) == CRYPTO_ERROR); + SS_CHECK(c->decrypt_udp(©, c->cipher, 2048, &server_session) == CRYPTO_ERROR); bfree(&buf); bfree(©); @@ -343,17 +333,16 @@ test_udp_replay_rejected(const char *method) } /* Encrypt one client packet into buf and hand it to the server's slot. */ -static void -client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len, +void client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len, void **client_session, void **server_session) { - memcpy(buf->data, pkt, len); + std::memcpy(buf->data, pkt, len); buf->len = len; aead_set_role(CRYPTO_ROLE_CLIENT); - assert(c->encrypt_udp(buf, c->cipher, 2048, client_session) == CRYPTO_OK); + SS_CHECK(c->encrypt_udp(buf, c->cipher, 2048, client_session) == CRYPTO_OK); aead_set_role(CRYPTO_ROLE_SERVER); - assert(c->decrypt_udp(buf, c->cipher, 2048, server_session) == CRYPTO_OK); - assert(buf->len == len && memcmp(buf->data, pkt, len) == 0); + SS_CHECK(c->decrypt_udp(buf, c->cipher, 2048, server_session) == CRYPTO_OK); + SS_CHECK(buf->len == len && std::memcmp(buf->data, pkt, len) == 0); } /* @@ -361,34 +350,33 @@ client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len, * the table with fresh client session IDs used to free the session behind * the relay's back, so the reply path encrypted with freed memory. */ -static void -test_udp_session_survives_table_eviction(const char *method) +void test_udp_session_survives_table_eviction(const char *method) { static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 }; crypto_t *c = make_crypto(method); - void *client_session = NULL; - void *server_session = NULL; + void *client_session = nullptr; + void *server_session = nullptr; buffer_t buf; - memset(&buf, 0, sizeof(buf)); + std::memset(&buf, 0, sizeof(buf)); balloc(&buf, 2048); client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session); - assert(server_session != NULL); + SS_CHECK(server_session != nullptr); void *first = server_session; - void *shared_session = NULL; + void *shared_session = nullptr; client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &shared_session); - assert(shared_session == first); + SS_CHECK(shared_session == first); /* Reattaching the same slot must not acquire an extra reference. */ client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session); - assert(server_session == first); + SS_CHECK(server_session == first); /* Each new client session ID inserts a table entry; overflow evicts ours. */ for (int i = 0; i < AEAD_2022_UDP_SESSION_TABLE_CAP + 1; i++) { - void *other_client = NULL, *other_server = NULL; + void *other_client = nullptr, *other_server = nullptr; client_to_server(c, &buf, pkt, sizeof(pkt), &other_client, &other_server); - assert(other_server != NULL && other_server != first); + SS_CHECK(other_server != nullptr && other_server != first); c->udp_session_release(other_client); c->udp_session_release(other_server); } @@ -397,18 +385,18 @@ test_udp_session_survives_table_eviction(const char *method) c->udp_session_release(shared_session); /* The reply must still encrypt with the evicted session and verify. */ - memcpy(buf.data, pkt, sizeof(pkt)); + std::memcpy(buf.data, pkt, sizeof(pkt)); buf.len = sizeof(pkt); aead_set_role(CRYPTO_ROLE_SERVER); - assert(c->encrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); - assert(server_session == first); + SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK); + SS_CHECK(server_session == first); aead_set_role(CRYPTO_ROLE_CLIENT); - assert(c->decrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); - assert(buf.len == sizeof(pkt) && memcmp(buf.data, pkt, sizeof(pkt)) == 0); + SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK); + SS_CHECK(buf.len == sizeof(pkt) && std::memcmp(buf.data, pkt, sizeof(pkt)) == 0); /* The client's next packet re-creates a table entry and swaps the slot. */ client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session); - assert(server_session != NULL && server_session != first); + SS_CHECK(server_session != nullptr && server_session != first); /* Releasing the slot after eviction must neither leak nor double free. */ c->udp_session_release(server_session); @@ -420,15 +408,14 @@ test_udp_session_survives_table_eviction(const char *method) } /* The 2022 ciphers must reject a PSK of the wrong size rather than pad it. */ -static void -test_psk_parsing(void) +void test_psk_parsing() { uint8_t key[32]; /* Correct size decodes. */ - assert(crypto_parse_psk(PSK32, key, 32) == 32); + SS_CHECK(crypto_parse_psk(PSK32, key, 32) == 32); for (int i = 0; i < 32; i++) - assert(key[i] == 0); + SS_CHECK(key[i] == 0); /* * The standard alphabet ('+' and '/') and the URL-safe one ('-' and '_') @@ -436,38 +423,40 @@ test_psk_parsing(void) * tools using either convention. */ uint8_t k1[32], k2[32]; - assert(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32); - assert(crypto_parse_psk("__________________________________________8=", k2, 32) == 32); - assert(memcmp(k1, k2, 32) == 0); + SS_CHECK(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32); + SS_CHECK(crypto_parse_psk("__________________________________________8=", k2, 32) == 32); + SS_CHECK(std::memcmp(k1, k2, 32) == 0); for (int i = 0; i < 32; i++) - assert(k1[i] == 0xff); + SS_CHECK(k1[i] == 0xff); /* A key with distinct bytes must decode to exactly those bytes. */ uint8_t k3[32]; - assert(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", + SS_CHECK(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", k3, 32) == 32); for (int i = 0; i < 32; i++) - assert(k3[i] == i); + SS_CHECK(k3[i] == i); } -int -main(void) +} // namespace + +int main() { - if (sodium_init() < 0) + if (sodium_init() < 0) { return 1; + } - test_blake3_empty_hash_vector(); - test_blake3_derive_key_known_vector(); - test_psk_parsing(); + SS_RUN(test_blake3_empty_hash_vector); + SS_RUN(test_blake3_derive_key_known_vector); + SS_RUN(test_psk_parsing); - for (size_t i = 0; i < sizeof(methods_2022) / sizeof(methods_2022[0]); i++) { - const char *m = methods_2022[i]; + for (const char *m : methods_2022) { test_tcp_request_roundtrip(m); test_tcp_partial_delivery(m); test_tcp_tamper_detection(m); test_udp_roundtrip(m); test_udp_replay_rejected(m); test_udp_session_survives_table_eviction(m); + std::printf("ok %s\n", m); } return 0; diff --git a/tests/test_base64.c b/tests/test_base64.c deleted file mode 100644 index f9c9d45f..00000000 --- a/tests/test_base64.c +++ /dev/null @@ -1,116 +0,0 @@ -#include -#include -#include - -#include "base64.h" - -static void -test_encode_decode(void) -{ - /* Known test vector: "Hello" */ - const uint8_t input[] = "Hello"; - char encoded[BASE64_SIZE(5)]; - uint8_t decoded[5]; - - char *ret = base64_encode(encoded, sizeof(encoded), input, 5); - assert(ret != NULL); - assert(strlen(encoded) > 0); - - int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); - assert(decoded_len == 5); - assert(memcmp(decoded, input, 5) == 0); -} - -static void -test_empty_input(void) -{ - char encoded[BASE64_SIZE(0)]; - char *ret = base64_encode(encoded, sizeof(encoded), (const uint8_t *)"", 0); - assert(ret != NULL); - assert(strlen(encoded) == 0); -} - -static void -test_single_byte(void) -{ - const uint8_t input[] = { 0x41 }; /* 'A' */ - char encoded[BASE64_SIZE(1)]; - uint8_t decoded[1]; - - char *ret = base64_encode(encoded, sizeof(encoded), input, 1); - assert(ret != NULL); - - int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); - assert(decoded_len == 1); - assert(decoded[0] == 0x41); -} - -static void -test_two_bytes(void) -{ - const uint8_t input[] = { 0x41, 0x42 }; /* "AB" */ - char encoded[BASE64_SIZE(2)]; - uint8_t decoded[2]; - - char *ret = base64_encode(encoded, sizeof(encoded), input, 2); - assert(ret != NULL); - - int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); - assert(decoded_len == 2); - assert(decoded[0] == 0x41); - assert(decoded[1] == 0x42); -} - -static void -test_three_bytes(void) -{ - const uint8_t input[] = { 0x00, 0xFF, 0x80 }; - char encoded[BASE64_SIZE(3)]; - uint8_t decoded[3]; - - char *ret = base64_encode(encoded, sizeof(encoded), input, 3); - assert(ret != NULL); - - int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); - assert(decoded_len == 3); - assert(decoded[0] == 0x00); - assert(decoded[1] == 0xFF); - assert(decoded[2] == 0x80); -} - -static void -test_roundtrip_binary(void) -{ - const uint8_t input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10, 11, 12, 13, 14, 15, 16 }; - char encoded[BASE64_SIZE(17)]; - uint8_t decoded[17]; - - char *ret = base64_encode(encoded, sizeof(encoded), input, 17); - assert(ret != NULL); - - int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); - assert(decoded_len == 17); - assert(memcmp(decoded, input, 17) == 0); -} - -static void -test_invalid_chars(void) -{ - /* Character '!' (ASCII 33) is below the base64 range start ('+' = 43) */ - int decoded_len = base64_decode((uint8_t[4]){}, "!!!!", 4); - assert(decoded_len == -1); -} - -int -main(void) -{ - test_encode_decode(); - test_empty_input(); - test_single_byte(); - test_two_bytes(); - test_three_bytes(); - test_roundtrip_binary(); - test_invalid_chars(); - return 0; -} diff --git a/tests/test_base64_cpp.cpp b/tests/test_base64_cpp.cpp new file mode 100644 index 00000000..0acef9b6 --- /dev/null +++ b/tests/test_base64_cpp.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Base64 encoder/decoder (src/base64.cpp): round trips and invalid input. +#include +#include + +#include "base64.h" +#include "test_helpers.hpp" + +namespace { + +void test_encode_decode() +{ + const uint8_t input[] = "Hello"; + char encoded[BASE64_SIZE(5)]; + uint8_t decoded[5]; + + char *ret = base64_encode(encoded, sizeof(encoded), input, 5); + SS_CHECK(ret != nullptr); + SS_CHECK(std::strlen(encoded) > 0); + + int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); + SS_CHECK(decoded_len == 5); + SS_CHECK(std::memcmp(decoded, input, 5) == 0); +} + +void test_empty_input() +{ + char encoded[BASE64_SIZE(0)]; + char *ret = base64_encode(encoded, sizeof(encoded), reinterpret_cast(""), 0); + SS_CHECK(ret != nullptr); + SS_CHECK(std::strlen(encoded) == 0); +} + +void test_single_byte() +{ + const uint8_t input[] = { 0x41 }; // 'A' + char encoded[BASE64_SIZE(1)]; + uint8_t decoded[1]; + + SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 1) != nullptr); + + int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); + SS_CHECK(decoded_len == 1); + SS_CHECK(decoded[0] == 0x41); +} + +void test_two_bytes() +{ + const uint8_t input[] = { 0x41, 0x42 }; // "AB" + char encoded[BASE64_SIZE(2)]; + uint8_t decoded[2]; + + SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 2) != nullptr); + + int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); + SS_CHECK(decoded_len == 2); + SS_CHECK(decoded[0] == 0x41); + SS_CHECK(decoded[1] == 0x42); +} + +void test_three_bytes() +{ + const uint8_t input[] = { 0x00, 0xFF, 0x80 }; + char encoded[BASE64_SIZE(3)]; + uint8_t decoded[3]; + + SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 3) != nullptr); + + int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); + SS_CHECK(decoded_len == 3); + SS_CHECK(decoded[0] == 0x00); + SS_CHECK(decoded[1] == 0xFF); + SS_CHECK(decoded[2] == 0x80); +} + +void test_roundtrip_binary() +{ + const uint8_t input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; + char encoded[BASE64_SIZE(17)]; + uint8_t decoded[17]; + + SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 17) != nullptr); + + int decoded_len = base64_decode(decoded, encoded, sizeof(decoded)); + SS_CHECK(decoded_len == 17); + SS_CHECK(std::memcmp(decoded, input, 17) == 0); +} + +void test_invalid_chars() +{ + // Character '!' (ASCII 33) is below the base64 range start ('+' = 43). + uint8_t out[4] = {}; + SS_CHECK(base64_decode(out, "!!!!", 4) == -1); +} + +} // namespace + +int main() +{ + SS_RUN(test_encode_decode); + SS_RUN(test_empty_input); + SS_RUN(test_single_byte); + SS_RUN(test_two_bytes); + SS_RUN(test_three_bytes); + SS_RUN(test_roundtrip_binary); + SS_RUN(test_invalid_chars); + return 0; +} diff --git a/tests/test_buffer.c b/tests/test_buffer.c deleted file mode 100644 index dd80f217..00000000 --- a/tests/test_buffer.c +++ /dev/null @@ -1,103 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include - -int verbose = 0; - -#include "crypto.h" - -/* Provide nonce_cache symbol needed by crypto.cpp */ -struct cache *nonce_cache = NULL; - -static void -test_balloc(void) -{ - buffer_t buf; - memset(&buf, 0, sizeof(buf)); - - int ret = balloc(&buf, 100); - assert(ret == 100); - (void)ret; - assert(buf.data != NULL); - assert(buf.capacity >= 100); - assert(buf.len == 0); - assert(buf.idx == 0); - - bfree(&buf); - assert(buf.data == NULL); - assert(buf.capacity == 0); -} - -static void -test_brealloc(void) -{ - buffer_t buf; - memset(&buf, 0, sizeof(buf)); - - balloc(&buf, 50); - buf.len = 10; - - /* Grow the buffer */ - int ret = brealloc(&buf, 10, 200); - assert(ret == 200); - (void)ret; - assert(buf.capacity >= 200); - assert(buf.len == 10); - - bfree(&buf); -} - -static void -test_bprepend(void) -{ - buffer_t dst, src; - memset(&dst, 0, sizeof(dst)); - memset(&src, 0, sizeof(src)); - - balloc(&dst, 100); - balloc(&src, 100); - - /* Put some data in src */ - memcpy(src.data, "HEADER", 6); - src.len = 6; - - /* Put some data in dst */ - memcpy(dst.data, "BODY", 4); - dst.len = 4; - - int ret = bprepend(&dst, &src, 200); - assert(ret == 10); - (void)ret; - assert(dst.len == 10); - assert(memcmp(dst.data, "HEADERBODY", 10) == 0); - - bfree(&dst); - bfree(&src); -} - -static void -test_balloc_zero(void) -{ - buffer_t buf; - memset(&buf, 0, sizeof(buf)); - - int ret = balloc(&buf, 0); - assert(ret == 0); - (void)ret; - /* A zero-capacity buffer should still succeed */ - bfree(&buf); -} - -int -main(void) -{ - test_balloc(); - test_brealloc(); - test_bprepend(); - test_balloc_zero(); - return 0; -} diff --git a/tests/test_buffer_cpp.cpp b/tests/test_buffer_cpp.cpp new file mode 100644 index 00000000..59ae23cc --- /dev/null +++ b/tests/test_buffer_cpp.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Legacy buffer helpers in the C crypto reference (tests/reference/crypto.h): +// balloc, brealloc and bprepend. +#include +#include + +#include "test_helpers.hpp" + +#include "crypto.h" + +extern "C" { +int verbose = 0; +// Provide the nonce_cache symbol needed by the crypto reference. +struct cache *nonce_cache = nullptr; +} + +namespace { + +void test_balloc() +{ + buffer_t buf; + std::memset(&buf, 0, sizeof(buf)); + + SS_CHECK(balloc(&buf, 100) == 100); + SS_CHECK(buf.data != nullptr); + SS_CHECK(buf.capacity >= 100); + SS_CHECK(buf.len == 0); + SS_CHECK(buf.idx == 0); + + bfree(&buf); + SS_CHECK(buf.data == nullptr); + SS_CHECK(buf.capacity == 0); +} + +void test_brealloc() +{ + buffer_t buf; + std::memset(&buf, 0, sizeof(buf)); + + balloc(&buf, 50); + buf.len = 10; + + // Grow the buffer. + SS_CHECK(brealloc(&buf, 10, 200) == 200); + SS_CHECK(buf.capacity >= 200); + SS_CHECK(buf.len == 10); + + bfree(&buf); +} + +void test_bprepend() +{ + buffer_t dst; + buffer_t src; + std::memset(&dst, 0, sizeof(dst)); + std::memset(&src, 0, sizeof(src)); + + balloc(&dst, 100); + balloc(&src, 100); + + std::memcpy(src.data, "HEADER", 6); + src.len = 6; + + std::memcpy(dst.data, "BODY", 4); + dst.len = 4; + + SS_CHECK(bprepend(&dst, &src, 200) == 10); + SS_CHECK(dst.len == 10); + SS_CHECK(std::memcmp(dst.data, "HEADERBODY", 10) == 0); + + bfree(&dst); + bfree(&src); +} + +void test_balloc_zero() +{ + buffer_t buf; + std::memset(&buf, 0, sizeof(buf)); + + SS_CHECK(balloc(&buf, 0) == 0); + // A zero-capacity buffer should still succeed. + bfree(&buf); +} + +} // namespace + +int main() +{ + SS_RUN(test_balloc); + SS_RUN(test_brealloc); + SS_RUN(test_bprepend); + SS_RUN(test_balloc_zero); + return 0; +} diff --git a/tests/test_cache.c b/tests/test_cache.c deleted file mode 100644 index d0e00b7f..00000000 --- a/tests/test_cache.c +++ /dev/null @@ -1,129 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include - -int verbose = 0; - -#include "cache.h" - -static void -test_create_delete(void) -{ - struct cache *c = NULL; - int ret = cache_create(&c, 100, NULL); - assert(ret == 0); - assert(c != NULL); - - ret = cache_delete(c, 0); - assert(ret == 0); - (void)ret; -} - -static void -test_create_null(void) -{ - int ret = cache_create(NULL, 100, NULL); - assert(ret == EINVAL); - (void)ret; -} - -static void -test_insert_lookup(void) -{ - struct cache *c = NULL; - cache_create(&c, 100, NULL); - - char *data = strdup("test_data"); - cache_insert(c, "key1", 4, data); - - char *result = NULL; - cache_lookup(c, "key1", 4, &result); - assert(result != NULL); - assert(strcmp(result, "test_data") == 0); - - cache_delete(c, 0); -} - -static void -test_key_exist(void) -{ - struct cache *c = NULL; - cache_create(&c, 100, NULL); - - char *data = strdup("value"); - cache_insert(c, "mykey", 5, data); - - assert(cache_key_exist(c, "mykey", 5) == 1); - assert(cache_key_exist(c, "nokey", 5) == 0); - - cache_delete(c, 0); -} - -static void -test_remove(void) -{ - struct cache *c = NULL; - cache_create(&c, 100, NULL); - - char *data = strdup("to_remove"); - cache_insert(c, "rmkey", 5, data); - assert(cache_key_exist(c, "rmkey", 5) == 1); - - cache_remove(c, "rmkey", 5); - assert(cache_key_exist(c, "rmkey", 5) == 0); - - cache_delete(c, 0); -} - -static void -test_lookup_missing(void) -{ - struct cache *c = NULL; - cache_create(&c, 100, NULL); - - char *result = (char *)0xdeadbeef; - cache_lookup(c, "missing", 7, &result); - assert(result == NULL); - - cache_delete(c, 0); -} - -static void -test_eviction(void) -{ - struct cache *c = NULL; - cache_create(&c, 3, NULL); - - /* Insert 3 entries to fill cache */ - cache_insert(c, "k1", 2, strdup("v1")); - cache_insert(c, "k2", 2, strdup("v2")); - cache_insert(c, "k3", 2, strdup("v3")); - - /* This should trigger eviction of the oldest entry */ - cache_insert(c, "k4", 2, strdup("v4")); - - /* k1 should have been evicted */ - assert(cache_key_exist(c, "k1", 2) == 0); - /* k4 should exist */ - assert(cache_key_exist(c, "k4", 2) == 1); - - cache_delete(c, 0); -} - -int -main(void) -{ - test_create_delete(); - test_create_null(); - test_insert_lookup(); - test_key_exist(); - test_remove(); - test_lookup_missing(); - test_eviction(); - return 0; -} diff --git a/tests/test_cache_cpp.cpp b/tests/test_cache_cpp.cpp index 9d677e27..a866fcc9 100644 --- a/tests/test_cache_cpp.cpp +++ b/tests/test_cache_cpp.cpp @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ // Edge cases of the fixed-capacity cache (src/cache.cpp) beyond -// test_cache.c, and proof that a full cache churns without allocating. +// test_cache_reference_cpp.cpp, and proof that a full cache churns without allocating. #include #include #include diff --git a/tests/test_cache_reference_cpp.cpp b/tests/test_cache_reference_cpp.cpp new file mode 100644 index 00000000..c5aa8c64 --- /dev/null +++ b/tests/test_cache_reference_cpp.cpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Basic behaviour of the legacy cache (tests/reference/cache.cpp): create, +// insert, lookup, remove and eviction. +#include +#include +#include +#include + +#include "cache.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +char *dup_string(const char *text) +{ + const size_t size = std::strlen(text) + 1; + auto *copy = static_cast(std::malloc(size)); + SS_CHECK(copy != nullptr); + std::memcpy(copy, text, size); + return copy; +} + +// The legacy API takes writable keys; copy literals into a buffer. +struct Key { + char text[16]; + size_t length; + explicit Key(const char *literal) : text{}, length(std::strlen(literal)) + { + SS_CHECK(length < sizeof(text)); + std::memcpy(text, literal, length); + } +}; + +void insert(struct cache *c, const char *key, const char *value) +{ + Key k(key); + cache_insert(c, k.text, k.length, dup_string(value)); +} + +void lookup(struct cache *c, const char *key, char **result) +{ + Key k(key); + cache_lookup(c, k.text, k.length, result); +} + +int exists(struct cache *c, const char *key) +{ + Key k(key); + return cache_key_exist(c, k.text, k.length); +} + +void remove_key(struct cache *c, const char *key) +{ + Key k(key); + cache_remove(c, k.text, k.length); +} + +void test_create_delete() +{ + struct cache *c = nullptr; + SS_CHECK(cache_create(&c, 100, nullptr) == 0); + SS_CHECK(c != nullptr); + SS_CHECK(cache_delete(c, 0) == 0); +} + +void test_create_null() +{ + SS_CHECK(cache_create(nullptr, 100, nullptr) == EINVAL); +} + +void test_insert_lookup() +{ + struct cache *c = nullptr; + cache_create(&c, 100, nullptr); + + insert(c, "key1", "test_data"); + + char *result = nullptr; + lookup(c, "key1", &result); + SS_CHECK(result != nullptr); + SS_CHECK(std::strcmp(result, "test_data") == 0); + + cache_delete(c, 0); +} + +void test_key_exist() +{ + struct cache *c = nullptr; + cache_create(&c, 100, nullptr); + + insert(c, "mykey", "value"); + + SS_CHECK(exists(c, "mykey") == 1); + SS_CHECK(exists(c, "nokey") == 0); + + cache_delete(c, 0); +} + +void test_remove() +{ + struct cache *c = nullptr; + cache_create(&c, 100, nullptr); + + insert(c, "rmkey", "to_remove"); + SS_CHECK(exists(c, "rmkey") == 1); + + remove_key(c, "rmkey"); + SS_CHECK(exists(c, "rmkey") == 0); + + cache_delete(c, 0); +} + +void test_lookup_missing() +{ + struct cache *c = nullptr; + cache_create(&c, 100, nullptr); + + char *result = reinterpret_cast(static_cast(0xdeadbeef)); + lookup(c, "missing", &result); + SS_CHECK(result == nullptr); + + cache_delete(c, 0); +} + +void test_eviction() +{ + struct cache *c = nullptr; + cache_create(&c, 3, nullptr); + + // Fill the cache. + insert(c, "k1", "v1"); + insert(c, "k2", "v2"); + insert(c, "k3", "v3"); + + // This triggers eviction of the oldest entry. + insert(c, "k4", "v4"); + + SS_CHECK(exists(c, "k1") == 0); + SS_CHECK(exists(c, "k4") == 1); + + cache_delete(c, 0); +} + +} // namespace + +int main() +{ + SS_RUN(test_create_delete); + SS_RUN(test_create_null); + SS_RUN(test_insert_lookup); + SS_RUN(test_key_exist); + SS_RUN(test_remove); + SS_RUN(test_lookup_missing); + SS_RUN(test_eviction); + return 0; +} diff --git a/tests/test_crypto_aead2022_cpp.cpp b/tests/test_crypto_aead2022_cpp.cpp index f65e5a1a..99f30196 100644 --- a/tests/test_crypto_aead2022_cpp.cpp +++ b/tests/test_crypto_aead2022_cpp.cpp @@ -398,8 +398,9 @@ void test_methods() salt_pool(); SS_CHECK(!sc::find_aead2022("aes-256-gcm")); auto spec = sc::find_aead2022("2022-blake3-aes-256-gcm"); - SS_CHECK(!sc::Psk::from_base64(*spec, kPsk16)); // too short - SS_CHECK(!sc::Psk::from_base64(*spec, "not base64")); + SS_CHECK(spec.has_value()); + SS_CHECK(!sc::Psk::from_base64(spec.value(), kPsk16)); // too short + SS_CHECK(!sc::Psk::from_base64(spec.value(), "not base64")); } void test_no_heap_in_steady_state() diff --git a/tests/test_crypto.c b/tests/test_crypto_cpp.cpp similarity index 54% rename from tests/test_crypto.c rename to tests/test_crypto_cpp.cpp index 88a91c62..2172806a 100644 --- a/tests/test_crypto.c +++ b/tests/test_crypto_cpp.cpp @@ -1,79 +1,78 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif +// SPDX-License-Identifier: GPL-3.0-or-later +// Legacy crypto reference (tests/reference/crypto.cpp): MD5, key derivation, +// HKDF, key parsing, AEAD salt replay and the printable-salt experiment. +#include +#include +#include -#include -#include -#include #include -int verbose = 0; +#include "test_helpers.hpp" #include "crypto.h" #include "ppbloom.h" #include "utils.h" -/* Provide nonce_cache symbol needed by crypto.cpp */ -struct cache *nonce_cache = NULL; +extern "C" { +int verbose = 0; +// Provide the nonce_cache symbol needed by the crypto reference. +struct cache *nonce_cache = nullptr; +} -static void -test_crypto_md5(void) +namespace { + +void test_crypto_md5() { /* MD5("") = d41d8cd98f00b204e9800998ecf8427e */ unsigned char result[16]; - crypto_md5((const unsigned char *)"", 0, result); + crypto_md5(reinterpret_cast(""), 0, result); unsigned char expected[] = { 0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04, 0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e }; - assert(memcmp(result, expected, 16) == 0); - (void)expected; + SS_CHECK(std::memcmp(result, expected, 16) == 0); /* MD5("abc") = 900150983cd24fb0d6963f7d28e17f72 */ - crypto_md5((const unsigned char *)"abc", 3, result); + crypto_md5(reinterpret_cast("abc"), 3, result); unsigned char expected_abc[] = { 0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0, 0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1, 0x7f, 0x72 }; - assert(memcmp(result, expected_abc, 16) == 0); - (void)expected_abc; + SS_CHECK(std::memcmp(result, expected_abc, 16) == 0); } -static void -test_crypto_derive_key(void) +void test_crypto_derive_key() { uint8_t key[32]; - assert(crypto_derive_key(NULL, key, 32) == 0); + SS_CHECK(crypto_derive_key(nullptr, key, 32) == 0); /* derive_key should produce deterministic output from a password */ int ret = crypto_derive_key("password", key, 32); - assert(ret == 32); + SS_CHECK(ret == 32); /* Same password should produce same key */ uint8_t key2[32]; ret = crypto_derive_key("password", key2, 32); - assert(ret == 32); - assert(memcmp(key, key2, 32) == 0); + SS_CHECK(ret == 32); + SS_CHECK(std::memcmp(key, key2, 32) == 0); /* Different password should produce different key */ uint8_t key3[32]; ret = crypto_derive_key("different", key3, 32); - assert(ret == 32); - assert(memcmp(key, key3, 32) != 0); - (void)ret; + SS_CHECK(ret == 32); + SS_CHECK(std::memcmp(key, key3, 32) != 0); } -static void -test_crypto_hkdf(void) +void test_crypto_hkdf() { /* RFC 5869 Test Case 1 */ const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - assert(md != NULL); + SS_CHECK(md != nullptr); unsigned char ikm[22]; - memset(ikm, 0x0b, 22); + std::memset(ikm, 0x0b, 22); unsigned char salt[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, @@ -88,8 +87,7 @@ test_crypto_hkdf(void) unsigned char okm[42]; int ret = crypto_hkdf(md, salt, sizeof(salt), ikm, sizeof(ikm), info, sizeof(info), okm, sizeof(okm)); - assert(ret == 0); - (void)ret; + SS_CHECK(ret == 0); unsigned char expected_okm[] = { 0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a, @@ -99,18 +97,16 @@ test_crypto_hkdf(void) 0x34, 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18, 0x58, 0x65 }; - assert(memcmp(okm, expected_okm, 42) == 0); - (void)expected_okm; + SS_CHECK(std::memcmp(okm, expected_okm, 42) == 0); } -static void -test_crypto_hkdf_extract(void) +void test_crypto_hkdf_extract() { const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - assert(md != NULL); + SS_CHECK(md != nullptr); unsigned char ikm[22]; - memset(ikm, 0x0b, 22); + std::memset(ikm, 0x0b, 22); unsigned char salt[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, @@ -119,8 +115,7 @@ test_crypto_hkdf_extract(void) unsigned char prk[32]; int ret = crypto_hkdf_extract(md, salt, sizeof(salt), ikm, sizeof(ikm), prk); - assert(ret == 0); - (void)ret; + SS_CHECK(ret == 0); /* RFC 5869 Test Case 1 PRK */ unsigned char expected_prk[] = { @@ -129,12 +124,10 @@ test_crypto_hkdf_extract(void) 0x90, 0xb6, 0xc7, 0x3b, 0xb5, 0x0f, 0x9c, 0x31, 0x22, 0xec, 0x84, 0x4a, 0xd7, 0xc2, 0xb3, 0xe5 }; - assert(memcmp(prk, expected_prk, 32) == 0); - (void)expected_prk; + SS_CHECK(std::memcmp(prk, expected_prk, 32) == 0); } -static void -test_crypto_parse_key(void) +void test_crypto_parse_key() { /* base64_encode uses URL-safe base64 with -_ instead of +/ */ uint8_t key[32]; @@ -143,29 +136,27 @@ test_crypto_parse_key(void) /* 32 zero bytes = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" in standard base64 */ /* With URL-safe: same since no +/ needed */ int ret = crypto_parse_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", key, 32); - assert(ret == 32); - (void)ret; + SS_CHECK(ret == 32); /* All bytes should be 0 */ for (int i = 0; i < 32; i++) { - assert(key[i] == 0); + SS_CHECK(key[i] == 0); } } -static void -test_aead_repeat_salt_rejection_releases_context(void) +void test_aead_repeat_salt_rejection_releases_context() { - crypto_t *crypto = crypto_init("password", NULL, "aes-128-gcm"); - assert(crypto != NULL); + crypto_t *crypto = crypto_init("password", nullptr, "aes-128-gcm"); + SS_CHECK(crypto != nullptr); buffer_t packet; - memset(&packet, 0, sizeof(packet)); + std::memset(&packet, 0, sizeof(packet)); balloc(&packet, 64); - memcpy(packet.data, "payload", 7); + std::memcpy(packet.data, "payload", 7); packet.len = 7; - assert(crypto->encrypt_all(&packet, crypto->cipher, 128) == CRYPTO_OK); - assert(crypto->decrypt_all(&packet, crypto->cipher, 128) == CRYPTO_ERROR); + SS_CHECK(crypto->encrypt_all(&packet, crypto->cipher, 128) == CRYPTO_OK); + SS_CHECK(crypto->decrypt_all(&packet, crypto->cipher, 128) == CRYPTO_ERROR); bfree(&packet); ppbloom_free(); @@ -175,40 +166,39 @@ test_aead_repeat_salt_rejection_releases_context(void) /* A nonprintable sentinel exposes the selected span without adding wire state. * Retain the random tail so every test connection still has a fresh salt. */ -static void -test_printable_salt_lengths(const char *method) +void test_printable_salt_lengths(const char *method) { - crypto_t *crypto = crypto_init("password", NULL, method); - assert(crypto != NULL); + crypto_t *crypto = crypto_init("password", nullptr, method); + SS_CHECK(crypto != nullptr); unsigned seen = 0; - buffer_t buf = {0}; + buffer_t buf{}; balloc(&buf, 2048); for (int sample = 0; sample < 512; sample++) { cipher_ctx_t enc; crypto->ctx_init(crypto->cipher, &enc, 1); enc.printable_salt = 1; - memset(enc.salt, 0xff, 12); + std::memset(enc.salt, 0xff, 12); uint8_t tail[20]; - memcpy(tail, enc.salt + 12, sizeof(tail)); + std::memcpy(tail, enc.salt + 12, sizeof(tail)); buf.data[0] = 'x'; buf.len = 1; - assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); - assert(buf.len == 32 + 2 + 32 + 1); + SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); + SS_CHECK(buf.len == 32 + 2 + 32 + 1); size_t length = 0; while (length < 12 && enc.salt[length] != 0xff) { - assert(enc.salt[length] >= 0x20 && enc.salt[length] <= 0x7e); + SS_CHECK(enc.salt[length] >= 0x20 && enc.salt[length] <= 0x7e); length++; } - assert(length >= 6 && length <= 12); + SS_CHECK(length >= 6 && length <= 12); for (size_t i = length; i < 12; i++) - assert(enc.salt[i] == 0xff); - assert(memcmp(tail, enc.salt + 12, sizeof(tail)) == 0); - assert(memcmp(buf.data, enc.salt, 32) == 0); + SS_CHECK(enc.salt[i] == 0xff); + SS_CHECK(std::memcmp(tail, enc.salt + 12, sizeof(tail)) == 0); + SS_CHECK(std::memcmp(buf.data, enc.salt, 32) == 0); seen |= 1u << (length - 6); crypto->ctx_release(&enc); } /* Probability of missing a length with unbiased sampling is < 4e-34. */ - assert(seen == 0x7f); + SS_CHECK(seen == 0x7f); bfree(&buf); ppbloom_free(); ss_free(crypto->cipher); @@ -217,38 +207,38 @@ test_printable_salt_lengths(const char *method) /* Exercise empty first writes, exact wire overhead, fragmented input, tampering * and replay with the receiver's independent bloom filter. */ -static void -test_printable_salt(const char *method, int enabled) +void test_printable_salt(const char *method, int enabled) { - crypto_t *crypto = crypto_init("password", NULL, method); - assert(crypto != NULL); + crypto_t *crypto = crypto_init("password", nullptr, method); + SS_CHECK(crypto != nullptr); cipher_ctx_t enc, dec; crypto->ctx_init(crypto->cipher, &enc, 1); crypto->ctx_init(crypto->cipher, &dec, 0); - assert(enc.printable_salt == 0); + SS_CHECK(enc.printable_salt == 0); enc.printable_salt = enabled; uint8_t original[32]; - memcpy(original, enc.salt, 32); - buffer_t buf = {0}, fragment = {0}; + std::memcpy(original, enc.salt, 32); + buffer_t buf{}; + buffer_t fragment{}; balloc(&buf, 2048); balloc(&fragment, 2048); - assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); - assert(enc.init == 0 && buf.len == 0); - assert(memcmp(original, enc.salt, 32) == 0); - memcpy(buf.data, "payload", 7); + SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); + SS_CHECK(enc.init == 0 && buf.len == 0); + SS_CHECK(std::memcmp(original, enc.salt, 32) == 0); + std::memcpy(buf.data, "payload", 7); buf.len = 7; - assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); - assert(buf.len == 32 + 2 + 32 + 7); - assert(memcmp(buf.data, enc.salt, 32) == 0); - assert(memcmp(original + 12, enc.salt + 12, 20) == 0); + SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); + SS_CHECK(buf.len == 32 + 2 + 32 + 7); + SS_CHECK(std::memcmp(buf.data, enc.salt, 32) == 0); + SS_CHECK(std::memcmp(original + 12, enc.salt + 12, 20) == 0); if (enabled) { for (int i = 0; i < 6; i++) - assert(enc.salt[i] >= 0x20 && enc.salt[i] <= 0x7e); + SS_CHECK(enc.salt[i] >= 0x20 && enc.salt[i] <= 0x7e); } else { - assert(memcmp(original, enc.salt, 32) == 0); + SS_CHECK(std::memcmp(original, enc.salt, 32) == 0); } uint8_t wire[73]; - memcpy(wire, buf.data, sizeof(wire)); + std::memcpy(wire, buf.data, sizeof(wire)); ppbloom_free(); ppbloom_init(10000, 1e-15); for (size_t i = 0; i < sizeof(wire); i++) { @@ -256,33 +246,33 @@ test_printable_salt(const char *method, int enabled) fragment.len = 1; int result = crypto->decrypt(&fragment, &dec, 2048); if (i + 1 < sizeof(wire)) { - assert(result == CRYPTO_NEED_MORE); + SS_CHECK(result == CRYPTO_NEED_MORE); } else { - assert(result == CRYPTO_OK); - assert(fragment.len == 7 && memcmp(fragment.data, "payload", 7) == 0); + SS_CHECK(result == CRYPTO_OK); + SS_CHECK(fragment.len == 7 && std::memcmp(fragment.data, "payload", 7) == 0); } } - memcpy(original, enc.salt, 32); - memcpy(buf.data, "next", 4); + std::memcpy(original, enc.salt, 32); + std::memcpy(buf.data, "next", 4); buf.len = 4; - assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); - assert(buf.len == 2 + 32 + 4); - assert(memcmp(original, enc.salt, 32) == 0); - assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_OK); - assert(buf.len == 4 && memcmp(buf.data, "next", 4) == 0); + SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK); + SS_CHECK(buf.len == 2 + 32 + 4); + SS_CHECK(std::memcmp(original, enc.salt, 32) == 0); + SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_OK); + SS_CHECK(buf.len == 4 && std::memcmp(buf.data, "next", 4) == 0); crypto->ctx_release(&dec); crypto->ctx_init(crypto->cipher, &dec, 0); - memcpy(buf.data, wire, sizeof(wire)); + std::memcpy(buf.data, wire, sizeof(wire)); buf.len = sizeof(wire); - assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR); + SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR); crypto->ctx_release(&dec); ppbloom_free(); ppbloom_init(10000, 1e-15); crypto->ctx_init(crypto->cipher, &dec, 0); - memcpy(buf.data, wire, sizeof(wire)); + std::memcpy(buf.data, wire, sizeof(wire)); buf.data[sizeof(wire) - 1] ^= 1; buf.len = sizeof(wire); - assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR); + SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR); crypto->ctx_release(&dec); crypto->ctx_release(&enc); bfree(&buf); @@ -292,26 +282,30 @@ test_printable_salt(const char *method, int enabled) ss_free(crypto); } -int -main(void) +} // namespace + +int main() { if (sodium_init() < 0) { return 1; } test_printable_salt_lengths("chacha20-ietf-poly1305"); + std::printf("ok test_printable_salt_lengths chacha20-ietf-poly1305\n"); test_printable_salt_lengths("aes-256-gcm"); + std::printf("ok test_printable_salt_lengths aes-256-gcm\n"); test_printable_salt("chacha20-ietf-poly1305", 0); test_printable_salt("chacha20-ietf-poly1305", 1); test_printable_salt("aes-256-gcm", 0); test_printable_salt("aes-256-gcm", 1); - test_crypto_md5(); - test_crypto_derive_key(); - test_crypto_hkdf(); - test_crypto_hkdf_extract(); - test_crypto_parse_key(); - test_aead_repeat_salt_rejection_releases_context(); + std::printf("ok test_printable_salt\n"); + SS_RUN(test_crypto_md5); + SS_RUN(test_crypto_derive_key); + SS_RUN(test_crypto_hkdf); + SS_RUN(test_crypto_hkdf_extract); + SS_RUN(test_crypto_parse_key); + SS_RUN(test_aead_repeat_salt_rejection_releases_context); - assert(crypto_init("password", NULL, "aes-256-cfb") == NULL); + SS_CHECK(crypto_init("password", nullptr, "aes-256-cfb") == nullptr); return 0; } diff --git a/tests/test_ipset.c b/tests/test_ipset.c deleted file mode 100644 index e0146a1f..00000000 --- a/tests/test_ipset.c +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include -#include -#include "ipset.h" -int verbose = 0; - -static uint32_t state = 42; -static uint32_t -next_random(void) -{ - state = state * 1664525u + 1013904223u; - return state; -} - -static void -check_family(unsigned width) -{ - struct ss_ipset set = {0}; - uint8_t address[16] = {0}; - bool oracle[256] = {false}; - unsigned last = width / 8 - 1; - /* Independent exhaustive bitmap oracle over a /24 or /120. Prefix writes - * can cover the whole space; host deletion must punch holes in them. */ - for (unsigned round = 0; round < 2000; round++) { - unsigned host = next_random() >> 24; - unsigned bits = next_random() % 9; - bool present = (next_random() >> 31) != 0; - unsigned mask = bits == 0 ? 0 : (255u << (8 - bits)) & 255u; - address[last] = (uint8_t)host; - ss_ipset_assign(&set, address, width, width - 8 + bits, present); - for (unsigned i = 0; i < 256; i++) { - if ((i & mask) == (host & mask)) { - oracle[i] = present; - } - address[last] = (uint8_t)i; - assert(ss_ipset_contains(&set, address, width) == oracle[i]); - } - } - ss_ipset_assign(&set, address, width, 0, true); - memset(address, 255, sizeof(address)); - assert(ss_ipset_contains(&set, address, width)); - ss_ipset_assign(&set, address, width, width, false); - assert(!ss_ipset_contains(&set, address, width)); - address[last]--; - assert(ss_ipset_contains(&set, address, width)); - ss_ipset_assign(&set, address, width, width + 1, false); - assert(ss_ipset_contains(&set, address, width)); - ss_ipset_clear(&set); - ss_ipset_clear(&set); - assert(!ss_ipset_contains(&set, address, width)); -} - -int -main(void) -{ - check_family(32); - check_family(128); - return 0; -} diff --git a/tests/test_ipset_cpp.cpp b/tests/test_ipset_cpp.cpp new file mode 100644 index 00000000..d41ffbd3 --- /dev/null +++ b/tests/test_ipset_cpp.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// IP prefix set (src/ipset.cpp) checked against an exhaustive bitmap oracle. +#include +#include + +#include "ipset.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +uint32_t g_state = 42; + +uint32_t next_random() +{ + g_state = g_state * 1664525u + 1013904223u; + return g_state; +} + +void check_family(unsigned width) +{ + ss_ipset set{}; + uint8_t address[16] = {}; + bool oracle[256] = {}; + const unsigned last = width / 8 - 1; + // Independent exhaustive bitmap oracle over a /24 or /120. Prefix writes + // can cover the whole space; host deletion must punch holes in them. + for (unsigned round = 0; round < 2000; round++) { + const unsigned host = next_random() >> 24; + const unsigned bits = next_random() % 9; + const bool present = (next_random() >> 31) != 0; + const unsigned mask = bits == 0 ? 0 : (255u << (8 - bits)) & 255u; + address[last] = static_cast(host); + ss_ipset_assign(&set, address, width, width - 8 + bits, present); + for (unsigned i = 0; i < 256; i++) { + if ((i & mask) == (host & mask)) { + oracle[i] = present; + } + address[last] = static_cast(i); + SS_CHECK(ss_ipset_contains(&set, address, width) == oracle[i]); + } + } + ss_ipset_assign(&set, address, width, 0, true); + std::memset(address, 255, sizeof(address)); + SS_CHECK(ss_ipset_contains(&set, address, width)); + ss_ipset_assign(&set, address, width, width, false); + SS_CHECK(!ss_ipset_contains(&set, address, width)); + address[last]--; + SS_CHECK(ss_ipset_contains(&set, address, width)); + ss_ipset_assign(&set, address, width, width + 1, false); + SS_CHECK(ss_ipset_contains(&set, address, width)); + ss_ipset_clear(&set); + ss_ipset_clear(&set); + SS_CHECK(!ss_ipset_contains(&set, address, width)); +} + +void test_ipv4() +{ + check_family(32); +} + +void test_ipv6() +{ + check_family(128); +} + +} // namespace + +int main() +{ + SS_RUN(test_ipv4); + SS_RUN(test_ipv6); + return 0; +} diff --git a/tests/test_ipset_reference.c b/tests/test_ipset_reference.c deleted file mode 100644 index b8a3405a..00000000 --- a/tests/test_ipset_reference.c +++ /dev/null @@ -1,59 +0,0 @@ -/* Optional migration differential test. Link against the pre-migration ipset - * and cork archives; these are intentionally not production dependencies. */ -#include -#include -#include -#include -#include "ipset.h" -int verbose = 0; -static uint32_t state = 0x725cab91; -static uint32_t next_random(void) -{ - state ^= state << 13; - state ^= state >> 17; - state ^= state << 5; - return state; -} -static void check_family(unsigned width) -{ - struct ss_ipset current = {0}; - struct ip_set reference; - ipset_init(&reference); - for (unsigned round = 0; round < 2000; round++) { - uint8_t bytes[16]; - for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random(); - unsigned prefix = next_random() % (width + 1); - bool present = (next_random() & 1) != 0; - ss_ipset_assign(¤t, bytes, width, prefix, present); - struct cork_ipv4 v4; - struct cork_ipv6 v6; - memcpy(v4._.u8, bytes, sizeof(v4._.u8)); - memcpy(v6._.u8, bytes, sizeof(v6._.u8)); - if (width == 32) { - if (present) ipset_ipv4_add_network(&reference, &v4, prefix); - else ipset_ipv4_remove_network(&reference, &v4, prefix); - } else { - if (present) ipset_ipv6_add_network(&reference, &v6, prefix); - else ipset_ipv6_remove_network(&reference, &v6, prefix); - } - for (unsigned sample = 0; sample < 257; sample++) { - if (sample != 0) { - for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random(); - } - memcpy(v4._.u8, bytes, sizeof(v4._.u8)); - memcpy(v6._.u8, bytes, sizeof(v6._.u8)); - bool old = width == 32 ? ipset_contains_ipv4(&reference, &v4) : ipset_contains_ipv6(&reference, &v6); - assert(old == ss_ipset_contains(¤t, bytes, width)); - } - } - ipset_done(&reference); - ss_ipset_clear(¤t); -} -int main(void) -{ - ipset_init_library(); - check_family(32); - check_family(128); - puts("1,028,000 IPv4/IPv6 membership comparisons matched the original libipset"); - return 0; -} diff --git a/tests/test_ipset_reference_cpp.cpp b/tests/test_ipset_reference_cpp.cpp new file mode 100644 index 00000000..dc259e48 --- /dev/null +++ b/tests/test_ipset_reference_cpp.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Optional migration differential test. Link against the pre-migration ipset +// and cork archives; these are intentionally not production dependencies. +#include +#include +#include + +#include + +#include "ipset.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +uint32_t g_state = 0x725cab91; + +uint32_t next_random() +{ + g_state ^= g_state << 13; + g_state ^= g_state >> 17; + g_state ^= g_state << 5; + return g_state; +} + +void check_family(unsigned width) +{ + ss_ipset current{}; + ip_set reference; + ipset_init(&reference); + for (unsigned round = 0; round < 2000; round++) { + uint8_t bytes[16]; + for (unsigned i = 0; i < sizeof(bytes); i++) { + bytes[i] = static_cast(next_random()); + } + const unsigned prefix = next_random() % (width + 1); + const bool present = (next_random() & 1) != 0; + ss_ipset_assign(¤t, bytes, width, prefix, present); + cork_ipv4 v4; + cork_ipv6 v6; + std::memcpy(v4._.u8, bytes, sizeof(v4._.u8)); + std::memcpy(v6._.u8, bytes, sizeof(v6._.u8)); + if (width == 32) { + if (present) { + ipset_ipv4_add_network(&reference, &v4, prefix); + } else { + ipset_ipv4_remove_network(&reference, &v4, prefix); + } + } else { + if (present) { + ipset_ipv6_add_network(&reference, &v6, prefix); + } else { + ipset_ipv6_remove_network(&reference, &v6, prefix); + } + } + for (unsigned sample = 0; sample < 257; sample++) { + if (sample != 0) { + for (unsigned i = 0; i < sizeof(bytes); i++) { + bytes[i] = static_cast(next_random()); + } + } + std::memcpy(v4._.u8, bytes, sizeof(v4._.u8)); + std::memcpy(v6._.u8, bytes, sizeof(v6._.u8)); + const bool old = width == 32 ? ipset_contains_ipv4(&reference, &v4) : ipset_contains_ipv6(&reference, &v6); + SS_CHECK(old == ss_ipset_contains(¤t, bytes, width)); + } + } + ipset_done(&reference); + ss_ipset_clear(¤t); +} + +} // namespace + +int main() +{ + ipset_init_library(); + check_family(32); + check_family(128); + std::puts("1,028,000 IPv4/IPv6 membership comparisons matched the original libipset"); + return 0; +} diff --git a/tests/test_jconf.c b/tests/test_jconf.c deleted file mode 100644 index 36353325..00000000 --- a/tests/test_jconf.c +++ /dev/null @@ -1,253 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include -#include "test_helpers.h" -#ifndef _WIN32 -#include -#endif - -int verbose = 0; - -#include "netutils.h" -#include "jconf.h" - -static void -test_parse_addr_ipv4_with_port(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("192.168.1.1:8080", &addr); - assert(addr.host != NULL); - assert(addr.port != NULL); - assert(strcmp(addr.host, "192.168.1.1") == 0); - assert(strcmp(addr.port, "8080") == 0); - free_addr(&addr); -} - -static void -test_parse_addr_ipv6_with_port(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("[::1]:443", &addr); - assert(addr.host != NULL); - assert(addr.port != NULL); - assert(strcmp(addr.host, "::1") == 0); - assert(strcmp(addr.port, "443") == 0); - free_addr(&addr); -} - -static void -test_parse_addr_hostname_with_port(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("example.com:1234", &addr); - assert(addr.host != NULL); - assert(addr.port != NULL); - assert(strcmp(addr.host, "example.com") == 0); - assert(strcmp(addr.port, "1234") == 0); - free_addr(&addr); -} - -static void -test_parse_addr_no_port(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("10.0.0.1", &addr); - assert(addr.host != NULL); - assert(strcmp(addr.host, "10.0.0.1") == 0); - /* Port may be NULL when no port is specified */ - free_addr(&addr); -} - -static void -test_parse_addr_ipv6_no_port(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("::1", &addr); - assert(addr.host != NULL); - assert(strcmp(addr.host, "::1") == 0); - free_addr(&addr); -} - -static void -test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("[::1", &addr); - assert(addr.host != NULL); - assert(addr.port == NULL); - assert(strcmp(addr.host, "[::1") == 0); - free_addr(&addr); -} - -static void -test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(void) -{ - ss_addr_t addr; - memset(&addr, 0, sizeof(addr)); - parse_addr("bad::host", &addr); - assert(addr.host != NULL); - assert(addr.port == NULL); - assert(strcmp(addr.host, "bad::host") == 0); - free_addr(&addr); -} - -static void -test_read_jconf_preserves_64bit_integer_strings(void) -{ - char path[4096]; - FILE *f = test_tempfile(path, sizeof(path)); - assert(f != NULL); - fprintf(f, "{\n"); - fprintf(f, "\"server\":\"127.0.0.1\",\n"); - fprintf(f, "\"server_port\":4294967296,\n"); - fprintf(f, "\"password\":\"password\",\n"); - fprintf(f, "\"method\":\"aes-128-gcm\"\n"); - fprintf(f, "}\n"); - fclose(f); - - jconf_t *conf = read_jconf(path); - assert(conf != NULL); - assert(conf->remote_port != NULL); - assert(strcmp(conf->remote_port, "4294967296") == 0); - - remove(path); -} - -static void -write_minimal_config(FILE *f, const char *extra_line) -{ - fprintf(f, "{\n"); - fprintf(f, "\"server\":\"127.0.0.1\",\n"); - fprintf(f, "\"server_port\":8388,\n"); - fprintf(f, "\"password\":\"password\",\n"); - fprintf(f, "\"method\":\"aes-128-gcm\""); - if (extra_line != NULL) { - fprintf(f, ",\n%s", extra_line); - } - fprintf(f, "\n}\n"); -} - -static const char *test_executable; -static void -test_read_jconf_rejects_out_of_range_int_options(void) -{ - char path[4096]; - FILE *f = test_tempfile(path, sizeof(path)); - assert(f != NULL); - write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648"); - fclose(f); - -#ifdef _WIN32 - char command[3 * 4096]; - assert(snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"", - test_executable, path) < (int)sizeof(command)); - STARTUPINFOA startup = {0}; - PROCESS_INFORMATION child = {0}; - startup.cb = sizeof(startup); - assert(CreateProcessA(test_executable, command, NULL, NULL, FALSE, 0, - NULL, NULL, &startup, &child)); - CloseHandle(child.hThread); - assert(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0); - DWORD status; - assert(GetExitCodeProcess(child.hProcess, &status)); - CloseHandle(child.hProcess); - /* FATAL exits with -1, which is a valid nonzero DWORD exit status. */ - assert(status != 0); -#else - pid_t pid = fork(); - assert(pid >= 0); - if (pid == 0) { - read_jconf(path); - _exit(0); - } - - int status; - assert(waitpid(pid, &status, 0) == pid); - assert(WIFEXITED(status)); - assert(WEXITSTATUS(status) != 0); -#endif - - remove(path); -} - -static void -test_server_endpoints(void) -{ - const char *valid[][3] = { - { "example.com:8388", "example.com", "8388" }, - { "127.0.0.1:1", "127.0.0.1", "1" }, - { "[2001:db8::1]:65535", "2001:db8::1", "65535" }, - { "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" }, - { "[fe80::1%3]:8388", "fe80::1%3", "8388" }, - { "2001:db8::1:8388", "2001:db8::1:8388", NULL }, - { "fe80::1%eth0", "fe80::1%eth0", NULL }, - { "::", "::", NULL }, - { "[::1]", "::1", NULL }, - { "example.com", "example.com", NULL } - }; - for (size_t i = 0; i < sizeof(valid) / sizeof(valid[0]); i++) { - ss_addr_t addr = { 0 }; - assert(parse_server_endpoint(valid[i][0], &addr) == 0); - assert(strcmp(addr.host, valid[i][1]) == 0); - assert(valid[i][2] ? addr.port && strcmp(addr.port, valid[i][2]) == 0 : addr.port == NULL); - free_addr(&addr); - } - const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536", - "host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk", - "[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80", - "[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80", - "[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" }; - for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { - ss_addr_t addr = { 0 }; - assert(parse_server_endpoint(invalid[i], &addr) == -1); - assert(addr.host == NULL && addr.port == NULL); - } - ss_addr_t addr[2] = { { 0 }, { 0 } }; - assert(parse_server_endpoint("[::1]:8388", &addr[0]) == 0); - assert(parse_server_endpoint("localhost:8389", &addr[1]) == 0); - assert(complete_server_ports(addr, 2, NULL, 0) == 0); - assert(complete_server_ports(addr, 2, "9000", 0) == 0); - assert(strcmp(addr[0].port, "8388") == 0 && strcmp(addr[1].port, "8389") == 0); - assert(complete_server_ports(addr, 2, NULL, 1) == -1); - free_addr(&addr[1]); - memset(&addr[1], 0, sizeof(addr[1])); - assert(parse_server_endpoint("::1", &addr[1]) == 0); - assert(complete_server_ports(addr, 2, NULL, 0) == -1); - assert(complete_server_ports(addr, 2, "8388", 1) == 0); - assert(strcmp(addr[1].port, "8388") == 0); - free_addr(&addr[0]); - free_addr(&addr[1]); -} - -int -main(int argc, char **argv) -{ - test_executable = argv[0]; - test_network_init(); - if (argc == 3 && strcmp(argv[1], "--read-config") == 0) { - read_jconf(argv[2]); - return 0; - } - test_server_endpoints(); - test_parse_addr_ipv4_with_port(); - test_parse_addr_ipv6_with_port(); - test_parse_addr_hostname_with_port(); - test_parse_addr_no_port(); - test_parse_addr_ipv6_no_port(); - test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(); - test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(); - test_read_jconf_preserves_64bit_integer_strings(); - test_read_jconf_rejects_out_of_range_int_options(); - return 0; -} diff --git a/tests/test_jconf_cpp.cpp b/tests/test_jconf_cpp.cpp new file mode 100644 index 00000000..c8c1937a --- /dev/null +++ b/tests/test_jconf_cpp.cpp @@ -0,0 +1,294 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// Configuration parsing: the address splitters and read_jconf, whose result +// lives in the configuration arena. + +#include +#include +#include + +#include "test_helpers.hpp" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "test_helpers.h" +#ifndef _WIN32 +#include +#endif + +extern "C" int verbose; +int verbose = 0; + +#include "netutils.h" // defines ss_addr_t, which jconf.h uses +#include "jconf.h" + +namespace { + +bool +same(const char *a, const char *b) +{ + return a == nullptr ? b == nullptr : b != nullptr && std::strcmp(a, b) == 0; +} + +struct AddrCase { + const char *input; + const char *host; + const char *port; +}; + +// parse_addr keeps anything it cannot split whole, so malformed bracketed +// IPv6 and IPv6-like host names are not rewritten. +constexpr AddrCase kAddrCases[] = { + { "192.168.1.1:8080", "192.168.1.1", "8080" }, + { "[::1]:443", "::1", "443" }, + { "example.com:1234", "example.com", "1234" }, + { "10.0.0.1", "10.0.0.1", nullptr }, + { "::1", "::1", nullptr }, + { "[::1", "[::1", nullptr }, + { "bad::host", "bad::host", nullptr }, + { "[::1]:", "::1", nullptr }, + { "[::1]junk", "::1", nullptr }, + { "host:", "host", nullptr }, + { "", "", nullptr }, +}; + +void +test_parse_addr() +{ + for (const AddrCase &c : kAddrCases) { + ss_addr_t addr = {}; + parse_addr(c.input, &addr); + SS_CHECK(same(addr.host, c.host)); + SS_CHECK(same(addr.port, c.port)); + free_addr(&addr); + } +} + +void +write_config(const char *path, const char *body) +{ + FILE *f = std::fopen(path, "wb"); + SS_CHECK(f != nullptr); + std::fputs(body, f); + std::fclose(f); +} + +void +test_read_jconf_preserves_64bit_integer_strings() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fclose(f); + write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":4294967296,\n" + "\"password\":\"password\",\n\"method\":\"aes-128-gcm\"\n}\n"); + + jconf_t *conf = read_jconf(path); + SS_CHECK(conf != nullptr); + SS_CHECK(same(conf->remote_port, "4294967296")); + std::remove(path); +} + +// Every field that read_jconf copies out of the parse tree, which is gone by +// the time it returns. +void +test_read_jconf_copies_out_of_the_parse_tree() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fclose(f); + write_config(path, + "{\n" + " \"server\": [\"192.168.1.1:8080\", \"[::1]:443\", \"example.com\", \"bad::host\"],\n" + " \"server_port\": 8388,\n" + " \"local_address\": \"127.0.0.1\",\n" + " \"local_port\": \"1080\",\n" + " \"password\": \"pass\\u00e9word\",\n" + " \"method\": \"chacha20-ietf-poly1305\",\n" + " \"timeout\": 60,\n" + " \"plugin\": \"\",\n" + " \"plugin_opts\": \"a=b;c=d\",\n" + " \"mode\": \"tcp_and_udp\",\n" + " \"port_password\": {\"8381\": \"one\", \"8382\": \"two\", \"8383\": 3},\n" + " \"dscp\": {\"8381\": \"EF\", \"8382\": \"AF41\"},\n" + " \"acl\": null,\n" + " \"max_connections\": 64\n" + "}\n"); + + jconf_t *conf = read_jconf(path); + SS_CHECK(conf != nullptr); + SS_CHECK(conf->remote_num == 4); + SS_CHECK(same(conf->remote_addr[0].host, "192.168.1.1") && same(conf->remote_addr[0].port, "8080")); + SS_CHECK(same(conf->remote_addr[1].host, "::1") && same(conf->remote_addr[1].port, "443")); + SS_CHECK(same(conf->remote_addr[2].host, "example.com") && conf->remote_addr[2].port == nullptr); + SS_CHECK(same(conf->remote_addr[3].host, "bad::host") && conf->remote_addr[3].port == nullptr); + SS_CHECK(same(conf->remote_port, "8388")); + SS_CHECK(same(conf->local_addr, "127.0.0.1")); + SS_CHECK(same(conf->local_port, "1080")); + SS_CHECK(same(conf->password, "pass\xc3\xa9word")); + SS_CHECK(same(conf->method, "chacha20-ietf-poly1305")); + SS_CHECK(same(conf->timeout, "60")); + SS_CHECK(conf->plugin == nullptr); + SS_CHECK(same(conf->plugin_opts, "a=b;c=d")); + SS_CHECK(conf->mode == TCP_AND_UDP); + // Non-string passwords are skipped, as before. + SS_CHECK(conf->port_password_num == 2); + SS_CHECK(same(conf->port_password[0].port, "8381") && same(conf->port_password[0].password, "one")); + SS_CHECK(same(conf->port_password[1].port, "8382") && same(conf->port_password[1].password, "two")); + SS_CHECK(conf->dscp_num == 2); + SS_CHECK(same(conf->dscp[0].port, "8381") && conf->dscp[0].dscp == 46); + SS_CHECK(same(conf->dscp[1].port, "8382") && conf->dscp[1].dscp == 34); + SS_CHECK(conf->acl == nullptr); + SS_CHECK(conf->max_connections == 64); + + // A second read replaces the first one. + write_config(path, "{\"server\":\"10.0.0.1:1\",\"method\":\"aes-256-gcm\",\"password\":\"x\"}"); + conf = read_jconf(path); + SS_CHECK(conf != nullptr); + SS_CHECK(conf->remote_num == 1); + SS_CHECK(same(conf->remote_addr[0].host, "10.0.0.1") && same(conf->remote_addr[0].port, "1")); + SS_CHECK(same(conf->method, "aes-256-gcm")); + SS_CHECK(conf->remote_port == nullptr && conf->port_password_num == 0); + std::remove(path); +} + +// The largest accepted file must fit both arenas: many short values cost the +// most tree per input byte. +void +test_read_jconf_dense_file_fits() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fputs("{\"method\":\"aes-128-gcm\",\"password\":\"p\",\"server\":\"h\",\"x\":[", f); + long written = std::ftell(f); + while (written < MAX_CONF_SIZE - 16) { + std::fputs("[0],", f); + written += 4; + } + std::fputs("0]}", f); + std::fclose(f); + + jconf_t *conf = read_jconf(path); + SS_CHECK(conf != nullptr); + SS_CHECK(same(conf->method, "aes-128-gcm")); + SS_CHECK(same(conf->remote_addr[0].host, "h")); + std::remove(path); +} + +const char *test_executable; + +void +test_read_jconf_rejects_out_of_range_int_options() +{ + char path[4096]; + FILE *f = test_tempfile(path, sizeof(path)); + SS_CHECK(f != nullptr); + std::fclose(f); + write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":8388,\n\"password\":\"password\",\n" + "\"method\":\"aes-128-gcm\",\n\"tcp_incoming_sndbuf\":2147483648\n}\n"); + +#ifdef _WIN32 + char command[3 * 4096]; + SS_CHECK(std::snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"", test_executable, path) + < static_cast(sizeof(command))); + STARTUPINFOA startup = {}; + PROCESS_INFORMATION child = {}; + startup.cb = sizeof(startup); + SS_CHECK(CreateProcessA(test_executable, command, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, + &child)); + CloseHandle(child.hThread); + SS_CHECK(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0); + DWORD status; + SS_CHECK(GetExitCodeProcess(child.hProcess, &status)); + CloseHandle(child.hProcess); + // FATAL exits with -1, which is a valid nonzero DWORD exit status. + SS_CHECK(status != 0); +#else + std::fflush(stdout); // the child's FATAL exit would flush it again + pid_t pid = fork(); + SS_CHECK(pid >= 0); + if (pid == 0) { + read_jconf(path); + _exit(0); + } + + int status; + SS_CHECK(waitpid(pid, &status, 0) == pid); + SS_CHECK(WIFEXITED(status)); + SS_CHECK(WEXITSTATUS(status) != 0); +#endif + + std::remove(path); +} + +void +test_server_endpoints() +{ + const AddrCase valid[] = { + { "example.com:8388", "example.com", "8388" }, + { "127.0.0.1:1", "127.0.0.1", "1" }, + { "[2001:db8::1]:65535", "2001:db8::1", "65535" }, + { "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" }, + { "[fe80::1%3]:8388", "fe80::1%3", "8388" }, + { "2001:db8::1:8388", "2001:db8::1:8388", nullptr }, + { "fe80::1%eth0", "fe80::1%eth0", nullptr }, + { "::", "::", nullptr }, + { "[::1]", "::1", nullptr }, + { "example.com", "example.com", nullptr }, + }; + for (const AddrCase &c : valid) { + ss_addr_t addr = {}; + SS_CHECK(parse_server_endpoint(c.input, &addr) == 0); + SS_CHECK(same(addr.host, c.host)); + SS_CHECK(same(addr.port, c.port)); + free_addr(&addr); + } + const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536", + "host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk", + "[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80", + "[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80", + "[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" }; + for (const char *input : invalid) { + ss_addr_t addr = {}; + SS_CHECK(parse_server_endpoint(input, &addr) == -1); + SS_CHECK(addr.host == nullptr && addr.port == nullptr); + } + ss_addr_t addr[2] = {}; + SS_CHECK(parse_server_endpoint("[::1]:8388", &addr[0]) == 0); + SS_CHECK(parse_server_endpoint("localhost:8389", &addr[1]) == 0); + SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == 0); + SS_CHECK(complete_server_ports(addr, 2, "9000", 0) == 0); + SS_CHECK(same(addr[0].port, "8388") && same(addr[1].port, "8389")); + SS_CHECK(complete_server_ports(addr, 2, nullptr, 1) == -1); + free_addr(&addr[1]); + addr[1] = {}; + SS_CHECK(parse_server_endpoint("::1", &addr[1]) == 0); + SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == -1); + SS_CHECK(complete_server_ports(addr, 2, "8388", 1) == 0); + SS_CHECK(same(addr[1].port, "8388")); + free_addr(&addr[0]); + free_addr(&addr[1]); +} + +} // namespace + +int +main(int argc, char **argv) +{ + test_executable = argv[0]; + test_network_init(); + if (argc == 3 && std::strcmp(argv[1], "--read-config") == 0) { + read_jconf(argv[2]); + return 0; + } + SS_RUN(test_server_endpoints); + SS_RUN(test_parse_addr); + SS_RUN(test_read_jconf_preserves_64bit_integer_strings); + SS_RUN(test_read_jconf_copies_out_of_the_parse_tree); + SS_RUN(test_read_jconf_dense_file_fits); + SS_RUN(test_read_jconf_rejects_out_of_range_int_options); + return 0; +} diff --git a/tests/test_json.c b/tests/test_json.c deleted file mode 100644 index 64f87e23..00000000 --- a/tests/test_json.c +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include -#include - -#include "json.h" - -static void -test_parse_simple_object(void) -{ - const char *json_str = "{\"key\": \"value\", \"num\": 42}"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_object); - assert(val->u.object.length == 2); - - /* Check first entry */ - assert(strcmp(val->u.object.values[0].name, "key") == 0); - assert(val->u.object.values[0].value->type == json_string); - assert(strcmp(val->u.object.values[0].value->u.string.ptr, "value") == 0); - - /* Check second entry */ - assert(strcmp(val->u.object.values[1].name, "num") == 0); - assert(val->u.object.values[1].value->type == json_integer); - assert(val->u.object.values[1].value->u.integer == 42); - - json_value_free(val); -} - -static void -test_parse_array(void) -{ - const char *json_str = "[1, 2, 3]"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_array); - assert(val->u.array.length == 3); - assert(val->u.array.values[0]->type == json_integer); - assert(val->u.array.values[0]->u.integer == 1); - assert(val->u.array.values[1]->u.integer == 2); - assert(val->u.array.values[2]->u.integer == 3); - - json_value_free(val); -} - -static void -test_parse_nested(void) -{ - const char *json_str = "{\"outer\": {\"inner\": true}}"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_object); - assert(val->u.object.length == 1); - - json_value *outer = val->u.object.values[0].value; - assert(outer->type == json_object); - assert(outer->u.object.length == 1); - - assert(outer->u.object.values[0].value->type == json_boolean); - assert(outer->u.object.values[0].value->u.boolean != 0); - - json_value_free(val); -} - -static void -test_parse_types(void) -{ - const char *json_str = "{\"s\": \"hello\", \"i\": -5, \"d\": 3.14, \"b\": false, \"n\": null}"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_object); - assert(val->u.object.length == 5); - - assert(val->u.object.values[0].value->type == json_string); - assert(val->u.object.values[1].value->type == json_integer); - assert(val->u.object.values[1].value->u.integer == -5); - assert(val->u.object.values[2].value->type == json_double); - assert(val->u.object.values[3].value->type == json_boolean); - assert(val->u.object.values[3].value->u.boolean == 0); - assert(val->u.object.values[4].value->type == json_null); - - json_value_free(val); -} - -static void -test_parse_invalid(void) -{ - /* Missing closing brace */ - assert(json_parse("{\"key\": 1", 9) == NULL); - - /* Empty string */ - assert(json_parse("", 0) == NULL); - - /* Just garbage */ - assert(json_parse("not json", 8) == NULL); -} - -static void -test_parse_truncated_escape_sequences(void) -{ - /* Four bytes remain after 'u', but the fourth hex read would hit EOF. */ - const char *truncated_unicode_escape = "\"\\uB2D"; - assert(json_parse(truncated_unicode_escape, strlen(truncated_unicode_escape)) == NULL); - - /* Six bytes remain after the high surrogate, but the sixth read would hit EOF. */ - const char *truncated_surrogate_pair = "\"\\udfff\\ufff"; - assert(json_parse(truncated_surrogate_pair, strlen(truncated_surrogate_pair)) == NULL); -} - -static void -test_parse_truncated_literals(void) -{ - assert(json_parse("tru", 3) == NULL); - assert(json_parse("fals", 4) == NULL); - assert(json_parse("nul", 3) == NULL); -} - -static void -test_parse_empty_object(void) -{ - const char *json_str = "{}"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_object); - assert(val->u.object.length == 0); - json_value_free(val); -} - -static void -test_parse_empty_array(void) -{ - const char *json_str = "[]"; - json_value *val = json_parse(json_str, strlen(json_str)); - assert(val != NULL); - assert(val->type == json_array); - assert(val->u.array.length == 0); - json_value_free(val); -} - -int -main(void) -{ - const char *wide_numbers = "[4294967296,9223372036854775807,-4294967296,0.4294967296]"; - json_value *wide = json_parse(wide_numbers, strlen(wide_numbers)); - assert(wide != NULL && wide->type == json_array && wide->u.array.length == 4); - assert(wide->u.array.values[0]->u.integer == INT64_C(4294967296)); - assert(wide->u.array.values[1]->u.integer == INT64_MAX); - assert(wide->u.array.values[2]->u.integer == -INT64_C(4294967296)); - assert(wide->u.array.values[3]->u.dbl > 0.4294967295 && wide->u.array.values[3]->u.dbl < 0.4294967297); - json_value_free(wide); - assert(json_parse("9223372036854775808", 19) == NULL); - test_parse_simple_object(); - test_parse_array(); - test_parse_nested(); - test_parse_types(); - test_parse_invalid(); - test_parse_truncated_escape_sequences(); - test_parse_truncated_literals(); - test_parse_empty_object(); - test_parse_empty_array(); - return 0; -} diff --git a/tests/test_json_cpp.cpp b/tests/test_json_cpp.cpp new file mode 100644 index 00000000..a2e77a64 --- /dev/null +++ b/tests/test_json_cpp.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// JSON parser (src/json.cpp): values, nesting and malformed input. +#include +#include + +#include "json.h" +#include "test_helpers.hpp" + +namespace { + +json_value *parse(const char *text) +{ + return json_parse(text, std::strlen(text)); +} + +void test_parse_simple_object() +{ + json_value *val = parse("{\"key\": \"value\", \"num\": 42}"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_object); + SS_CHECK(val->u.object.length == 2); + + // First entry. + SS_CHECK(std::strcmp(val->u.object.values[0].name, "key") == 0); + SS_CHECK(val->u.object.values[0].value->type == json_string); + SS_CHECK(std::strcmp(val->u.object.values[0].value->u.string.ptr, "value") == 0); + + // Second entry. + SS_CHECK(std::strcmp(val->u.object.values[1].name, "num") == 0); + SS_CHECK(val->u.object.values[1].value->type == json_integer); + SS_CHECK(val->u.object.values[1].value->u.integer == 42); + + json_value_free(val); +} + +void test_parse_array() +{ + json_value *val = parse("[1, 2, 3]"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_array); + SS_CHECK(val->u.array.length == 3); + SS_CHECK(val->u.array.values[0]->type == json_integer); + SS_CHECK(val->u.array.values[0]->u.integer == 1); + SS_CHECK(val->u.array.values[1]->u.integer == 2); + SS_CHECK(val->u.array.values[2]->u.integer == 3); + + json_value_free(val); +} + +void test_parse_nested() +{ + json_value *val = parse("{\"outer\": {\"inner\": true}}"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_object); + SS_CHECK(val->u.object.length == 1); + + json_value *outer = val->u.object.values[0].value; + SS_CHECK(outer->type == json_object); + SS_CHECK(outer->u.object.length == 1); + + SS_CHECK(outer->u.object.values[0].value->type == json_boolean); + SS_CHECK(outer->u.object.values[0].value->u.boolean != 0); + + json_value_free(val); +} + +void test_parse_types() +{ + json_value *val = parse("{\"s\": \"hello\", \"i\": -5, \"d\": 3.14, \"b\": false, \"n\": null}"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_object); + SS_CHECK(val->u.object.length == 5); + + SS_CHECK(val->u.object.values[0].value->type == json_string); + SS_CHECK(val->u.object.values[1].value->type == json_integer); + SS_CHECK(val->u.object.values[1].value->u.integer == -5); + SS_CHECK(val->u.object.values[2].value->type == json_double); + SS_CHECK(val->u.object.values[3].value->type == json_boolean); + SS_CHECK(val->u.object.values[3].value->u.boolean == 0); + SS_CHECK(val->u.object.values[4].value->type == json_null); + + json_value_free(val); +} + +void test_parse_invalid() +{ + // Missing closing brace. + SS_CHECK(json_parse("{\"key\": 1", 9) == nullptr); + + // Empty string. + SS_CHECK(json_parse("", 0) == nullptr); + + // Just garbage. + SS_CHECK(json_parse("not json", 8) == nullptr); +} + +void test_parse_truncated_escape_sequences() +{ + // Four bytes remain after 'u', but the fourth hex read would hit EOF. + SS_CHECK(parse("\"\\uB2D") == nullptr); + + // Six bytes remain after the high surrogate, but the sixth read would hit EOF. + SS_CHECK(parse("\"\\udfff\\ufff") == nullptr); +} + +void test_parse_truncated_literals() +{ + SS_CHECK(json_parse("tru", 3) == nullptr); + SS_CHECK(json_parse("fals", 4) == nullptr); + SS_CHECK(json_parse("nul", 3) == nullptr); +} + +void test_parse_empty_object() +{ + json_value *val = parse("{}"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_object); + SS_CHECK(val->u.object.length == 0); + json_value_free(val); +} + +void test_parse_empty_array() +{ + json_value *val = parse("[]"); + SS_CHECK(val != nullptr); + SS_CHECK(val->type == json_array); + SS_CHECK(val->u.array.length == 0); + json_value_free(val); +} + +void test_parse_wide_numbers() +{ + json_value *wide = parse("[4294967296,9223372036854775807,-4294967296,0.4294967296]"); + SS_CHECK(wide != nullptr && wide->type == json_array && wide->u.array.length == 4); + SS_CHECK(wide->u.array.values[0]->u.integer == INT64_C(4294967296)); + SS_CHECK(wide->u.array.values[1]->u.integer == INT64_MAX); + SS_CHECK(wide->u.array.values[2]->u.integer == -INT64_C(4294967296)); + SS_CHECK(wide->u.array.values[3]->u.dbl > 0.4294967295 && wide->u.array.values[3]->u.dbl < 0.4294967297); + json_value_free(wide); + SS_CHECK(json_parse("9223372036854775808", 19) == nullptr); +} + +} // namespace + +int main() +{ + SS_RUN(test_parse_wide_numbers); + SS_RUN(test_parse_simple_object); + SS_RUN(test_parse_array); + SS_RUN(test_parse_nested); + SS_RUN(test_parse_types); + SS_RUN(test_parse_invalid); + SS_RUN(test_parse_truncated_escape_sequences); + SS_RUN(test_parse_truncated_literals); + SS_RUN(test_parse_empty_object); + SS_RUN(test_parse_empty_array); + return 0; +} diff --git a/tests/test_netutils.c b/tests/test_netutils.c deleted file mode 100644 index 763ca968..00000000 --- a/tests/test_netutils.c +++ /dev/null @@ -1,138 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include "test_helpers.h" - -int verbose = 0; - -#include "netutils.h" - -static void -test_get_sockaddr_len(void) -{ - struct sockaddr_in addr4; - struct sockaddr_in6 addr6; - struct sockaddr_storage unknown; - - memset(&addr4, 0, sizeof(addr4)); - addr4.sin_family = AF_INET; - assert(get_sockaddr_len((struct sockaddr *)&addr4) == sizeof(struct sockaddr_in)); - - memset(&addr6, 0, sizeof(addr6)); - addr6.sin6_family = AF_INET6; - assert(get_sockaddr_len((struct sockaddr *)&addr6) == sizeof(struct sockaddr_in6)); - - memset(&unknown, 0, sizeof(unknown)); - unknown.ss_family = AF_UNSPEC; - assert(get_sockaddr_len((struct sockaddr *)&unknown) == 0); -} - -static void -test_sockaddr_cmp(void) -{ - struct sockaddr_storage a, b; - struct sockaddr_in *a4 = (struct sockaddr_in *)&a; - struct sockaddr_in *b4 = (struct sockaddr_in *)&b; - - /* Same address and port */ - memset(&a, 0, sizeof(a)); - memset(&b, 0, sizeof(b)); - a4->sin_family = AF_INET; - b4->sin_family = AF_INET; - a4->sin_port = htons(80); - b4->sin_port = htons(80); - inet_pton(AF_INET, "127.0.0.1", &a4->sin_addr); - inet_pton(AF_INET, "127.0.0.1", &b4->sin_addr); - assert(sockaddr_cmp(&a, &b, sizeof(struct sockaddr_in)) == 0); - - /* Different port */ - b4->sin_port = htons(81); - assert(sockaddr_cmp(&a, &b, sizeof(struct sockaddr_in)) != 0); -} - -static void -test_sockaddr_cmp_addr(void) -{ - struct sockaddr_storage a, b; - struct sockaddr_in *a4 = (struct sockaddr_in *)&a; - struct sockaddr_in *b4 = (struct sockaddr_in *)&b; - - memset(&a, 0, sizeof(a)); - memset(&b, 0, sizeof(b)); - a4->sin_family = AF_INET; - b4->sin_family = AF_INET; - a4->sin_port = htons(80); - b4->sin_port = htons(443); - inet_pton(AF_INET, "10.0.0.1", &a4->sin_addr); - inet_pton(AF_INET, "10.0.0.1", &b4->sin_addr); - - /* Same address, different port - should be equal */ - assert(sockaddr_cmp_addr(&a, &b, sizeof(struct sockaddr_in)) == 0); - - /* Different address */ - inet_pton(AF_INET, "10.0.0.2", &b4->sin_addr); - assert(sockaddr_cmp_addr(&a, &b, sizeof(struct sockaddr_in)) != 0); -} - -static void -test_validate_hostname(void) -{ - /* Valid hostnames */ - assert(validate_hostname("example.com", 11) == 1); - assert(validate_hostname("sub.example.com", 15) == 1); - assert(validate_hostname("a", 1) == 1); - assert(validate_hostname("a-b", 3) == 1); - assert(validate_hostname("123.456", 7) == 1); - - /* Invalid hostnames */ - assert(validate_hostname(NULL, 0) == 0); - assert(validate_hostname("", 0) == 0); - assert(validate_hostname(".example.com", 12) == 0); /* starts with dot */ - assert(validate_hostname("-example.com", 12) == 0); /* label starts with hyphen */ - assert(validate_hostname("example-.com", 12) == 0); /* label ends with hyphen */ - - /* Too long hostname (> 255) */ - char long_name[260]; - memset(long_name, 'a', 259); - long_name[259] = '\0'; - assert(validate_hostname(long_name, 259) == 0); - - /* Explicit length must be honored; callers may pass non-NUL data. */ - char raw_name[3] = { 'a', 'b', 'c' }; - assert(validate_hostname(raw_name, 3) == 1); -} - -static void -test_get_sockaddr_rejects_invalid_ip_literal_ports(void) -{ - struct sockaddr_storage storage; - struct sockaddr_in *addr4 = (struct sockaddr_in *)&storage; - - memset(&storage, 0, sizeof(storage)); - assert(get_sockaddr("localhost", "8388", &storage, 0, 0) == -1); - assert(get_sockaddr("127.0.0.1", "8388", &storage, 0, 0) == 0); - assert(addr4->sin_family == AF_INET); - assert(ntohs(addr4->sin_port) == 8388); - - memset(&storage, 0, sizeof(storage)); - assert(get_sockaddr("127.0.0.1", "70000", &storage, 0, 0) == -1); - - memset(&storage, 0, sizeof(storage)); - assert(get_sockaddr("127.0.0.1", "12x", &storage, 0, 0) == -1); -} - -int -main(void) -{ - test_network_init(); - test_get_sockaddr_len(); - test_sockaddr_cmp(); - test_sockaddr_cmp_addr(); - test_validate_hostname(); - test_get_sockaddr_rejects_invalid_ip_literal_ports(); - return 0; -} diff --git a/tests/test_netutils_cpp.cpp b/tests/test_netutils_cpp.cpp new file mode 100644 index 00000000..afb57877 --- /dev/null +++ b/tests/test_netutils_cpp.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Address helpers (src/netutils.cpp): sockaddr comparison, hostname +// validation and literal-address parsing. +#include + +#include "netutils.h" +#include "test_helpers.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +// get_sockaddr() takes writable strings; copy the literals. +ssize_t resolve(const char *host, const char *port, sockaddr_storage *storage) +{ + char host_buf[64]; + char port_buf[16]; + SS_CHECK(std::strlen(host) < sizeof(host_buf) && std::strlen(port) < sizeof(port_buf)); + std::strcpy(host_buf, host); + std::strcpy(port_buf, port); + return get_sockaddr(host_buf, port_buf, storage, 0, 0); +} + +void test_get_sockaddr_len() +{ + sockaddr_in addr4{}; + addr4.sin_family = AF_INET; + SS_CHECK(get_sockaddr_len(reinterpret_cast(&addr4)) == sizeof(sockaddr_in)); + + sockaddr_in6 addr6{}; + addr6.sin6_family = AF_INET6; + SS_CHECK(get_sockaddr_len(reinterpret_cast(&addr6)) == sizeof(sockaddr_in6)); + + sockaddr_storage unknown{}; + unknown.ss_family = AF_UNSPEC; + SS_CHECK(get_sockaddr_len(reinterpret_cast(&unknown)) == 0); +} + +void test_sockaddr_cmp() +{ + sockaddr_storage a{}; + sockaddr_storage b{}; + auto *a4 = reinterpret_cast(&a); + auto *b4 = reinterpret_cast(&b); + + // Same address and port. + a4->sin_family = AF_INET; + b4->sin_family = AF_INET; + a4->sin_port = htons(80); + b4->sin_port = htons(80); + inet_pton(AF_INET, "127.0.0.1", &a4->sin_addr); + inet_pton(AF_INET, "127.0.0.1", &b4->sin_addr); + SS_CHECK(sockaddr_cmp(&a, &b, sizeof(sockaddr_in)) == 0); + + // Different port. + b4->sin_port = htons(81); + SS_CHECK(sockaddr_cmp(&a, &b, sizeof(sockaddr_in)) != 0); +} + +void test_sockaddr_cmp_addr() +{ + sockaddr_storage a{}; + sockaddr_storage b{}; + auto *a4 = reinterpret_cast(&a); + auto *b4 = reinterpret_cast(&b); + + a4->sin_family = AF_INET; + b4->sin_family = AF_INET; + a4->sin_port = htons(80); + b4->sin_port = htons(443); + inet_pton(AF_INET, "10.0.0.1", &a4->sin_addr); + inet_pton(AF_INET, "10.0.0.1", &b4->sin_addr); + + // Same address, different port - should be equal. + SS_CHECK(sockaddr_cmp_addr(&a, &b, sizeof(sockaddr_in)) == 0); + + // Different address. + inet_pton(AF_INET, "10.0.0.2", &b4->sin_addr); + SS_CHECK(sockaddr_cmp_addr(&a, &b, sizeof(sockaddr_in)) != 0); +} + +void test_validate_hostname() +{ + // Valid hostnames. + SS_CHECK(validate_hostname("example.com", 11) == 1); + SS_CHECK(validate_hostname("sub.example.com", 15) == 1); + SS_CHECK(validate_hostname("a", 1) == 1); + SS_CHECK(validate_hostname("a-b", 3) == 1); + SS_CHECK(validate_hostname("123.456", 7) == 1); + + // Invalid hostnames. + SS_CHECK(validate_hostname(nullptr, 0) == 0); + SS_CHECK(validate_hostname("", 0) == 0); + SS_CHECK(validate_hostname(".example.com", 12) == 0); // starts with dot + SS_CHECK(validate_hostname("-example.com", 12) == 0); // label starts with hyphen + SS_CHECK(validate_hostname("example-.com", 12) == 0); // label ends with hyphen + + // Too long hostname (> 255). + char long_name[260]; + std::memset(long_name, 'a', 259); + long_name[259] = '\0'; + SS_CHECK(validate_hostname(long_name, 259) == 0); + + // Explicit length must be honored; callers may pass non-NUL data. + const char raw_name[3] = { 'a', 'b', 'c' }; + SS_CHECK(validate_hostname(raw_name, 3) == 1); +} + +void test_get_sockaddr_rejects_invalid_ip_literal_ports() +{ + sockaddr_storage storage{}; + auto *addr4 = reinterpret_cast(&storage); + + SS_CHECK(resolve("localhost", "8388", &storage) == -1); + SS_CHECK(resolve("127.0.0.1", "8388", &storage) == 0); + SS_CHECK(addr4->sin_family == AF_INET); + SS_CHECK(ntohs(addr4->sin_port) == 8388); + + storage = sockaddr_storage{}; + SS_CHECK(resolve("127.0.0.1", "70000", &storage) == -1); + + storage = sockaddr_storage{}; + SS_CHECK(resolve("127.0.0.1", "12x", &storage) == -1); +} + +} // namespace + +int main() +{ + test_network_init(); + SS_RUN(test_get_sockaddr_len); + SS_RUN(test_sockaddr_cmp); + SS_RUN(test_sockaddr_cmp_addr); + SS_RUN(test_validate_hostname); + SS_RUN(test_get_sockaddr_rejects_invalid_ip_literal_ports); + return 0; +} diff --git a/tests/test_ppbloom.c b/tests/test_ppbloom.c deleted file mode 100644 index 0ef17bbc..00000000 --- a/tests/test_ppbloom.c +++ /dev/null @@ -1,74 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include -#include - -int verbose = 0; - -#include "ppbloom.h" - -static void -test_init_free(void) -{ - int ret = ppbloom_init(1000, 0.01); - assert(ret == 0); - (void)ret; - ppbloom_free(); -} - -static void -test_add_check(void) -{ - ppbloom_init(1000, 0.01); - - const char *item1 = "hello"; - const char *item2 = "world"; - const char *item3 = "missing"; - - /* Not in filter initially */ - assert(ppbloom_check(item1, strlen(item1)) == 0); - assert(ppbloom_check(item2, strlen(item2)) == 0); - - /* Add items */ - ppbloom_add(item1, strlen(item1)); - ppbloom_add(item2, strlen(item2)); - - /* Should be found */ - assert(ppbloom_check(item1, strlen(item1)) == 1); - assert(ppbloom_check(item2, strlen(item2)) == 1); - - /* Should not be found */ - assert(ppbloom_check(item3, strlen(item3)) == 0); - (void)item3; - - ppbloom_free(); -} - -static void -test_binary_data(void) -{ - ppbloom_init(1000, 0.01); - - const uint8_t data1[] = { 0x00, 0x01, 0x02, 0x03 }; - const uint8_t data2[] = { 0xFF, 0xFE, 0xFD, 0xFC }; - - ppbloom_add(data1, sizeof(data1)); - assert(ppbloom_check(data1, sizeof(data1)) == 1); - assert(ppbloom_check(data2, sizeof(data2)) == 0); - (void)data2; - - ppbloom_free(); -} - -int -main(void) -{ - test_init_free(); - test_add_check(); - test_binary_data(); - return 0; -} diff --git a/tests/test_ppbloom_cpp.cpp b/tests/test_ppbloom_cpp.cpp new file mode 100644 index 00000000..1e5340c0 --- /dev/null +++ b/tests/test_ppbloom_cpp.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Ping-pong bloom filter (src/ppbloom.cpp): add and membership checks. +#include +#include + +#include "ppbloom.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +void test_init_free() +{ + SS_CHECK(ppbloom_init(1000, 0.01) == 0); + ppbloom_free(); +} + +void test_add_check() +{ + ppbloom_init(1000, 0.01); + + const char *item1 = "hello"; + const char *item2 = "world"; + const char *item3 = "missing"; + + // Not in the filter initially. + SS_CHECK(ppbloom_check(item1, std::strlen(item1)) == 0); + SS_CHECK(ppbloom_check(item2, std::strlen(item2)) == 0); + + ppbloom_add(item1, std::strlen(item1)); + ppbloom_add(item2, std::strlen(item2)); + + SS_CHECK(ppbloom_check(item1, std::strlen(item1)) == 1); + SS_CHECK(ppbloom_check(item2, std::strlen(item2)) == 1); + + SS_CHECK(ppbloom_check(item3, std::strlen(item3)) == 0); + + ppbloom_free(); +} + +void test_binary_data() +{ + ppbloom_init(1000, 0.01); + + const uint8_t data1[] = { 0x00, 0x01, 0x02, 0x03 }; + const uint8_t data2[] = { 0xFF, 0xFE, 0xFD, 0xFC }; + + ppbloom_add(data1, sizeof(data1)); + SS_CHECK(ppbloom_check(data1, sizeof(data1)) == 1); + SS_CHECK(ppbloom_check(data2, sizeof(data2)) == 0); + + ppbloom_free(); +} + +} // namespace + +int main() +{ + SS_RUN(test_init_free); + SS_RUN(test_add_check); + SS_RUN(test_binary_data); + return 0; +} diff --git a/tests/test_process.c b/tests/test_process.c deleted file mode 100644 index a4d88840..00000000 --- a/tests/test_process.c +++ /dev/null @@ -1,85 +0,0 @@ -#include -#include -#include -#include -#include "ss_process.h" -#ifdef _WIN32 -#include -static void pause_tick(void) { Sleep(10); } -#else -#include -static void pause_tick(void) { struct timespec delay = {0, 10000000}; nanosleep(&delay, NULL); } -#endif -int verbose = 0; - -int -main(int argc, char **argv) -{ - if (argc > 1 && strcmp(argv[1], "--child") == 0) { - assert(argc == 5); - FILE *file = fopen(argv[2], "w"); - assert(file != NULL); - assert(strcmp(argv[3], "spaces and \"quotes\" with \\") == 0); - assert(strcmp(argv[4], "") == 0); - assert(strcmp(getenv("SS_PROCESS_TEST"), "child value") == 0); - fputs("ready\n", file); - fclose(file); - for (unsigned i = 0; i < 1000; i++) pause_tick(); - return 0; - } - if (argc > 1 && strcmp(argv[1], "--exit") == 0) return 17; - - struct ss_process *process = ss_process_new("ss-no-such-program-ef91ac"); - ss_process_arg(process, "ss-no-such-program-ef91ac"); - int started = ss_process_start(process, 0); - assert(started == -1 || started == 0); - /* POSIX permits exec failure to be reported by child exit (notably under - * Valgrind's spawn wrapper), rather than synchronously by posix_spawnp. */ - if (started == 0) { - for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick(); - assert(!ss_process_running(process)); - } - ss_process_free(process); - - char path[4096]; - assert(snprintf(path, sizeof(path), "%s.child-output", argv[0]) < (int)sizeof(path)); - remove(path); - process = ss_process_new(argv[0]); - ss_process_arg(process, argv[0]); - ss_process_arg(process, "--child"); - ss_process_arg(process, path); - ss_process_arg(process, "spaces and \"quotes\" with \\"); - ss_process_arg(process, ""); - ss_process_env(process, "SS_PROCESS_TEST", "child value"); - const char *before = getenv("SS_PROCESS_TEST"); - char *saved = before == NULL ? NULL : strdup(before); - assert(ss_process_start(process, 0) == 0); - bool ready = false; - for (unsigned i = 0; i < 500; i++) { - FILE *file = fopen(path, "r"); - if (file != NULL) { - char line[16]; - ready = fgets(line, sizeof(line), file) != NULL && strcmp(line, "ready\n") == 0; - fclose(file); - } - if (ready) break; - pause_tick(); - } - assert(ready); - assert(ss_process_running(process)); - const char *after = getenv("SS_PROCESS_TEST"); - assert(saved == NULL ? after == NULL : after != NULL && strcmp(after, saved) == 0); - free(saved); - ss_process_free(process); - remove(path); - - process = ss_process_new(argv[0]); - ss_process_arg(process, argv[0]); - ss_process_arg(process, "--exit"); - assert(ss_process_start(process, 0) == 0); - for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick(); - assert(!ss_process_running(process)); - ss_process_free(process); - ss_process_free(NULL); - return 0; -} diff --git a/tests/test_process_cpp.cpp b/tests/test_process_cpp.cpp new file mode 100644 index 00000000..3d8d8423 --- /dev/null +++ b/tests/test_process_cpp.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Child process management (src/process.cpp): spawn failure, argument and +// environment passing, and exit detection. The test binary re-executes itself +// as the child via --child / --exit. +#include +#include +#include +#include + +#include "ss_process.h" +#include "test_helpers.hpp" + +#ifdef _WIN32 +#include +#else +#include +#endif + +extern "C" { +int verbose = 0; +} + +namespace { + +void pause_tick() +{ +#ifdef _WIN32 + Sleep(10); +#else + timespec delay = { 0, 10000000 }; + nanosleep(&delay, nullptr); +#endif +} + +const char *const k_tricky_arg = "spaces and \"quotes\" with \\"; + +int run_child(int argc, char **argv) +{ + SS_CHECK(argc == 5); + FILE *file = std::fopen(argv[2], "w"); + SS_CHECK(file != nullptr); + SS_CHECK(std::strcmp(argv[3], k_tricky_arg) == 0); + SS_CHECK(std::strcmp(argv[4], "") == 0); + const char *env = std::getenv("SS_PROCESS_TEST"); + SS_CHECK(env != nullptr && std::strcmp(env, "child value") == 0); + std::fputs("ready\n", file); + std::fclose(file); + for (unsigned i = 0; i < 1000; i++) { + pause_tick(); + } + return 0; +} + +void test_missing_program() +{ + ss_process *process = ss_process_new("ss-no-such-program-ef91ac"); + ss_process_arg(process, "ss-no-such-program-ef91ac"); + const int started = ss_process_start(process, 0); + SS_CHECK(started == -1 || started == 0); + // POSIX permits exec failure to be reported by child exit (notably under + // Valgrind's spawn wrapper), rather than synchronously by posix_spawnp. + if (started == 0) { + for (unsigned i = 0; i < 500 && ss_process_running(process); i++) { + pause_tick(); + } + SS_CHECK(!ss_process_running(process)); + } + ss_process_free(process); +} + +void test_args_and_environment(const char *self) +{ + const std::string path = std::string(self) + ".child-output"; + std::remove(path.c_str()); + ss_process *process = ss_process_new(self); + ss_process_arg(process, self); + ss_process_arg(process, "--child"); + ss_process_arg(process, path.c_str()); + ss_process_arg(process, k_tricky_arg); + ss_process_arg(process, ""); + ss_process_env(process, "SS_PROCESS_TEST", "child value"); + const char *before = std::getenv("SS_PROCESS_TEST"); + const std::string saved(before == nullptr ? "" : before); + const bool had_value = before != nullptr; + SS_CHECK(ss_process_start(process, 0) == 0); + bool ready = false; + for (unsigned i = 0; i < 500; i++) { + FILE *file = std::fopen(path.c_str(), "r"); + if (file != nullptr) { + char line[16]; + ready = std::fgets(line, sizeof(line), file) != nullptr && std::strcmp(line, "ready\n") == 0; + std::fclose(file); + } + if (ready) { + break; + } + pause_tick(); + } + SS_CHECK(ready); + SS_CHECK(ss_process_running(process)); + // The parent's environment is left untouched. + const char *after = std::getenv("SS_PROCESS_TEST"); + SS_CHECK(had_value ? after != nullptr && saved == after : after == nullptr); + ss_process_free(process); + std::remove(path.c_str()); +} + +void test_exit_detection(const char *self) +{ + ss_process *process = ss_process_new(self); + ss_process_arg(process, self); + ss_process_arg(process, "--exit"); + SS_CHECK(ss_process_start(process, 0) == 0); + for (unsigned i = 0; i < 500 && ss_process_running(process); i++) { + pause_tick(); + } + SS_CHECK(!ss_process_running(process)); + ss_process_free(process); + ss_process_free(nullptr); +} + +} // namespace + +int main(int argc, char **argv) +{ + if (argc > 1 && std::strcmp(argv[1], "--child") == 0) { + return run_child(argc, argv); + } + if (argc > 1 && std::strcmp(argv[1], "--exit") == 0) { + return 17; + } + + SS_RUN(test_missing_program); + test_args_and_environment(argv[0]); + std::printf("ok test_args_and_environment\n"); + test_exit_detection(argv[0]); + std::printf("ok test_exit_detection\n"); + return 0; +} diff --git a/tests/test_ref.c b/tests/test_ref.c deleted file mode 100644 index 00d5f8e5..00000000 --- a/tests/test_ref.c +++ /dev/null @@ -1,63 +0,0 @@ -#include -#include - -#include "ss_ref.h" - -typedef struct { - ss_ref_t ref; - int *destroyed; -} test_object_t; - -static void -destroy_object(void *object) -{ - test_object_t *value = object; - ++*value->destroyed; - free(value); -} - -static void * -new_object(int *destroyed) -{ - test_object_t *value = malloc(sizeof(*value)); - assert(value != NULL); - ss_ref_init(value, destroy_object); - value->destroyed = destroyed; - return value; -} - -int -main(void) -{ - int destroyed = 0; - void *table = new_object(&destroyed); - void *relay = NULL; - ss_ref_assign(&relay, table); - ss_ref_assign(&relay, relay); /* Self-assignment must preserve ownership. */ - ss_ptr_reset(&table, ss_ref_release); /* Simulate eviction. */ - assert(table == NULL && destroyed == 0); - ss_ptr_reset(&relay, ss_ref_release); - assert(relay == NULL && destroyed == 1); - ss_ptr_reset(&relay, ss_ref_release); /* Repeated cleanup is harmless. */ - assert(destroyed == 1); - - void *query = new_object(&destroyed); - relay = new_object(&destroyed); - ss_ptr_move(&relay, &query, ss_ref_release); - assert(query == NULL && relay != NULL && destroyed == 2); - ss_ptr_move(&relay, &relay, ss_ref_release); /* Self-move. */ - assert(relay != NULL && destroyed == 2); - ss_ref_assign(&query, relay); - ss_ptr_move(&relay, &query, ss_ref_release); /* Two owners, same object. */ - assert(query == NULL && relay != NULL && destroyed == 2); - ss_ref_assign(&relay, NULL); - assert(relay == NULL && destroyed == 3); - - relay = new_object(&destroyed); - ss_ptr_move(&relay, &query, ss_ref_release); /* Move an empty source. */ - assert(relay == NULL && query == NULL && destroyed == 4); - assert(ss_ref_retain(NULL) == NULL); - ss_ref_release(NULL); - ss_ptr_reset(&relay, NULL); /* Legacy crypto has no session/callback. */ - return 0; -} diff --git a/tests/test_ref_cpp.cpp b/tests/test_ref_cpp.cpp new file mode 100644 index 00000000..e8a89efd --- /dev/null +++ b/tests/test_ref_cpp.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Intrusive reference counting (tests/reference/ss_ref.h): assignment, +// move and release helpers used by the legacy relay code. +#include + +#include "ss_ref.h" +#include "test_helpers.hpp" + +namespace { + +struct test_object_t { + ss_ref_t ref; + int *destroyed; +}; + +void destroy_object(void *object) +{ + auto *value = static_cast(object); + ++*value->destroyed; + std::free(value); +} + +void *new_object(int *destroyed) +{ + auto *value = static_cast(std::malloc(sizeof(test_object_t))); + SS_CHECK(value != nullptr); + ss_ref_init(value, destroy_object); + value->destroyed = destroyed; + return value; +} + +void test_ref_lifecycle() +{ + int destroyed = 0; + void *table = new_object(&destroyed); + void *relay = nullptr; + ss_ref_assign(&relay, table); + ss_ref_assign(&relay, relay); // Self-assignment must preserve ownership. + ss_ptr_reset(&table, ss_ref_release); // Simulate eviction. + SS_CHECK(table == nullptr && destroyed == 0); + ss_ptr_reset(&relay, ss_ref_release); + SS_CHECK(relay == nullptr && destroyed == 1); + ss_ptr_reset(&relay, ss_ref_release); // Repeated cleanup is harmless. + SS_CHECK(destroyed == 1); + + void *query = new_object(&destroyed); + relay = new_object(&destroyed); + ss_ptr_move(&relay, &query, ss_ref_release); + SS_CHECK(query == nullptr && relay != nullptr && destroyed == 2); + ss_ptr_move(&relay, &relay, ss_ref_release); // Self-move. + SS_CHECK(relay != nullptr && destroyed == 2); + ss_ref_assign(&query, relay); + ss_ptr_move(&relay, &query, ss_ref_release); // Two owners, same object. + SS_CHECK(query == nullptr && relay != nullptr && destroyed == 2); + ss_ref_assign(&relay, nullptr); + SS_CHECK(relay == nullptr && destroyed == 3); + + relay = new_object(&destroyed); + ss_ptr_move(&relay, &query, ss_ref_release); // Move an empty source. + SS_CHECK(relay == nullptr && query == nullptr && destroyed == 4); + SS_CHECK(ss_ref_retain(nullptr) == nullptr); + ss_ref_release(nullptr); + ss_ptr_reset(&relay, nullptr); // Legacy crypto has no session/callback. +} + +} // namespace + +int main() +{ + SS_RUN(test_ref_lifecycle); + return 0; +} diff --git a/tests/test_ssurl.c b/tests/test_ssurl.c deleted file mode 100644 index 9ebbce61..00000000 --- a/tests/test_ssurl.c +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Unit tests for ss:// URI parsing (SIP002 and the legacy form). - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include - -int verbose = 0; - -#include "ssurl.h" - -#define STREQ(a, b) ((a) != NULL && strcmp((a), (b)) == 0) - -/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */ -static void -test_sip002_basic(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888", - &u) == 0); - assert(STREQ(u.method, "aes-256-gcm")); - assert(STREQ(u.password, "password")); - assert(STREQ(u.host, "192.168.100.1")); - assert(STREQ(u.port, "8888")); - assert(u.plugin == NULL); - ss_url_free(&u); -} - -/* A tag after '#' is descriptive only and must not leak into the fields. */ -static void -test_sip002_tag(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888" - "#Example%20Server", &u) == 0); - assert(STREQ(u.host, "192.168.100.1")); - assert(STREQ(u.tag, "Example Server")); - ss_url_free(&u); -} - -/* SIP003 plugin, with its options after the ';'. */ -static void -test_sip002_plugin(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888" - "/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0); - assert(STREQ(u.host, "192.168.100.1")); - assert(STREQ(u.port, "8888")); - assert(STREQ(u.plugin, "obfs-local")); - assert(STREQ(u.plugin_opts, "obfs=http")); - assert(STREQ(u.tag, "tag")); - ss_url_free(&u); -} - -/* The trailing slash before a query is allowed, and so is no query at all. */ -static void -test_sip002_trailing_slash(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/", - &u) == 0); - assert(STREQ(u.host, "example.com")); - assert(STREQ(u.port, "8388")); - ss_url_free(&u); -} - -/* A bracketed IPv6 literal must keep its address and port apart. */ -static void -test_sip002_ipv6(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388", - &u) == 0); - assert(STREQ(u.host, "2001:db8::1")); - assert(STREQ(u.port, "8388")); - ss_url_free(&u); -} - -/* Some producers write the userinfo in the clear rather than base64. */ -static void -test_sip002_plain_userinfo(void) -{ - ss_url_t u; - - assert(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0); - assert(STREQ(u.method, "aes-256-gcm")); - assert(STREQ(u.password, "secret")); - assert(STREQ(u.host, "example.com")); - ss_url_free(&u); -} - -/* - * A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded - * into the userinfo it must survive the round trip byte for byte. - */ -static void -test_sip002_2022_psk(void) -{ - ss_url_t u; - - /* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */ - const char *uri = - "ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1" - "Pz8_Pz8_Pz89@example.com:8388"; - - assert(ss_url_parse(uri, &u) == 0); - assert(STREQ(u.method, "2022-blake3-aes-256-gcm")); - assert(STREQ(u.host, "example.com")); - assert(STREQ(u.port, "8388")); - ss_url_free(&u); -} - -/* Legacy form: the whole body is one base64 blob. */ -static void -test_legacy(void) -{ - ss_url_t u; - - /* base64("aes-256-cfb:password@192.168.100.1:8888") */ - assert(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4", - &u) == 0); - assert(STREQ(u.method, "aes-256-cfb")); - assert(STREQ(u.password, "password")); - assert(STREQ(u.host, "192.168.100.1")); - assert(STREQ(u.port, "8888")); - ss_url_free(&u); -} - -/* Malformed input must be rejected, not half-parsed. */ -static void -test_rejects_malformed(void) -{ - ss_url_t u; - - static const char *bad[] = { - NULL, - "", - "http://example.com", - "ss://", - "ss://@example.com:8388", /* empty userinfo */ - "ss://YWVzOnB3@example.com", /* no port */ - "ss://YWVzOnB3@:8388", /* no host */ - "ss://YWVzOnB3@example.com:", /* empty port */ - "ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */ - "ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */ - }; - - for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { - memset(&u, 0xa5, sizeof(u)); - assert(ss_url_parse(bad[i], &u) == -1); - /* a rejected parse must leave nothing behind to free */ - assert(u.method == NULL && u.host == NULL && u.port == NULL); - } -} - -/* - * URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim. - * These pin the shapes another implementation actually produces, which is - * what the parser has to accept in the field. - */ -static void -test_rust_ssurl_output(void) -{ - ss_url_t u; - - /* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */ - assert(ss_url_parse("ss://2022-blake3-aes-256-gcm:" - "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D" - "@192.168.100.1:8888", &u) == 0); - assert(STREQ(u.method, "2022-blake3-aes-256-gcm")); - assert(STREQ(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE=")); - assert(STREQ(u.host, "192.168.100.1")); - assert(STREQ(u.port, "8888")); - ss_url_free(&u); - - /* A legacy cipher: unpadded base64 userinfo, plugin in the query. */ - assert(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA" - "@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp", - &u) == 0); - assert(STREQ(u.method, "aes-256-gcm")); - assert(STREQ(u.password, "test-password")); - assert(STREQ(u.plugin, "obfs-local")); - assert(STREQ(u.plugin_opts, "obfs=http")); - ss_url_free(&u); - - /* An IPv6 server. */ - assert(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw" - "@[2001:db8::1]:8388", &u) == 0); - assert(STREQ(u.method, "chacha20-ietf-poly1305")); - assert(STREQ(u.password, "pw")); - assert(STREQ(u.host, "2001:db8::1")); - assert(STREQ(u.port, "8388")); - ss_url_free(&u); -} - -/* Freeing a zeroed struct, or NULL, must be safe. */ -static void -test_free_is_safe(void) -{ - ss_url_t u = { 0 }; - - ss_url_free(&u); - ss_url_free(NULL); -} - -int -main(void) -{ - test_sip002_basic(); - test_sip002_tag(); - test_sip002_plugin(); - test_sip002_trailing_slash(); - test_sip002_ipv6(); - test_sip002_plain_userinfo(); - test_sip002_2022_psk(); - test_legacy(); - test_rust_ssurl_output(); - test_rejects_malformed(); - test_free_is_safe(); - return 0; -} diff --git a/tests/test_ssurl_cpp.cpp b/tests/test_ssurl_cpp.cpp new file mode 100644 index 00000000..9d7d50d4 --- /dev/null +++ b/tests/test_ssurl_cpp.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// ss:// URI parsing (src/ssurl.cpp): SIP002, the legacy form and malformed +// input. +#include +#include + +#include "ssurl.h" +#include "test_helpers.hpp" + +extern "C" { +int verbose = 0; +} + +namespace { + +bool streq(const char *a, const char *b) +{ + return a != nullptr && std::strcmp(a, b) == 0; +} + +/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */ +void test_sip002_basic() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888", + &u) == 0); + SS_CHECK(streq(u.method, "aes-256-gcm")); + SS_CHECK(streq(u.password, "password")); + SS_CHECK(streq(u.host, "192.168.100.1")); + SS_CHECK(streq(u.port, "8888")); + SS_CHECK(u.plugin == nullptr); + ss_url_free(&u); +} + +/* A tag after '#' is descriptive only and must not leak into the fields. */ +void test_sip002_tag() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888" + "#Example%20Server", &u) == 0); + SS_CHECK(streq(u.host, "192.168.100.1")); + SS_CHECK(streq(u.tag, "Example Server")); + ss_url_free(&u); +} + +/* SIP003 plugin, with its options after the ';'. */ +void test_sip002_plugin() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888" + "/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0); + SS_CHECK(streq(u.host, "192.168.100.1")); + SS_CHECK(streq(u.port, "8888")); + SS_CHECK(streq(u.plugin, "obfs-local")); + SS_CHECK(streq(u.plugin_opts, "obfs=http")); + SS_CHECK(streq(u.tag, "tag")); + ss_url_free(&u); +} + +/* The trailing slash before a query is allowed, and so is no query at all. */ +void test_sip002_trailing_slash() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/", + &u) == 0); + SS_CHECK(streq(u.host, "example.com")); + SS_CHECK(streq(u.port, "8388")); + ss_url_free(&u); +} + +/* A bracketed IPv6 literal must keep its address and port apart. */ +void test_sip002_ipv6() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388", + &u) == 0); + SS_CHECK(streq(u.host, "2001:db8::1")); + SS_CHECK(streq(u.port, "8388")); + ss_url_free(&u); +} + +/* Some producers write the userinfo in the clear rather than base64. */ +void test_sip002_plain_userinfo() +{ + ss_url_t u; + + SS_CHECK(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0); + SS_CHECK(streq(u.method, "aes-256-gcm")); + SS_CHECK(streq(u.password, "secret")); + SS_CHECK(streq(u.host, "example.com")); + ss_url_free(&u); +} + +/* + * A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded + * into the userinfo it must survive the round trip byte for byte. + */ +void test_sip002_2022_psk() +{ + ss_url_t u; + + /* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */ + const char *uri = + "ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1" + "Pz8_Pz8_Pz89@example.com:8388"; + + SS_CHECK(ss_url_parse(uri, &u) == 0); + SS_CHECK(streq(u.method, "2022-blake3-aes-256-gcm")); + SS_CHECK(streq(u.host, "example.com")); + SS_CHECK(streq(u.port, "8388")); + ss_url_free(&u); +} + +/* Legacy form: the whole body is one base64 blob. */ +void test_legacy() +{ + ss_url_t u; + + /* base64("aes-256-cfb:password@192.168.100.1:8888") */ + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4", + &u) == 0); + SS_CHECK(streq(u.method, "aes-256-cfb")); + SS_CHECK(streq(u.password, "password")); + SS_CHECK(streq(u.host, "192.168.100.1")); + SS_CHECK(streq(u.port, "8888")); + ss_url_free(&u); +} + +/* Malformed input must be rejected, not half-parsed. */ +void test_rejects_malformed() +{ + ss_url_t u; + + static const char *const bad[] = { + nullptr, + "", + "http://example.com", + "ss://", + "ss://@example.com:8388", /* empty userinfo */ + "ss://YWVzOnB3@example.com", /* no port */ + "ss://YWVzOnB3@:8388", /* no host */ + "ss://YWVzOnB3@example.com:", /* empty port */ + "ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */ + "ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */ + }; + + for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + std::memset(&u, 0xa5, sizeof(u)); + SS_CHECK(ss_url_parse(bad[i], &u) == -1); + /* a rejected parse must leave nothing behind to free */ + SS_CHECK(u.method == nullptr && u.host == nullptr && u.port == nullptr); + } +} + +/* + * URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim. + * These pin the shapes another implementation actually produces, which is + * what the parser has to accept in the field. + */ +void test_rust_ssurl_output() +{ + ss_url_t u; + + /* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */ + SS_CHECK(ss_url_parse("ss://2022-blake3-aes-256-gcm:" + "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D" + "@192.168.100.1:8888", &u) == 0); + SS_CHECK(streq(u.method, "2022-blake3-aes-256-gcm")); + SS_CHECK(streq(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE=")); + SS_CHECK(streq(u.host, "192.168.100.1")); + SS_CHECK(streq(u.port, "8888")); + ss_url_free(&u); + + /* A legacy cipher: unpadded base64 userinfo, plugin in the query. */ + SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA" + "@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp", + &u) == 0); + SS_CHECK(streq(u.method, "aes-256-gcm")); + SS_CHECK(streq(u.password, "test-password")); + SS_CHECK(streq(u.plugin, "obfs-local")); + SS_CHECK(streq(u.plugin_opts, "obfs=http")); + ss_url_free(&u); + + /* An IPv6 server. */ + SS_CHECK(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw" + "@[2001:db8::1]:8388", &u) == 0); + SS_CHECK(streq(u.method, "chacha20-ietf-poly1305")); + SS_CHECK(streq(u.password, "pw")); + SS_CHECK(streq(u.host, "2001:db8::1")); + SS_CHECK(streq(u.port, "8388")); + ss_url_free(&u); +} + +/* Freeing a zeroed struct, or nullptr, must be safe. */ +void test_free_is_safe() +{ + ss_url_t u{}; + + ss_url_free(&u); + ss_url_free(nullptr); +} + +} // namespace + +int main() +{ + SS_RUN(test_sip002_basic); + SS_RUN(test_sip002_tag); + SS_RUN(test_sip002_plugin); + SS_RUN(test_sip002_trailing_slash); + SS_RUN(test_sip002_ipv6); + SS_RUN(test_sip002_plain_userinfo); + SS_RUN(test_sip002_2022_psk); + SS_RUN(test_legacy); + SS_RUN(test_rejects_malformed); + SS_RUN(test_rust_ssurl_output); + SS_RUN(test_free_is_safe); + return 0; +} diff --git a/tests/test_utils.c b/tests/test_utils.c deleted file mode 100644 index 7312fa20..00000000 --- a/tests/test_utils.c +++ /dev/null @@ -1,113 +0,0 @@ -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include -#include -#include - -int verbose = 0; - -#include "utils.h" - -static void -test_ss_itoa(void) -{ - char *s; - - s = ss_itoa(0); - assert(s != NULL); - assert(strcmp(s, "0") == 0); - - s = ss_itoa(42); - assert(s != NULL); - assert(strcmp(s, "42") == 0); - - s = ss_itoa(-1); - assert(s != NULL); - assert(strcmp(s, "-1") == 0); - - s = ss_itoa(12345); - assert(s != NULL); - assert(strcmp(s, "12345") == 0); - (void)s; -} - -static void -test_ss_isnumeric(void) -{ - assert(ss_isnumeric("12345") == 1); - assert(ss_isnumeric("0") == 1); - assert(ss_isnumeric("") == 0); - assert(ss_isnumeric("abc") == 0); - assert(ss_isnumeric("123abc") == 0); - assert(ss_isnumeric("12.34") == 0); -} - -static void -test_ss_strndup(void) -{ - char *s; - - s = ss_strndup("hello world", 5); - assert(s != NULL); - assert(strcmp(s, "hello") == 0); - assert(strlen(s) == 5); - free(s); - - s = ss_strndup("short", 10); - assert(s != NULL); - assert(strcmp(s, "short") == 0); - free(s); - - s = ss_strndup("", 0); - assert(s != NULL); - assert(strcmp(s, "") == 0); - free(s); - - char raw[3] = { 'a', 'b', 'c' }; - s = ss_strndup(raw, sizeof(raw)); - assert(s != NULL); - assert(strcmp(s, "abc") == 0); - free(s); -} - -static void -test_ss_parse_int(void) -{ - int value = -1; - uint16_t port = 0; - - assert(ss_parse_int("0", 0, 10, &value) == 0); - assert(value == 0); - - assert(ss_parse_int("10", 0, 10, &value) == 0); - assert(value == 10); - - assert(ss_parse_int("-1", 0, 10, &value) == -1); - assert(ss_parse_int("11", 0, 10, &value) == -1); - assert(ss_parse_int("12x", 0, 100, &value) == -1); - assert(ss_parse_int("", 0, 100, &value) == -1); - assert(ss_parse_int("999999999999999999999999", 0, INT_MAX, &value) == -1); - assert(ss_parse_int("1", 10, 0, &value) == -1); - assert(ss_parse_int("1", 0, 10, NULL) == -1); - - assert(ss_parse_uint16_port("1", &port) == 0); - assert(port == 1); - assert(ss_parse_uint16_port("65535", &port) == 0); - assert(port == 65535); - assert(ss_parse_uint16_port("0", &port) == -1); - assert(ss_parse_uint16_port("65536", &port) == -1); - assert(ss_parse_uint16_port("22x", &port) == -1); - assert(ss_parse_uint16_port("22", NULL) == -1); -} - -int -main(void) -{ - test_ss_itoa(); - test_ss_isnumeric(); - test_ss_strndup(); - test_ss_parse_int(); - return 0; -} diff --git a/tests/test_utils_cpp.cpp b/tests/test_utils_cpp.cpp new file mode 100644 index 00000000..001a2d20 --- /dev/null +++ b/tests/test_utils_cpp.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// String and number helpers (src/utils.cpp). +#include +#include +#include +#include + +#include "test_helpers.hpp" +#include "utils.h" + +extern "C" { +int verbose = 0; +} + +namespace { + +void test_ss_itoa() +{ + char *s = ss_itoa(0); + SS_CHECK(s != nullptr && std::strcmp(s, "0") == 0); + + s = ss_itoa(42); + SS_CHECK(s != nullptr && std::strcmp(s, "42") == 0); + + s = ss_itoa(-1); + SS_CHECK(s != nullptr && std::strcmp(s, "-1") == 0); + + s = ss_itoa(12345); + SS_CHECK(s != nullptr && std::strcmp(s, "12345") == 0); +} + +void test_ss_isnumeric() +{ + SS_CHECK(ss_isnumeric("12345") == 1); + SS_CHECK(ss_isnumeric("0") == 1); + SS_CHECK(ss_isnumeric("") == 0); + SS_CHECK(ss_isnumeric("abc") == 0); + SS_CHECK(ss_isnumeric("123abc") == 0); + SS_CHECK(ss_isnumeric("12.34") == 0); +} + +void test_ss_strndup() +{ + char *s = ss_strndup("hello world", 5); + SS_CHECK(s != nullptr && std::strcmp(s, "hello") == 0 && std::strlen(s) == 5); + std::free(s); + + s = ss_strndup("short", 10); + SS_CHECK(s != nullptr && std::strcmp(s, "short") == 0); + std::free(s); + + s = ss_strndup("", 0); + SS_CHECK(s != nullptr && std::strcmp(s, "") == 0); + std::free(s); + + const char raw[3] = { 'a', 'b', 'c' }; + s = ss_strndup(raw, sizeof(raw)); + SS_CHECK(s != nullptr && std::strcmp(s, "abc") == 0); + std::free(s); +} + +void test_ss_parse_int() +{ + int value = -1; + uint16_t port = 0; + + SS_CHECK(ss_parse_int("0", 0, 10, &value) == 0); + SS_CHECK(value == 0); + + SS_CHECK(ss_parse_int("10", 0, 10, &value) == 0); + SS_CHECK(value == 10); + + SS_CHECK(ss_parse_int("-1", 0, 10, &value) == -1); + SS_CHECK(ss_parse_int("11", 0, 10, &value) == -1); + SS_CHECK(ss_parse_int("12x", 0, 100, &value) == -1); + SS_CHECK(ss_parse_int("", 0, 100, &value) == -1); + SS_CHECK(ss_parse_int("999999999999999999999999", 0, INT_MAX, &value) == -1); + SS_CHECK(ss_parse_int("1", 10, 0, &value) == -1); + SS_CHECK(ss_parse_int("1", 0, 10, nullptr) == -1); + + SS_CHECK(ss_parse_uint16_port("1", &port) == 0); + SS_CHECK(port == 1); + SS_CHECK(ss_parse_uint16_port("65535", &port) == 0); + SS_CHECK(port == 65535); + SS_CHECK(ss_parse_uint16_port("0", &port) == -1); + SS_CHECK(ss_parse_uint16_port("65536", &port) == -1); + SS_CHECK(ss_parse_uint16_port("22x", &port) == -1); + SS_CHECK(ss_parse_uint16_port("22", nullptr) == -1); +} + +} // namespace + +int main() +{ + SS_RUN(test_ss_itoa); + SS_RUN(test_ss_isnumeric); + SS_RUN(test_ss_strndup); + SS_RUN(test_ss_parse_int); + return 0; +}