#include "config_file.hpp"
#include <toml++/toml.hpp>
#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string_view>
namespace nethack_mcp
{
namespace
{
bool fail(const std::filesystem::path& path, const std::string& message,
std::string& error)
{
error = path.string() + ": " + message;
return false;
}
bool validateKeys(const toml::table& table,
std::initializer_list<std::string_view> allowed,
const std::string& section,
const std::filesystem::path& path,
std::string& error)
{
std::size_t matched = 0;
for(const std::string_view key : allowed)
{
if(table.get(key) != nullptr)
{
++matched;
}
}
if(matched != table.size())
{
return fail(path, "unknown setting in [" + section + "]", error);
}
return true;
}
bool readSection(const toml::table& root, std::string_view name,
const toml::table*& section,
const std::filesystem::path& path,
std::string& error)
{
const toml::node* node = root.get(name);
if(node == nullptr)
{
section = nullptr;
return true;
}
section = node->as_table();
if(section == nullptr)
{
return fail(path, "[" + std::string(name) + "] must be a table",
error);
}
return true;
}
bool readString(const toml::table& table, std::string_view key,
const std::string& section, std::string& value,
const std::filesystem::path& path, std::string& error)
{
const toml::node* node = table.get(key);
if(node == nullptr)
{
return true;
}
const auto parsed = node->value<std::string>();
if(!parsed || parsed->empty())
{
return fail(path, "[" + section + "]." + std::string(key)
+ " must be a non-empty string", error);
}
value = *parsed;
return true;
}
bool readPath(const toml::table& table, std::string_view key,
const std::string& section, std::filesystem::path& value,
const std::filesystem::path& path, std::string& error)
{
std::string parsed;
if(!readString(table, key, section, parsed, path, error))
{
return false;
}
if(table.get(key) != nullptr)
{
value = parsed;
}
return true;
}
bool readPositiveInteger(const toml::table& table, std::string_view key,
const std::string& section, std::size_t& value,
const std::filesystem::path& path,
std::string& error)
{
const toml::node* node = table.get(key);
if(node == nullptr)
{
return true;
}
const auto parsed = node->value<std::int64_t>();
if(!parsed || *parsed <= 0
|| static_cast<std::uint64_t>(*parsed)
> static_cast<std::uint64_t>(
std::numeric_limits<std::size_t>::max()))
{
return fail(path, "[" + section + "]." + std::string(key)
+ " must be a positive integer", error);
}
value = static_cast<std::size_t>(*parsed);
return true;
}
bool readPort(const toml::table& table, std::string_view key,
int& value, const std::filesystem::path& path,
std::string& error)
{
const toml::node* node = table.get(key);
if(node == nullptr)
{
return true;
}
const auto parsed = node->value<std::int64_t>();
if(!parsed || *parsed <= 0 || *parsed > 65535)
{
return fail(path, "[server].port must be an integer from 1 to 65535",
error);
}
value = static_cast<int>(*parsed);
return true;
}
bool readSocketPermission(const toml::table& table, std::string_view key,
std::optional<unsigned int>& value,
const std::filesystem::path& path,
std::string& error)
{
const toml::node* node = table.get(key);
if(node == nullptr)
{
return true;
}
const auto parsed = node->value<std::int64_t>();
if(!parsed || *parsed < 0 || *parsed > 0777)
{
return fail(path,
"[server].listen_socket_permission must be an octal TOML "
"integer from 0o000 to 0o777", error);
}
value = static_cast<unsigned int>(*parsed);
return true;
}
bool readSeconds(const toml::table& table, std::string_view key,
const std::string& section, std::chrono::seconds& value,
const std::filesystem::path& path, std::string& error)
{
std::size_t parsed = 0;
if(!readPositiveInteger(table, key, section, parsed, path, error))
{
return false;
}
if(table.get(key) != nullptr)
{
value = std::chrono::seconds(parsed);
}
return true;
}
bool readServerSection(const toml::table& table, ServerConfig& config,
ConfigExplicitSettings& explicit_settings,
const std::filesystem::path& path, std::string& error)
{
if(!validateKeys(table, {
"listen_address", "listen_socket_permission", "port",
"public_base_url",
}, "server", path, error)
|| !readString(table, "listen_address", "server",
config.listen_address, path, error)
|| !readSocketPermission(table, "listen_socket_permission",
config.listen_socket_permission, path, error)
|| !readPort(table, "port", config.port, path, error)
|| !readString(table, "public_base_url", "server",
config.public_base_url, path, error))
{
return false;
}
explicit_settings.public_base_url =
table.get("public_base_url") != nullptr;
return true;
}
bool readPathsSection(const toml::table& table, ServerConfig& config,
const std::filesystem::path& path, std::string& error)
{
return validateKeys(table, {
"data_root", "database", "runtime_dir",
}, "paths", path, error)
&& readPath(table, "data_root", "paths", config.data_root,
path, error)
&& readPath(table, "database", "paths", config.database_path,
path, error)
&& readPath(table, "runtime_dir", "paths", config.runtime_directory,
path, error);
}
bool readLimitsSection(const toml::table& table, ServerConfig& config,
ConfigExplicitSettings& explicit_settings,
const std::filesystem::path& path, std::string& error)
{
if(!validateKeys(table, {
"max_active_games", "new_games_per_client",
"new_game_rate_window_seconds", "control_failures_per_client",
"control_failure_window_seconds", "max_rate_limit_clients",
"max_concurrent_requests", "max_open_connections",
"idle_timeout_seconds", "max_game_duration_seconds",
"lifecycle_sweep_seconds", "max_mcp_body_bytes",
"max_worker_output_bytes",
}, "limits", path, error)
|| !readPositiveInteger(table, "max_active_games", "limits",
config.max_active_games, path, error)
|| !readPositiveInteger(table, "new_games_per_client", "limits",
config.new_games_per_client, path, error)
|| !readSeconds(table, "new_game_rate_window_seconds", "limits",
config.new_game_rate_window, path, error)
|| !readPositiveInteger(table, "control_failures_per_client", "limits",
config.control_failures_per_client, path, error)
|| !readSeconds(table, "control_failure_window_seconds", "limits",
config.control_failure_window, path, error)
|| !readPositiveInteger(table, "max_rate_limit_clients", "limits",
config.max_rate_limit_clients, path, error)
|| !readPositiveInteger(table, "max_concurrent_requests", "limits",
config.max_concurrent_requests, path, error)
|| !readPositiveInteger(table, "max_open_connections", "limits",
config.max_open_connections, path, error)
|| !readSeconds(table, "idle_timeout_seconds", "limits",
config.idle_timeout, path, error)
|| !readSeconds(table, "max_game_duration_seconds", "limits",
config.max_game_duration, path, error)
|| !readSeconds(table, "lifecycle_sweep_seconds", "limits",
config.lifecycle_sweep_interval, path, error)
|| !readPositiveInteger(table, "max_mcp_body_bytes", "limits",
config.max_mcp_body_bytes, path, error)
|| !readPositiveInteger(table, "max_worker_output_bytes", "limits",
config.max_worker_output_bytes, path, error))
{
return false;
}
explicit_settings.max_active_games =
table.get("max_active_games") != nullptr;
explicit_settings.new_games_per_client =
table.get("new_games_per_client") != nullptr;
explicit_settings.new_game_rate_window =
table.get("new_game_rate_window_seconds") != nullptr;
explicit_settings.max_rate_limit_clients =
table.get("max_rate_limit_clients") != nullptr;
explicit_settings.max_concurrent_requests =
table.get("max_concurrent_requests") != nullptr;
explicit_settings.max_open_connections =
table.get("max_open_connections") != nullptr;
explicit_settings.max_worker_output_bytes =
table.get("max_worker_output_bytes") != nullptr;
return true;
}
} // namespace
bool readConfigFile(const std::filesystem::path& path,
ServerConfig& config,
ConfigExplicitSettings& explicit_settings,
std::string& error)
{
toml::table root;
try
{
root = toml::parse_file(path.string());
}
catch(const std::exception& exception)
{
return fail(path, exception.what(), error);
}
if(!validateKeys(root, {"server", "paths", "limits"}, "root",
path, error))
{
return false;
}
const toml::table* server = nullptr;
const toml::table* paths = nullptr;
const toml::table* limits = nullptr;
if(!readSection(root, "server", server, path, error)
|| !readSection(root, "paths", paths, path, error)
|| !readSection(root, "limits", limits, path, error))
{
return false;
}
if(server != nullptr
&& !readServerSection(*server, config, explicit_settings, path, error))
{
return false;
}
if(paths != nullptr && !readPathsSection(*paths, config, path, error))
{
return false;
}
return limits == nullptr
|| readLimitsSection(*limits, config, explicit_settings, path, error);
}
} // namespace nethack_mcp