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/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/test_jconf_cpp.cpp b/tests/test_jconf_cpp.cpp index 36353325..8aa39ad7 100644 --- a/tests/test_jconf_cpp.cpp +++ b/tests/test_jconf_cpp.cpp @@ -1,253 +1,292 @@ +/* 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 -#include -#include -#include #include "test_helpers.h" #ifndef _WIN32 #include #endif +extern "C" int verbose; int verbose = 0; -#include "netutils.h" +#include "netutils.h" // defines ss_addr_t, which jconf.h uses #include "jconf.h" -static void -test_parse_addr_ipv4_with_port(void) +namespace { + +bool +same(const char *a, const char *b) { - 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); + return a == nullptr ? b == nullptr : b != nullptr && std::strcmp(a, b) == 0; } -static void -test_parse_addr_ipv6_with_port(void) +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() { - 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); + 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); + } } -static void -test_parse_addr_hostname_with_port(void) +void +write_config(const char *path, const char *body) { - 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); + FILE *f = std::fopen(path, "wb"); + SS_CHECK(f != nullptr); + std::fputs(body, f); + std::fclose(f); } -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) +void +test_read_jconf_preserves_64bit_integer_strings() { 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); + 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); - assert(conf != NULL); - assert(conf->remote_port != NULL); - assert(strcmp(conf->remote_port, "4294967296") == 0); - - remove(path); + SS_CHECK(conf != nullptr); + SS_CHECK(same(conf->remote_port, "4294967296")); + std::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) +// 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)); - assert(f != NULL); - write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648"); - fclose(f); + 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->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(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]; - 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)); + 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); - assert(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0); + SS_CHECK(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0); DWORD status; - assert(GetExitCodeProcess(child.hProcess, &status)); + SS_CHECK(GetExitCodeProcess(child.hProcess, &status)); CloseHandle(child.hProcess); - /* FATAL exits with -1, which is a valid nonzero DWORD exit status. */ - assert(status != 0); + // 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(); - assert(pid >= 0); + SS_CHECK(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); + SS_CHECK(waitpid(pid, &status, 0) == pid); + SS_CHECK(WIFEXITED(status)); + SS_CHECK(WEXITSTATUS(status) != 0); #endif - remove(path); + std::remove(path); } -static void -test_server_endpoints(void) +void +test_server_endpoints() { - const char *valid[][3] = { + 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", NULL }, - { "fe80::1%eth0", "fe80::1%eth0", NULL }, - { "::", "::", NULL }, - { "[::1]", "::1", NULL }, - { "example.com", "example.com", NULL } + { "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 (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); + 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 (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); + "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] = { { 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); + 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]); - 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); + 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 && strcmp(argv[1], "--read-config") == 0) { + if (argc == 3 && std::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(); + 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; }