util: fix parseEnv incorrectly splitting multiple ‘=‘ in value

Previously, parseEnv would create multiple environment
variables if a single line contained multiple ‘=‘ characters
(e.g. A=B=C would become { A: ‘B=C’, B: ‘C’ }).
This commit ensures that only the first ‘=‘ is used as
the key-value delimiter, and the rest of the line is treated
as the value.

Fixes: https://github.com/nodejs/node/issues/57411
PR-URL: https://github.com/nodejs/node/pull/57421
Reviewed-By: Daniel Lemire <daniel@lemire.me>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
HEESEUNG 2025-04-06 16:47:28 +09:00 committed by GitHub
parent 74722a55a6
commit e6a0d77d57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 38 additions and 8 deletions

View File

@ -6,6 +6,8 @@ BASIC=basic
# previous line intentionally left blank
AFTER_LINE=after_line
A="B=C"
B=C=D
EMPTY=
EMPTY_SINGLE_QUOTES=''
EMPTY_DOUBLE_QUOTES=""

View File

@ -145,7 +145,16 @@ void Dotenv::ParseContent(const std::string_view input) {
// If there is no equal character, then ignore everything
auto equal = content.find('=');
if (equal == std::string_view::npos) {
break;
auto newline = content.find('\n');
if (newline != std::string_view::npos) {
// If we used `newline` only,
// the '\n' might remain and cause an empty-line parse
content.remove_prefix(newline + 1);
} else {
content = {};
}
// No valid data here, skip to next line
continue;
}
key = content.substr(0, equal);
@ -195,7 +204,9 @@ void Dotenv::ParseContent(const std::string_view input) {
store_.insert_or_assign(std::string(key), multi_line_value);
auto newline = content.find('\n', closing_quote + 1);
if (newline != std::string_view::npos) {
content.remove_prefix(newline);
content.remove_prefix(newline + 1);
} else {
content = {};
}
continue;
}
@ -216,7 +227,7 @@ void Dotenv::ParseContent(const std::string_view input) {
if (newline != std::string_view::npos) {
value = content.substr(0, newline);
store_.insert_or_assign(std::string(key), value);
content.remove_prefix(newline);
content.remove_prefix(newline + 1);
}
} else {
// Example: KEY="value"
@ -226,8 +237,13 @@ void Dotenv::ParseContent(const std::string_view input) {
// since there could be newline characters inside the value.
auto newline = content.find('\n', closing_quote + 1);
if (newline != std::string_view::npos) {
content.remove_prefix(newline);
// Use +1 to discard the '\n' itself => next line
content.remove_prefix(newline + 1);
} else {
content = {};
}
// No valid data here, skip to next line
continue;
}
} else {
// Regular key value pair.
@ -243,15 +259,21 @@ void Dotenv::ParseContent(const std::string_view input) {
if (hash_character != std::string_view::npos) {
value = content.substr(0, hash_character);
}
content.remove_prefix(newline);
store_.insert_or_assign(std::string(key), trim_spaces(value));
content.remove_prefix(newline + 1);
} else {
// In case the last line is a single key/value pair
// Example: KEY=VALUE (without a newline at the EOF)
value = content.substr(0);
value = content;
auto hash_char = value.find('#');
if (hash_char != std::string_view::npos) {
value = content.substr(0, hash_char);
}
store_.insert_or_assign(std::string(key), trim_spaces(value));
content = {};
}
value = trim_spaces(value);
store_.insert_or_assign(std::string(key), value);
store_.insert_or_assign(std::string(key), trim_spaces(value));
}
}
}

View File

@ -6,6 +6,8 @@ BASIC=basic
# previous line intentionally left blank
AFTER_LINE=after_line
A="B=C"
B=C=D
EMPTY=
EMPTY_SINGLE_QUOTES=''
EMPTY_DOUBLE_QUOTES=""

View File

@ -82,3 +82,5 @@ assert.strictEqual(process.env.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines');
assert.strictEqual(process.env.EXPORT_EXAMPLE, 'ignore export');
// Ignore spaces before double quotes to avoid quoted strings as value
assert.strictEqual(process.env.SPACE_BEFORE_DOUBLE_QUOTES, 'space before double quotes');
assert.strictEqual(process.env.A, 'B=C');
assert.strictEqual(process.env.B, 'C=D');

View File

@ -11,6 +11,8 @@ const fs = require('node:fs');
const validContent = fs.readFileSync(validEnvFilePath, 'utf8');
assert.deepStrictEqual(util.parseEnv(validContent), {
A: 'B=C',
B: 'C=D',
AFTER_LINE: 'after_line',
BACKTICKS: 'backticks',
BACKTICKS_INSIDE_DOUBLE: '`backticks` work inside double quotes',