diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc index 0c6fa30a41d7..bde719d9008c 100644 --- a/src/node_dotenv.cc +++ b/src/node_dotenv.cc @@ -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 @@ -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; @@ -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 @@ -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() == '"') { @@ -318,8 +316,6 @@ void Dotenv::ParseContent(const std::string_view input) { content = {}; } } - - content = trim_spaces(content); } } diff --git a/test/parallel/test-util-parse-env.js b/test/parallel/test-util-parse-env.js index e932a31f683e..f68a17358f3a 100644 --- a/test/parallel/test-util-parse-env.js +++ b/test/parallel/test-util-parse-env.js @@ -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, {}, []]) {