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.
This commit is contained in:
Max Lv
2026-10-15 01:33:51 +08:00
parent fad7c44d43
commit 5f47a65c51
4 changed files with 360 additions and 253 deletions
+8 -2
View File
@@ -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
+1 -1
View File
@@ -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
+135 -73
View File
@@ -34,6 +34,11 @@
#include "string.h"
#include "core.h"
#include "core/arena.hpp"
#include <memory>
#include <new>
#include <string_view>
#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<char *>(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<ss::MonotonicArena *>(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<std::size_t>(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<char *>(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;
}
+216 -177
View File
@@ -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 <cstdio>
#include <cstdlib>
#include <cstring>
#include "test_helpers.hpp"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "test_helpers.h"
#ifndef _WIN32
#include <sys/wait.h>
#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<int>(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;
}