From 35eb89c7f37aca6a4b5f3b8176ae07415d5bb1a2 Mon Sep 17 00:00:00 2001 From: Ilyas Shabi Date: Tue, 26 Dec 2023 13:44:48 +0100 Subject: [PATCH] src: support multi-line values for .env file --- src/node_dotenv.cc | 77 +++++++++++++--------------------- src/node_dotenv.h | 2 +- test/fixtures/dotenv/valid.env | 18 ++++++++ test/parallel/test-dotenv.js | 4 ++ 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc index 992633c50b9a149..62ba671a9d46e81 100644 --- a/src/node_dotenv.cc +++ b/src/node_dotenv.cc @@ -1,4 +1,5 @@ #include "node_dotenv.h" +#include #include "env-inl.h" #include "node_file.h" #include "uv.h" @@ -8,6 +9,12 @@ namespace node { using v8::NewStringType; using v8::String; +std::regex LINE( + "(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|" + "[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?" + "\\s*(?:#.*)?(?:$|$)", + std::regex::multiline); + std::vector Dotenv::GetPathFromArgs( const std::vector& args) { const auto find_match = [](const std::string& arg) { @@ -98,12 +105,7 @@ bool Dotenv::ParsePath(const std::string_view path) { result.append(buf.base, r); } - using std::string_view_literals::operator""sv; - auto lines = SplitString(result, "\n"sv); - - for (const auto& line : lines) { - ParseLine(line); - } + Parse(result); return true; } @@ -115,56 +117,37 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) { } } -void Dotenv::ParseLine(const std::string_view line) { - auto equal_index = line.find('='); - - if (equal_index == std::string_view::npos) { - return; - } - - auto key = line.substr(0, equal_index); +void Dotenv::Parse(const std::string& src) { + // Convert line breaks to the same format + std::string lines = src; + std::regex_replace(lines, std::regex("\r\n?"), "\n"); - // Remove leading and trailing space characters from key. - while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1); - while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1); + std::smatch match; + while (std::regex_search(lines, match, LINE)) { + const std::string key = match[1].str(); - // Omit lines with comments - if (key.front() == '#' || key.empty()) { - return; - } + // Default undefined or null to an empty string + std::string value = match[2].str(); - auto value = std::string(line.substr(equal_index + 1)); + // Remove whitespace + value = std::regex_replace(value, std::regex("^\\s+|\\s+$"), ""); - // Might start and end with `"' characters. - auto quotation_index = value.find_first_of("`\"'"); + // Check if double-quoted + const char maybeQuote = value[0]; - if (quotation_index == 0) { - auto quote_character = value[quotation_index]; - value.erase(0, 1); + // Remove surrounding quotes + value = + std::regex_replace(value, std::regex("^(['\"`])([\\s\\S]*)\\1$"), "$2"); - auto end_quotation_index = value.find_last_of(quote_character); - - // We couldn't find the closing quotation character. Terminate. - if (end_quotation_index == std::string::npos) { - return; + // Expand newlines if double quoted + if (maybeQuote == '"') { + value = std::regex_replace(value, std::regex("\\\\n"), "\n"); + value = std::regex_replace(value, std::regex("\\\\r"), "\r"); } - value.erase(end_quotation_index); - } else { - auto hash_index = value.find('#'); - - // Remove any inline comments - if (hash_index != std::string::npos) { - value.erase(hash_index); - } - - // Remove any leading/trailing spaces from unquoted values. - while (!value.empty() && std::isspace(value.front())) value.erase(0, 1); - while (!value.empty() && std::isspace(value.back())) - value.erase(value.size() - 1); + store_.insert_or_assign(std::string(key), value); + lines = match.suffix(); } - - store_.insert_or_assign(std::string(key), value); } } // namespace node diff --git a/src/node_dotenv.h b/src/node_dotenv.h index cc87008d149f432..d278cec01aba0b3 100644 --- a/src/node_dotenv.h +++ b/src/node_dotenv.h @@ -26,7 +26,7 @@ class Dotenv { const std::vector& args); private: - void ParseLine(const std::string_view line); + void Parse(const std::string& src); std::map store_; }; diff --git a/test/fixtures/dotenv/valid.env b/test/fixtures/dotenv/valid.env index c1c12b112b965bb..f55bef8190c267d 100644 --- a/test/fixtures/dotenv/valid.env +++ b/test/fixtures/dotenv/valid.env @@ -33,3 +33,21 @@ RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}` TRIM_SPACE_FROM_UNQUOTED= some spaced out string EMAIL=therealnerdybeast@example.tld SPACED_KEY = parsed + +MULTI_DOUBLE_QUOTED="THIS +IS +A +MULTILINE +STRING" + +MULTI_SINGLE_QUOTED='THIS +IS +A +MULTILINE +STRING' + +MULTI_BACKTICKED=`THIS +IS +A +"MULTILINE'S" +STRING` \ No newline at end of file diff --git a/test/parallel/test-dotenv.js b/test/parallel/test-dotenv.js index 9c374c8735910d7..a0995a7b81ff6a4 100644 --- a/test/parallel/test-dotenv.js +++ b/test/parallel/test-dotenv.js @@ -68,3 +68,7 @@ assert.strictEqual(process.env.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string assert.strictEqual(process.env.EMAIL, 'therealnerdybeast@example.tld'); // Parses keys and values surrounded by spaces assert.strictEqual(process.env.SPACED_KEY, 'parsed'); +// Test multiple-line value +assert.strictEqual(process.env.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING'); +assert.strictEqual(process.env.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING'); +assert.strictEqual(process.env.MULTI_BACKTICKED, 'THIS\nIS\nA\n"MULTILINE\'S"\nSTRING'); \ No newline at end of file