Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ void Dotenv::ParseContent(const std::string_view input) {
std::string_view key;
std::string_view value;

while (!content.empty()) {
// The increment also runs after `continue`, so every line starts trimmed
// no matter which branch consumed the previous one.
for (; !content.empty(); content = trim_spaces(content)) {
// Skip empty lines and comments
if (content.front() == '\n' || content.front() == '#') {
// Check if the first character of the content is a newline or a hash
Expand All @@ -183,7 +185,6 @@ void Dotenv::ParseContent(const std::string_view input) {
content.at(equal_or_newline) == '\n') {
if (equal_or_newline != std::string_view::npos) {
content.remove_prefix(equal_or_newline + 1);
content = trim_spaces(content);
continue;
}
break;
Expand All @@ -194,14 +195,6 @@ void Dotenv::ParseContent(const std::string_view input) {
content.remove_prefix(equal_or_newline + 1);
key = trim_spaces(key);

// If the value is not present (e.g. KEY=) set it to an empty string
if (content.empty() || content.front() == '\n') {
store_.insert_or_assign(std::string(key), "");
continue;
}

content = trim_spaces(content);

// Skip lines with empty keys after trimming spaces.
// Examples of invalid keys that would be skipped:
// =value
Expand All @@ -217,14 +210,19 @@ void Dotenv::ParseContent(const std::string_view input) {
key = trim_spaces(key);
}

// SAFETY: Content is guaranteed to have at least one character
if (content.empty()) {
// In case the last line is a single key without value
// Example: KEY= (without a newline at the EOF)
// Skip spaces and tabs before the value, but not a newline: in
// `KEY= \nNEXT=value` the value of KEY is empty.
content.remove_prefix(
std::min(content.find_first_not_of(" \t"), content.size()));

// If the value is not present (e.g. KEY=) set it to an empty string
if (content.empty() || content.front() == '\n') {
store_.insert_or_assign(std::string(key), "");
break;
continue;
}

content = trim_spaces(content);

// Expand new line if \n it's inside double quotes
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
if (content.front() == '"') {
Expand Down Expand Up @@ -318,8 +316,6 @@ void Dotenv::ParseContent(const std::string_view input) {
content = {};
}
}

content = trim_spaces(content);
}
}

Expand Down
22 changes: 22 additions & 0 deletions test/parallel/test-util-parse-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ assert.deepStrictEqual(util.parseEnv(''), { __proto__: null });
assert.deepStrictEqual(util.parseEnv('FOO=bar\nFOO=baz\n'),
{ __proto__: null, FOO: 'baz' });

// An empty value must not consume the next line or skip key normalization.
assert.deepStrictEqual(util.parseEnv('EMPTY= \nNEXT=value\n'),
{ __proto__: null, EMPTY: '', NEXT: 'value' });
assert.deepStrictEqual(util.parseEnv('EMPTY=\t\nNEXT=value'),
{ __proto__: null, EMPTY: '', NEXT: 'value' });
assert.deepStrictEqual(util.parseEnv('export EMPTY=\nexport SPACED= '),
{ __proto__: null, EMPTY: '', SPACED: '' });
assert.deepStrictEqual(util.parseEnv('=\nNEXT=value'),
{ __proto__: null, NEXT: 'value' });

// Comments indented with spaces are ignored after every kind of line.
assert.deepStrictEqual(util.parseEnv([
'A="a"', ' # X=1',
"B='b'", ' # X=2',
'C=`c`', ' # X=3',
'D=', ' # X=4',
'E= ', ' # X=5',
'= # X=6',
'# comment', ' # X=7',
'F=f',
].join('\n')), { __proto__: null, A: 'a', B: 'b', C: 'c', D: '', E: '', F: 'f' });

// Test for invalid input.
assert.throws(() => {
for (const value of [null, undefined, {}, []]) {
Expand Down
Loading