Skip to content

Commit 019e869

Browse files
marco-ippolitonodejs-github-bot
authored andcommitted
src: fix config file handling
Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com> PR-URL: #66431 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
1 parent 3c57714 commit 019e869

2 files changed

Lines changed: 105 additions & 5 deletions

File tree

‎src/node_config_file.cc‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "node_version.h"
44
#include "simdjson.h"
55

6+
#include <algorithm>
67
#include <cinttypes>
78

89
namespace node {
@@ -21,20 +22,67 @@ inline bool HasEqualsPrefix(std::string_view arg, std::string_view flag) {
2122
arg[flag.size()] == '=';
2223
}
2324

25+
inline bool IsBareConfigFileFlag(std::string_view arg) {
26+
return arg == kConfigFileFlag || arg == kExperimentalConfigFileFlag ||
27+
arg == kDefaultConfigFileFlag;
28+
}
29+
30+
inline bool IsConfigFileFlag(std::string_view arg) {
31+
return IsBareConfigFileFlag(arg) || HasEqualsPrefix(arg, kConfigFileFlag) ||
32+
HasEqualsPrefix(arg, kExperimentalConfigFileFlag) ||
33+
HasEqualsPrefix(arg, kDefaultConfigFileFlag);
34+
}
35+
36+
inline std::string DefaultConfigFileArg() {
37+
return std::string(kConfigFileFlag) + "=" +
38+
std::string(kDefaultConfigFileName);
39+
}
40+
41+
// Returns the index of the first entry in `args` that is neither the program
42+
// name nor an option for Node.js itself. Everything from there on is the
43+
// script and its arguments, or follows `--`, so it must not be read as a
44+
// config file flag.
45+
size_t GetNodeOptionsEnd(const std::vector<std::string>& args) {
46+
// The bare flags take no value. Spell them out the way GetDataFromArgs()
47+
// does before the real parse, otherwise the parser would consume the next
48+
// argument as the path.
49+
std::vector<std::string> remaining;
50+
remaining.reserve(args.size());
51+
for (const std::string& arg : args) {
52+
remaining.push_back(IsBareConfigFileFlag(arg) ? DefaultConfigFileArg()
53+
: arg);
54+
}
55+
56+
// Parsing into throwaway options stops exactly where the real parse will.
57+
// Any errors are reported by the real parse later on.
58+
PerProcessOptions options;
59+
std::vector<std::string> v8_args;
60+
std::vector<std::string> errors;
61+
options_parser::Parse(
62+
&remaining, nullptr, &v8_args, &options, kDisallowedInEnvvar, &errors);
63+
64+
// `remaining` is left with the program name and the unparsed arguments.
65+
return args.size() - remaining.size() + 1;
66+
}
67+
2468
std::optional<std::string_view> ConfigReader::GetDataFromArgs(
2569
std::vector<std::string>* args) {
2670
std::optional<std::string_view> result;
2771
invalid_default_config_file_argument_ = false;
2872

29-
for (size_t i = 0; i < args->size(); ++i) {
73+
// Only pay for the extra parse when a config file flag might be present.
74+
if (args->empty() || std::ranges::none_of(*args, IsConfigFileFlag)) {
75+
return result;
76+
}
77+
78+
const size_t node_options_end = GetNodeOptionsEnd(*args);
79+
for (size_t i = 1; i < node_options_end; ++i) {
3080
std::string& arg = (*args)[i];
3181

32-
if (arg == kConfigFileFlag || arg == kExperimentalConfigFileFlag ||
33-
arg == kDefaultConfigFileFlag) {
82+
if (IsBareConfigFileFlag(arg)) {
3483
// --config-file, --experimental-config-file or
3584
// --experimental-default-config-file
36-
arg = std::string(kConfigFileFlag) + "=" +
37-
std::string(kDefaultConfigFileName);
85+
arg = DefaultConfigFileArg();
3886
result = kDefaultConfigFileName;
3987
} else if (HasEqualsPrefix(arg, kConfigFileFlag) ||
4088
HasEqualsPrefix(arg, kExperimentalConfigFileFlag)) {

‎test/parallel/test-config-file.js‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,58 @@ test('should treat a space-separated config file path as the script',
617617
assert.strictEqual(result.code, 1);
618618
});
619619

620+
describe('config file flags that are not Node.js options', () => {
621+
const argsVariants = [
622+
['--config-file'],
623+
['--config-file', 'tool.config.mjs'],
624+
['--config-file=i-do-not-exist.json'],
625+
['--experimental-config-file'],
626+
['--experimental-config-file=i-do-not-exist.json'],
627+
['--experimental-default-config-file'],
628+
['--experimental-default-config-file=i-do-not-exist.json'],
629+
];
630+
631+
for (const args of argsVariants) {
632+
it(`should not read ${args.join(' ')} after the script`, async () => {
633+
const result = await spawnPromisified(process.execPath, [
634+
fixtures.path('printA.js'),
635+
...args,
636+
], {
637+
cwd: fixtures.path('rc'),
638+
});
639+
assert.strictEqual(result.stderr, '');
640+
assert.strictEqual(result.stdout, 'A\n');
641+
assert.strictEqual(result.code, 0);
642+
});
643+
644+
it(`should not read ${args.join(' ')} after --`, async () => {
645+
const result = await spawnPromisified(process.execPath, [
646+
'-p', 'process.argv.slice(1).join(" ")',
647+
'--',
648+
...args,
649+
], {
650+
cwd: fixtures.path('rc'),
651+
});
652+
assert.strictEqual(result.stderr, '');
653+
assert.strictEqual(result.stdout, `${args.join(' ')}\n`);
654+
assert.strictEqual(result.code, 0);
655+
});
656+
}
657+
});
658+
659+
test('should read the config file after an option that takes a separate value',
660+
onlyIfNodeOptionsSupport, async () => {
661+
const result = await spawnPromisified(process.execPath, [
662+
'--no-warnings',
663+
'--title', 'config-file-test',
664+
`--config-file=${fixtures.path('rc/default/node.config.json')}`,
665+
'-p', 'http.maxHeaderSize',
666+
]);
667+
assert.strictEqual(result.stderr, '');
668+
assert.strictEqual(result.stdout, '10\n');
669+
assert.strictEqual(result.code, 0);
670+
});
671+
620672
test('should error when --config-file= has empty argument',
621673
onlyIfNodeOptionsSupport, async () => {
622674
const result = await spawnPromisified(process.execPath, [

0 commit comments

Comments
 (0)