@ 261105.16
current
#include "node_config_file.h"
#include "debug_utils-inl.h"
#include "node_version.h"
#include "simdjson.h"

#include <algorithm>
#include <cinttypes>

namespace node {

constexpr std::string_view kConfigFileFlag = "--config-file";
// TODO(marco-ippolito): Remove these aliases in a semver-major.
constexpr std::string_view kExperimentalConfigFileFlag =
    "--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()] == '=';
}

inline bool IsBareConfigFileFlag(std::string_view arg) {
  return arg == kConfigFileFlag || arg == kExperimentalConfigFileFlag ||
         arg == kDefaultConfigFileFlag;
}

inline bool IsConfigFileFlag(std::string_view arg) {
  return IsBareConfigFileFlag(arg) || HasEqualsPrefix(arg, kConfigFileFlag) ||
         HasEqualsPrefix(arg, kExperimentalConfigFileFlag) ||
         HasEqualsPrefix(arg, kDefaultConfigFileFlag);
}

inline std::string DefaultConfigFileArg() {
  return std::string(kConfigFileFlag) + "=" +
         std::string(kDefaultConfigFileName);
}

// Returns the index of the first entry in `args` that is neither the program
// name nor an option for Node.js itself. Everything from there on is the
// script and its arguments, or follows `--`, so it must not be read as a
// config file flag.
size_t GetNodeOptionsEnd(const std::vector<std::string>& args) {
  // The bare flags take no value. Spell them out the way GetDataFromArgs()
  // does before the real parse, otherwise the parser would consume the next
  // argument as the path.
  std::vector<std::string> remaining;
  remaining.reserve(args.size());
  for (const std::string& arg : args) {
    remaining.push_back(IsBareConfigFileFlag(arg) ? DefaultConfigFileArg()
                                                  : arg);
  }

  // Parsing into throwaway options stops exactly where the real parse will.
  // Any errors are reported by the real parse later on.
  PerProcessOptions options;
  std::vector<std::string> v8_args;
  std::vector<std::string> errors;
  options_parser::Parse(
      &remaining, nullptr, &v8_args, &options, kDisallowedInEnvvar, &errors);

  // `remaining` is left with the program name and the unparsed arguments.
  return args.size() - remaining.size() + 1;
}

std::optional<std::string_view> ConfigReader::GetDataFromArgs(
    std::vector<std::string>* args) {
  std::optional<std::string_view> result;
  invalid_default_config_file_argument_ = false;

  // Only pay for the extra parse when a config file flag might be present.
  if (args->empty() || std::ranges::none_of(*args, IsConfigFileFlag)) {
    return result;
  }

  const size_t node_options_end = GetNodeOptionsEnd(*args);
  for (size_t i = 1; i < node_options_end; ++i) {
    std::string& arg = (*args)[i];

    if (IsBareConfigFileFlag(arg)) {
      // --config-file, --experimental-config-file or
      // --experimental-default-config-file
      arg = DefaultConfigFileArg();
      result = kDefaultConfigFileName;
    } else if (HasEqualsPrefix(arg, kConfigFileFlag) ||
               HasEqualsPrefix(arg, kExperimentalConfigFileFlag)) {
      // --config-file=path or --experimental-config-file=path
      std::string_view path = std::string_view(arg).substr(arg.find('=') + 1);
      if (!path.empty()) {
        result = path;
      }
    } 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 (--bench, --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;
  }

  // Walk the document again, this time to parse the configuration.
  document.rewind();
  if (document.get_object().get(main_object)) {
    return ParseResult::InvalidContent;
  }

  return ParseConfigObject(&main_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