diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc index 049f5cfcb77b9c..a27149e8645ca3 100644 --- a/src/node_dotenv.cc +++ b/src/node_dotenv.cc @@ -105,6 +105,11 @@ Local Dotenv::ToObject(Environment* env) const { return result; } +// Removes leading and trailing spaces from a string_view. +// Returns an empty string_view if the input is empty. +// Example: +// trim_spaces(" hello ") -> "hello" +// trim_spaces("") -> "" std::string_view trim_spaces(std::string_view input) { if (input.empty()) return ""; if (input.front() == ' ') { @@ -130,32 +135,62 @@ void Dotenv::ParseContent(const std::string_view input) { while (!content.empty()) { // Skip empty lines and comments + // Example: + // # This is a comment if (content.front() == '\n' || content.front() == '#') { - auto newline = content.find('\n'); - if (newline != std::string_view::npos) { - content.remove_prefix(newline + 1); - continue; + // Check if the first character of the content is a newline or a hash + if (content.front() == '\n') { + // If the first character is a newline, remove it + content.remove_prefix(1); + } else { + // If the first character is a hash, find the next newline character + auto newline = content.find('\n'); + if (newline != std::string_view::npos) { + // If a newline is found, remove the comment line including the + // newline character. + content.remove_prefix(newline + 1); + } } + + // Skip the remaining code in the loop and continue with the next + // iteration. + continue; } - // If there is no equal character, then ignore everything - auto equal = content.find('='); - if (equal == std::string_view::npos) { + // Find the next equals sign or newline in a single pass. + // This optimizes the search by avoiding multiple iterations. + auto equal_or_newline = content.find_first_of("=\n"); + + // If we found nothing or found a newline before equals, the line is invalid + if (equal_or_newline == std::string_view::npos || + 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; } - key = content.substr(0, equal); - content.remove_prefix(equal + 1); + // We found an equals sign, extract the key + key = content.substr(0, equal_or_newline); + content.remove_prefix(equal_or_newline + 1); key = trim_spaces(key); content = trim_spaces(content); - if (key.empty()) { - break; - } + // Skip lines with empty keys after trimming spaces. + // Examples of invalid keys that would be skipped: + // =value + // " "=value + if (key.empty()) continue; - // Remove export prefix from key + // Remove export prefix from key and ensure proper spacing. + // Example: export FOO=bar -> FOO=bar if (key.starts_with("export ")) { key.remove_prefix(7); + // Trim spaces after removing export prefix to handle cases like: + // export FOO=bar + key = trim_spaces(key); } // SAFETY: Content is guaranteed to have at least one character @@ -174,6 +209,7 @@ void Dotenv::ParseContent(const std::string_view input) { value = content.substr(1, closing_quote - 1); std::string multi_line_value = std::string(value); + // Replace \n with actual newlines in double-quoted strings size_t pos = 0; while ((pos = multi_line_value.find("\\n", pos)) != std::string_view::npos) { @@ -190,9 +226,9 @@ void Dotenv::ParseContent(const std::string_view input) { } } - // Check if the value is wrapped in quotes, single quotes or backticks - if ((content.front() == '\'' || content.front() == '"' || - content.front() == '`')) { + // Handle quoted values (single quotes, double quotes, backticks) + if (content.front() == '\'' || content.front() == '"' || + content.front() == '`') { auto closing_quote = content.find(content.front(), 1); // Check if the closing quote is not found @@ -206,16 +242,21 @@ void Dotenv::ParseContent(const std::string_view input) { value = content.substr(0, newline); store_.insert_or_assign(std::string(key), value); content.remove_prefix(newline); + } else { + // No newline - take rest of content + value = content; + store_.insert_or_assign(std::string(key), value); + break; } } else { - // Example: KEY="value" + // Found closing quote - take content between quotes value = content.substr(1, closing_quote - 1); store_.insert_or_assign(std::string(key), value); - // Select the first newline after the closing quotation mark - // 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); + } else { + break; } } } else { @@ -230,18 +271,21 @@ void Dotenv::ParseContent(const std::string_view input) { // Example: KEY=value # comment // The value pair should be `value` if (hash_character != std::string_view::npos) { - value = content.substr(0, hash_character); + value = value.substr(0, hash_character); } + value = trim_spaces(value); + store_.insert_or_assign(std::string(key), std::string(value)); content.remove_prefix(newline); } 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); + // Last line without newline + value = content; + value = trim_spaces(value); + store_.insert_or_assign(std::string(key), std::string(value)); + break; } - - value = trim_spaces(value); - store_.insert_or_assign(std::string(key), value); } + + content = trim_spaces(content); } } diff --git a/test/fixtures/dotenv/invalid-syntax.env b/test/fixtures/dotenv/invalid-syntax.env new file mode 100644 index 00000000000000..1fa57c00b281e7 --- /dev/null +++ b/test/fixtures/dotenv/invalid-syntax.env @@ -0,0 +1,8 @@ +foo + +bar +baz=whatever +VALID_AFTER_INVALID=test +multiple_invalid +lines_without_equals +ANOTHER_VALID=value diff --git a/test/parallel/test-dotenv-edge-cases.js b/test/parallel/test-dotenv-edge-cases.js index 926c8d0793ac8b..1ce49b565392cd 100644 --- a/test/parallel/test-dotenv-edge-cases.js +++ b/test/parallel/test-dotenv-edge-cases.js @@ -182,4 +182,30 @@ describe('.env supports edge cases', () => { assert.strictEqual(child.code, 9); assert.match(child.stderr, /bad option: --env-file-ABCD/); }); + + it('should handle invalid syntax in .env file', async () => { + const invalidEnvFilePath = fixtures.path('dotenv/invalid-syntax.env'); + const code = ` + const { parseEnv } = require('node:util'); + + const input = fs.readFileSync(${JSON.stringify(invalidEnvFilePath)}, 'utf8'); + const result = parseEnv(input); + assert.strictEqual(Object.keys(result).length, 3); + assert.strictEqual(result.baz, 'whatever'); + assert.strictEqual(result.VALID_AFTER_INVALID, 'test'); + assert.strictEqual(result.ANOTHER_VALID, 'value'); + assert.strictEqual(result.foo, undefined); + assert.strictEqual(result.bar, undefined); + assert.strictEqual(result.multiple_invalid, undefined); + assert.strictEqual(result.lines_without_equals, undefined); + `.trim(); + const child = await common.spawnPromisified( + process.execPath, + [ '--eval', code ], + { cwd: __dirname }, + ); + assert.strictEqual(child.stderr, ''); + assert.strictEqual(child.stdout, ''); + assert.strictEqual(child.code, 0); + }); });