From 3b3c645da2f4499ce73b455fb2ee00a026c174fa 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 | 91 ++++++++++++++-------------------- src/node_dotenv.h | 2 +- test/fixtures/dotenv/valid.env | 21 ++++++++ test/parallel/test-dotenv.js | 6 +++ 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc index 992633c50b9a14..6738ce119b2eaf 100644 --- a/src/node_dotenv.cc +++ b/src/node_dotenv.cc @@ -1,4 +1,6 @@ #include "node_dotenv.h" +#include // NOLINT(build/c++11) +#include #include "env-inl.h" #include "node_file.h" #include "uv.h" @@ -8,6 +10,15 @@ namespace node { using v8::NewStringType; using v8::String; +/** + * The inspiration for this implementation comes from the original dotenv code, + * available at https://github.com/motdotla/dotenv + */ +const std::regex + LINE("\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^']" + ")*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\r\n]+)?\\s*(?" + ":#.*)?"); // NOLINT(whitespace/line_length) + std::vector Dotenv::GetPathFromArgs( const std::vector& args) { const auto find_match = [](const std::string& arg) { @@ -81,7 +92,7 @@ bool Dotenv::ParsePath(const std::string_view path) { uv_fs_req_cleanup(&close_req); }); - std::string result{}; + std::string lines{}; char buffer[8192]; uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer)); @@ -95,15 +106,29 @@ bool Dotenv::ParsePath(const std::string_view path) { if (r <= 0) { break; } - result.append(buf.base, r); + lines.append(buf.base, r); } - using std::string_view_literals::operator""sv; - auto lines = SplitString(result, "\n"sv); + std::smatch match; + while (std::regex_search(lines, match, LINE)) { + const std::string key = match[1].str(); + + // Default undefined or null to an empty string + std::string value = match[2].str(); + + // Remove leading whitespaces + value.erase(0, value.find_first_not_of(" \t")); + + // Remove trailing whitespaces + value.erase(value.find_last_not_of(" \t") + 1); - for (const auto& line : lines) { - ParseLine(line); + // Remove surrounding quotes + value = trim_quotes(value); + + store_.insert_or_assign(std::string(key), value); + lines = match.suffix(); } + return true; } @@ -115,56 +140,12 @@ 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); - - // 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); - - // Omit lines with comments - if (key.front() == '#' || key.empty()) { - return; +std::string Dotenv::trim_quotes(std::string str) { + static const std::unordered_set quotes = {'"', '\'', '`'}; + if (str.size() >= 2 && quotes.count(str[0]) && quotes.count(str.back())) { + str = str.substr(1, str.size() - 2); } - - auto value = std::string(line.substr(equal_index + 1)); - - // Might start and end with `"' characters. - auto quotation_index = value.find_first_of("`\"'"); - - if (quotation_index == 0) { - auto quote_character = value[quotation_index]; - value.erase(0, 1); - - 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; - } - - 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); + return str; } } // namespace node diff --git a/src/node_dotenv.h b/src/node_dotenv.h index cc87008d149f43..fe25c43ef5895c 100644 --- a/src/node_dotenv.h +++ b/src/node_dotenv.h @@ -26,8 +26,8 @@ class Dotenv { const std::vector& args); private: - void ParseLine(const std::string_view line); std::map store_; + std::string trim_quotes(std::string str); }; } // namespace node diff --git a/test/fixtures/dotenv/valid.env b/test/fixtures/dotenv/valid.env index c1c12b112b965b..4518571254b0c5 100644 --- a/test/fixtures/dotenv/valid.env +++ b/test/fixtures/dotenv/valid.env @@ -33,3 +33,24 @@ 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` +MULTI_NOT_VALID_QUOTE=" +MULTI_NOT_VALID=THIS +IS NOT MULTILINE diff --git a/test/parallel/test-dotenv.js b/test/parallel/test-dotenv.js index 9c374c8735910d..492aef64963a0b 100644 --- a/test/parallel/test-dotenv.js +++ b/test/parallel/test-dotenv.js @@ -68,3 +68,9 @@ 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'); +assert.strictEqual(process.env.MULTI_NOT_VALID_QUOTE, '"'); +assert.strictEqual(process.env.MULTI_NOT_VALID, 'THIS');