Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63033 Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
560 lines
18 KiB
C++
560 lines
18 KiB
C++
#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
|