mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
src: remove regex usage for env file parsing
PR-URL: https://github.com/nodejs/node/pull/52406 Fixes: https://github.com/nodejs/node/issues/52248 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
This commit is contained in:
1 parent
f283d27285
commit
ac04c6434a
8 files changed
+173
-47
No files matched your search
+122
-39
@@ -1,5 +1,4 @@
|
||||
#include "node_dotenv.h"
|
||||
#include <regex> // NOLINT(build/c++11)
|
||||
#include <unordered_set>
|
||||
#include "env-inl.h"
|
||||
#include "node_file.h"
|
||||
@@ -12,15 +11,6 @@ using v8::NewStringType;
|
||||
using v8::Object;
|
||||
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<std::string> Dotenv::GetPathFromArgs(
|
||||
const std::vector<std::string>& args) {
|
||||
const auto find_match = [](const std::string& arg) {
|
||||
@@ -101,35 +91,137 @@ Local<Object> Dotenv::ToObject(Environment* env) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Dotenv::ParseContent(const std::string_view content) {
|
||||
std::string lines = std::string(content);
|
||||
lines = std::regex_replace(lines, std::regex("\r\n?"), "\n");
|
||||
std::string_view trim_spaces(std::string_view input) {
|
||||
if (input.empty()) return "";
|
||||
if (input.front() == ' ') {
|
||||
input.remove_prefix(input.find_first_not_of(' '));
|
||||
}
|
||||
if (!input.empty() && input.back() == ' ') {
|
||||
input = input.substr(0, input.find_last_not_of(' ') + 1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
std::smatch match;
|
||||
while (std::regex_search(lines, match, LINE)) {
|
||||
const std::string key = match[1].str();
|
||||
void Dotenv::ParseContent(const std::string_view input) {
|
||||
std::string lines(input);
|
||||
|
||||
// Default undefined or null to an empty string
|
||||
std::string value = match[2].str();
|
||||
// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
|
||||
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
|
||||
|
||||
// Remove leading whitespaces
|
||||
value.erase(0, value.find_first_not_of(" \t"));
|
||||
std::string_view content = lines;
|
||||
content = trim_spaces(content);
|
||||
|
||||
// Remove trailing whitespaces
|
||||
if (!value.empty()) {
|
||||
value.erase(value.find_last_not_of(" \t") + 1);
|
||||
std::string_view key;
|
||||
std::string_view value;
|
||||
|
||||
while (!content.empty()) {
|
||||
// Skip empty lines and comments
|
||||
if (content.front() == '\n' || content.front() == '#') {
|
||||
auto newline = content.find('\n');
|
||||
if (newline != std::string_view::npos) {
|
||||
content.remove_prefix(newline + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value.empty() && value.front() == '"') {
|
||||
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
|
||||
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
|
||||
// If there is no equal character, then ignore everything
|
||||
auto equal = content.find('=');
|
||||
if (equal == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove surrounding quotes
|
||||
value = trim_quotes(value);
|
||||
key = content.substr(0, equal);
|
||||
content.remove_prefix(equal + 1);
|
||||
key = trim_spaces(key);
|
||||
|
||||
store_.insert_or_assign(std::string(key), value);
|
||||
lines = match.suffix();
|
||||
if (key.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove export prefix from key
|
||||
auto have_export = key.compare(0, 7, "export ") == 0;
|
||||
if (have_export) {
|
||||
key.remove_prefix(7);
|
||||
}
|
||||
|
||||
// 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)
|
||||
store_.insert_or_assign(std::string(key), "");
|
||||
break;
|
||||
}
|
||||
|
||||
// Expand new line if \n it's inside double quotes
|
||||
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
|
||||
if (content.front() == '"') {
|
||||
auto closing_quote = content.find(content.front(), 1);
|
||||
if (closing_quote != std::string_view::npos) {
|
||||
value = content.substr(1, closing_quote - 1);
|
||||
std::string multi_line_value = std::string(value);
|
||||
|
||||
size_t pos = 0;
|
||||
while ((pos = multi_line_value.find("\\n", pos)) !=
|
||||
std::string_view::npos) {
|
||||
multi_line_value.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
store_.insert_or_assign(std::string(key), multi_line_value);
|
||||
content.remove_prefix(content.find('\n', closing_quote + 1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the value is wrapped in quotes, single quotes or backticks
|
||||
if ((content.front() == '\'' || content.front() == '"' ||
|
||||
content.front() == '`')) {
|
||||
auto closing_quote = content.find(content.front(), 1);
|
||||
|
||||
// Check if the closing quote is not found
|
||||
// Example: KEY="value
|
||||
if (closing_quote == std::string_view::npos) {
|
||||
// Check if newline exist. If it does, take the entire line as the value
|
||||
// Example: KEY="value\nKEY2=value2
|
||||
// The value pair should be `"value`
|
||||
auto newline = content.find('\n');
|
||||
if (newline != std::string_view::npos) {
|
||||
value = content.substr(0, newline);
|
||||
store_.insert_or_assign(std::string(key), value);
|
||||
content.remove_prefix(newline);
|
||||
}
|
||||
} else {
|
||||
// Example: KEY="value"
|
||||
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.
|
||||
content.remove_prefix(content.find('\n', closing_quote + 1));
|
||||
}
|
||||
} else {
|
||||
// Regular key value pair.
|
||||
// Example: `KEY=this is value`
|
||||
auto newline = content.find('\n');
|
||||
|
||||
if (newline != std::string_view::npos) {
|
||||
value = content.substr(0, newline);
|
||||
auto hash_character = value.find('#');
|
||||
// Check if there is a comment in the line
|
||||
// Example: KEY=value # comment
|
||||
// The value pair should be `value`
|
||||
if (hash_character != std::string_view::npos) {
|
||||
value = content.substr(0, hash_character);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
value = trim_spaces(value);
|
||||
store_.insert_or_assign(std::string(key), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +271,4 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view Dotenv::trim_quotes(std::string_view str) {
|
||||
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
|
||||
if (str.size() >= 2 && quotes.count(str.front()) &&
|
||||
quotes.count(str.back())) {
|
||||
str = str.substr(1, str.size() - 2);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
} // namespace node
|
||||
@@ -32,7 +32,6 @@ class Dotenv {
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> store_;
|
||||
std::string_view trim_quotes(std::string_view str);
|
||||
};
|
||||
|
||||
} // namespace node
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
BASIC=value
|
||||
EMPTY=
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
|
||||
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
|
||||
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
|
||||
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
|
||||
u4QuUoobAgMBAAE=
|
||||
-----END PUBLIC KEY-----"
|
||||
Vendored
+6
-1
@@ -1,5 +1,9 @@
|
||||
BASIC=basic
|
||||
|
||||
# COMMENTS=work
|
||||
#BASIC=basic2
|
||||
#BASIC=basic3
|
||||
|
||||
# previous line intentionally left blank
|
||||
AFTER_LINE=after_line
|
||||
EMPTY=
|
||||
@@ -55,7 +59,8 @@ IS
|
||||
A
|
||||
"MULTILINE'S"
|
||||
STRING`
|
||||
export EXPORT_EXAMPLE = ignore export
|
||||
|
||||
MULTI_NOT_VALID_QUOTE="
|
||||
MULTI_NOT_VALID=THIS
|
||||
IS NOT MULTILINE
|
||||
export EXAMPLE = ignore export
|
||||
@@ -4,6 +4,7 @@ const common = require('../common');
|
||||
const assert = require('node:assert');
|
||||
const path = require('node:path');
|
||||
const { describe, it } = require('node:test');
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const validEnvFilePath = '../fixtures/dotenv/valid.env';
|
||||
const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
|
||||
@@ -64,4 +65,37 @@ describe('.env supports edge cases', () => {
|
||||
assert.strictEqual(child.stderr, '');
|
||||
assert.strictEqual(child.code, 0);
|
||||
});
|
||||
|
||||
it('should handle multiline quoted values', async () => {
|
||||
// Ref: https://github.com/nodejs/node/issues/52248
|
||||
const code = `
|
||||
process.loadEnvFile('./multiline.env');
|
||||
require('node:assert').ok(process.env.JWT_PUBLIC_KEY);
|
||||
`.trim();
|
||||
const child = await common.spawnPromisified(
|
||||
process.execPath,
|
||||
[ '--eval', code ],
|
||||
{ cwd: fixtures.path('dotenv') },
|
||||
);
|
||||
assert.strictEqual(child.stdout, '');
|
||||
assert.strictEqual(child.stderr, '');
|
||||
assert.strictEqual(child.code, 0);
|
||||
});
|
||||
|
||||
it('should handle empty value without a newline at the EOF', async () => {
|
||||
// Ref: https://github.com/nodejs/node/issues/52466
|
||||
const code = `
|
||||
process.loadEnvFile('./eof-without-value.env');
|
||||
require('assert').strictEqual(process.env.BASIC, 'value');
|
||||
require('assert').strictEqual(process.env.EMPTY, '');
|
||||
`.trim();
|
||||
const child = await common.spawnPromisified(
|
||||
process.execPath,
|
||||
[ '--eval', code ],
|
||||
{ cwd: fixtures.path('dotenv') },
|
||||
);
|
||||
assert.strictEqual(child.stdout, '');
|
||||
assert.strictEqual(child.stderr, '');
|
||||
assert.strictEqual(child.code, 0);
|
||||
});
|
||||
});
|
||||
@@ -58,10 +58,6 @@ assert.strictEqual(process.env.COMMENTS, undefined);
|
||||
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==');
|
||||
// Retains inner quotes
|
||||
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}');
|
||||
// Respects equals signs in values
|
||||
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==');
|
||||
// Retains inner quotes
|
||||
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}');
|
||||
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}');
|
||||
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_BACKTICKS, '{"foo": "bar\'s"}');
|
||||
// Retains spaces in string
|
||||
@@ -83,4 +79,4 @@ assert.strictEqual(process.env.EXPAND_NEWLINES, 'expand\nnew\nlines');
|
||||
assert.strictEqual(process.env.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines');
|
||||
assert.strictEqual(process.env.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines');
|
||||
// Ignore export before key
|
||||
assert.strictEqual(process.env.EXAMPLE, 'ignore export');
|
||||
assert.strictEqual(process.env.EXPORT_EXAMPLE, 'ignore export');
|
||||
@@ -32,7 +32,7 @@ const fs = require('node:fs');
|
||||
EMPTY_DOUBLE_QUOTES: '',
|
||||
EMPTY_SINGLE_QUOTES: '',
|
||||
EQUAL_SIGNS: 'equals==',
|
||||
EXAMPLE: 'ignore export',
|
||||
EXPORT_EXAMPLE: 'ignore export',
|
||||
EXPAND_NEWLINES: 'expand\nnew\nlines',
|
||||
INLINE_COMMENTS: 'inline comments',
|
||||
INLINE_COMMENTS_BACKTICKS: 'inline comments outside of #backticks',
|
||||
|
||||
Reference in new issue
Block a user