blob: 68c13ad881fc9f82ce44d259cda0536100365f53 [file] [edit]
#include "node_config_file.h"
#include "debug_utils-inl.h"
#include "node_version.h"
#include "simdjson.h"
#include <cinttypes>
namespace node {
constexpr std::string_view kConfigFileFlag = "--experimental-config-file";
constexpr std::string_view kDefaultConfigFileFlag =
"--experimental-default-config-file";
constexpr std::string_view kDefaultConfigFileName = "node.config.json";
constexpr std::string_view kSchemaField = "$schema";
inline bool HasEqualsPrefix(std::string_view arg, std::string_view flag) {
return arg.size() > flag.size() && arg.starts_with(flag) &&
arg[flag.size()] == '=';
}
std::optional<std::string_view> ConfigReader::GetDataFromArgs(
std::vector<std::string>* args) {
std::optional<std::string_view> result;
invalid_default_config_file_argument_ = false;
for (size_t i = 0; i < args->size(); ++i) {
std::string& arg = (*args)[i];
if (arg == kConfigFileFlag) {
// --experimental-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
} else if (HasEqualsPrefix(arg, kConfigFileFlag)) {
// --experimental-config-file=path
std::string_view path =
std::string_view(arg).substr(kConfigFileFlag.size() + 1);
if (!path.empty()) {
result = path;
}
} else if (arg == kDefaultConfigFileFlag) {
// --experimental-default-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
} else if (HasEqualsPrefix(arg, kDefaultConfigFileFlag)) {
invalid_default_config_file_argument_ = true;
}
}
return result;
}
bool ConfigReader::HasInvalidDefaultConfigFileArgument() const {
return invalid_default_config_file_argument_;
}
ParseResult ConfigReader::ProcessOptionValue(
const std::pair<std::string, options_parser::OptionMappingDetails>&
option_details,
simdjson::ondemand::value* option_value,
std::vector<std::string>* output) {
const std::string& option_name = option_details.first;
const options_parser::OptionType option_type = option_details.second.type;
switch (option_type) {
case options_parser::OptionType::kBoolean: {
bool result;
if (option_value->get_bool().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
if (result) {
// If the value is true, we need to set the flag
output->push_back(option_name);
} else {
// Ensure negation is made putting the "--no-" prefix
output->push_back("--no-" +
option_name.substr(2, option_name.size() - 2));
}
break;
}
// String array can allow both string and array types
case options_parser::OptionType::kStringList: {
simdjson::ondemand::json_type field_type;
if (option_value->type().get(field_type)) {
return ParseResult::InvalidContent;
}
switch (field_type) {
case simdjson::ondemand::json_type::array: {
std::vector<std::string> result;
simdjson::ondemand::array raw_imports;
if (option_value->get_array().get(raw_imports)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
for (auto raw_import : raw_imports) {
std::string_view import;
if (raw_import.get_string(import)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::string(import));
}
break;
}
case simdjson::ondemand::json_type::string: {
std::string result;
if (option_value->get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + result);
break;
}
default:
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
break;
}
case options_parser::OptionType::kString: {
std::string result;
if (option_value->get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + result);
break;
}
case options_parser::OptionType::kInteger: {
int64_t result;
if (option_value->get_int64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kHostPort:
case options_parser::OptionType::kUInteger: {
uint64_t result;
if (option_value->get_uint64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name);
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kNoOp: {
FPrintF(
stderr, "No-op flag %s is currently not supported\n", option_name);
return ParseResult::InvalidContent;
}
case options_parser::OptionType::kV8Option: {
FPrintF(stderr, "V8 flag %s is currently not supported\n", option_name);
return ParseResult::InvalidContent;
}
default:
UNREACHABLE();
}
return ParseResult::Valid;
}
ParseResult ConfigReader::ParseOptions(
simdjson::ondemand::object* options_object,
std::unordered_set<std::string>* unique_options,
const std::string& namespace_name) {
// Determine which options map to use and output vector
std::unordered_map<std::string, options_parser::OptionMappingDetails>
options_map;
std::vector<std::string>* output_vector;
if (namespace_name == "nodeOptions") {
// Special case for backward compatibility: handle nodeOptions with env
// options map
options_map = options_parser::MapEnvOptionsFlagInputType();
output_vector = &node_options_;
} else {
// Handle other namespaces
options_map = options_parser::MapOptionsByNamespace(namespace_name);
output_vector = &namespace_options_;
if (!env_options_initialized_) {
env_options_map_ = options_parser::MapEnvOptionsFlagInputType();
env_options_initialized_ = true;
}
}
simdjson::ondemand::value option_value;
std::string_view option_key;
for (auto field : *options_object) {
if (field.unescaped_key().get(option_key) ||
field.value().get(option_value)) {
return ParseResult::InvalidContent;
}
// The key needs to match the CLI option
std::string prefix = "--";
auto option = options_map.find(prefix.append(option_key));
if (option != options_map.end()) {
// If the option has already been set, return an error
if (unique_options->contains(option->first)) {
FPrintF(stderr, "Option %s is already defined\n", option->first);
return ParseResult::InvalidContent;
}
// Add the option to the unique set to prevent duplicates
// on future iterations
unique_options->insert(option->first);
// Process the option value based on its type
ParseResult result =
ProcessOptionValue(*option, &option_value, output_vector);
if (result != ParseResult::Valid) {
return result;
}
} else {
FPrintF(stderr,
"Unknown or not allowed option %s for namespace %s\n",
option_key,
namespace_name);
return ParseResult::InvalidContent;
}
}
return ParseResult::Valid;
}
ParseResult ConfigReader::ParseNodeVersion(
simdjson::ondemand::value* version_value,
const std::string_view& config_path) {
int64_t version;
if (version_value->get_int64().get(version)) {
FPrintF(stderr,
"\"nodeVersion\" value unexpected for %s "
"(should be an integer)\n",
config_path.data());
return ParseResult::InvalidContent;
}
if (version != NODE_MAJOR_VERSION) {
FPrintF(stderr,
"\"nodeVersion\" %" PRId64
" does not match current Node.js version %d "
"for %s\n",
version,
NODE_MAJOR_VERSION,
config_path.data());
return ParseResult::InvalidContent;
}
return ParseResult::Valid;
}
ParseResult ConfigReader::ParseConfigs(simdjson::ondemand::array* configs,
const std::string_view& config_path) {
size_t index = 0;
for (auto raw_config : *configs) {
simdjson::ondemand::object config_wrapper;
if (raw_config.get_object().get(config_wrapper)) {
FPrintF(stderr,
"\"configs[%zu]\" value unexpected for %s "
"(should be an object)\n",
index,
config_path.data());
return ParseResult::InvalidContent;
}
simdjson::ondemand::value version_value;
auto version_error =
config_wrapper.find_field_unordered("nodeVersion").get(version_value);
if (version_error == simdjson::NO_SUCH_FIELD) {
FPrintF(stderr,
"\"configs[%zu].nodeVersion\" is required for %s\n",
index,
config_path.data());
return ParseResult::InvalidContent;
}
if (version_error) {
return ParseResult::InvalidContent;
}
int64_t version;
if (version_value.get_int64().get(version)) {
FPrintF(stderr,
"\"configs[%zu].nodeVersion\" value unexpected for %s "
"(should be an integer)\n",
index,
config_path.data());
return ParseResult::InvalidContent;
}
if (version != NODE_MAJOR_VERSION) {
index++;
continue;
}
simdjson::ondemand::value config_value;
auto config_error =
config_wrapper.find_field_unordered("config").get(config_value);
if (config_error == simdjson::NO_SUCH_FIELD) {
FPrintF(stderr,
"\"configs[%zu].config\" is required for %s\n",
index,
config_path.data());
return ParseResult::InvalidContent;
}
if (config_error) {
return ParseResult::InvalidContent;
}
simdjson::ondemand::object selected_config;
if (config_value.get_object().get(selected_config)) {
FPrintF(stderr,
"\"configs[%zu].config\" value unexpected for %s "
"(should be an object)\n",
index,
config_path.data());
return ParseResult::InvalidContent;
}
return ParseConfigObject(&selected_config, config_path, false);
}
FPrintF(stderr,
"No config found for current Node.js version %d in "
"\"configs\" for %s\n",
NODE_MAJOR_VERSION,
config_path.data());
return ParseResult::InvalidContent;
}
ParseResult ConfigReader::ParseConfigObject(
simdjson::ondemand::object* config_object,
const std::string_view& config_path,
bool allow_version_selection) {
// Get all available namespaces for validation
std::vector<std::string> available_namespaces =
options_parser::MapAvailableNamespaces();
// Add "nodeOptions" as a special case for backward compatibility
available_namespaces.emplace_back("nodeOptions");
// Create a set for faster lookup of valid namespaces
std::unordered_set<std::string> valid_namespaces(available_namespaces.begin(),
available_namespaces.end());
// Create a set to track unique options
std::unordered_set<std::string> unique_options;
// Namespaces in OPTION_NAMESPACE_LIST
std::unordered_set<std::string> namespaces_with_implicit_flags;
// Iterate through the main object to find all namespaces
for (auto field : *config_object) {
std::string_view field_name;
if (field.unescaped_key().get(field_name)) {
return ParseResult::InvalidContent;
}
std::string namespace_name(field_name);
if (namespace_name == "$schema") {
continue;
}
if (namespace_name == "nodeVersion") {
simdjson::ondemand::value version_value;
if (field.value().get(version_value)) {
return ParseResult::InvalidContent;
}
ParseResult result = ParseNodeVersion(&version_value, config_path);
if (result != ParseResult::Valid) {
return result;
}
continue;
}
if (namespace_name == "configs") {
if (!allow_version_selection) {
FPrintF(stderr,
"\"configs\" is not allowed inside a versioned config "
"for %s\n",
config_path.data());
return ParseResult::InvalidContent;
}
simdjson::ondemand::array configs;
auto field_error = field.value().get_array().get(configs);
if (field_error) {
FPrintF(stderr,
"\"configs\" value unexpected for %s "
"(should be an array)\n",
config_path.data());
return ParseResult::InvalidContent;
}
ParseResult result = ParseConfigs(&configs, config_path);
if (result != ParseResult::Valid) {
return result;
}
continue;
}
// TODO(@marco-ippolito): Remove warning for testRunner namespace
if (namespace_name == "testRunner") {
FPrintF(stderr,
"the \"testRunner\" namespace has been removed. "
"Use \"test\" instead.\n");
// Better to throw an error than to ignore it
// Otherwise users might think their test suite is green
// when it's not running
return ParseResult::InvalidContent;
}
if (namespace_name == kSchemaField) {
continue;
}
// Check if this field is a valid namespace
if (!valid_namespaces.contains(namespace_name)) {
FPrintF(stderr, "Unknown namespace %s\n", namespace_name);
return ParseResult::InvalidContent;
}
// List of implicit namespace flags
for (auto ns_enum : options_parser::AllNamespaces()) {
std::string ns_str = options_parser::NamespaceEnumToString(ns_enum);
if (!ns_str.empty() && namespace_name == ns_str) {
namespaces_with_implicit_flags.insert(namespace_name);
break;
}
}
// Get the namespace object
simdjson::ondemand::object namespace_object;
auto field_error = field.value().get_object().get(namespace_object);
// If namespace value is not an object
if (field_error) {
FPrintF(stderr,
"\"%s\" value unexpected for %s (should be an object)\n",
namespace_name,
config_path.data());
return ParseResult::InvalidContent;
}
// Process options for this namespace using the unified method
ParseResult result =
ParseOptions(&namespace_object, &unique_options, namespace_name);
if (result != ParseResult::Valid) {
return result;
}
}
// Add implicit flags for namespaces (--test, --permission, --watch)
// These flags are automatically enabled when their namespace is present
for (const auto& ns : namespaces_with_implicit_flags) {
std::string flag = "--" + ns;
std::string no_flag = "--no-" + ns;
// We skip if the user has already set the flag or its negation
if (!unique_options.contains(flag) && !unique_options.contains(no_flag)) {
namespace_options_.push_back(flag);
}
}
return ParseResult::Valid;
}
ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
std::string file_content;
// Read the configuration file
int r = ReadFileSync(&file_content, config_path.data());
if (r != 0) {
const char* err = uv_strerror(r);
FPrintF(
stderr, "Cannot read configuration from %s: %s\n", config_path, err);
return ParseResult::FileError;
}
// Parse the configuration file
simdjson::ondemand::parser json_parser;
simdjson::ondemand::document document;
if (json_parser.iterate(file_content).get(document)) {
FPrintF(stderr, "Can't parse %s\n", config_path.data());
return ParseResult::InvalidContent;
}
// Validate config is an object
simdjson::ondemand::object main_object;
auto root_error = document.get_object().get(main_object);
if (root_error) {
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
FPrintF(stderr,
"Root value unexpected not an object for %s\n\n",
config_path.data());
} else {
FPrintF(stderr, "Can't parse %s\n", config_path.data());
}
return ParseResult::InvalidContent;
}
bool has_configs = false;
bool has_other_fields = false;
for (auto field : main_object) {
std::string_view field_name;
if (field.unescaped_key().get(field_name)) {
return ParseResult::InvalidContent;
}
if (field_name == "$schema") {
continue;
}
if (field_name == "configs") {
has_configs = true;
} else {
has_other_fields = true;
}
}
if (has_configs && has_other_fields) {
FPrintF(stderr,
"\"configs\" cannot be mixed with other configuration fields "
"for %s\n",
config_path.data());
return ParseResult::InvalidContent;
}
simdjson::ondemand::parser config_parser;
simdjson::ondemand::document config_document;
if (config_parser.iterate(file_content).get(config_document)) {
FPrintF(stderr, "Can't parse %s\n", config_path.data());
return ParseResult::InvalidContent;
}
simdjson::ondemand::object config_object;
if (config_document.get_object().get(config_object)) {
return ParseResult::InvalidContent;
}
return ParseConfigObject(&config_object, config_path, true);
}
std::string ConfigReader::GetNodeOptions() {
std::string acc = "";
const size_t total_options = node_options_.size();
acc.reserve(total_options * 2);
for (auto& opt : node_options_) {
acc += " " + opt;
}
return acc;
}
const std::vector<std::string>& ConfigReader::GetNamespaceFlags() const {
return namespace_options_;
}
size_t ConfigReader::GetFlagsSize() {
return node_options_.size();
}
} // namespace node