Skip to content

Commit

Permalink
src: remove regex usage for env file parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyasShabiCS committed Apr 9, 2024
1 parent 756acd0 commit c42b6a0
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 46 deletions.
143 changes: 105 additions & 38 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,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) {
Expand Down Expand Up @@ -101,35 +92,120 @@ 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;
}

void Dotenv::ParseContent(const std::string_view input) {
std::string_view content = input;

std::string_view key;
std::string_view value;

std::smatch match;
while (std::regex_search(lines, match, LINE)) {
const std::string key = match[1].str();
content = trim_spaces(content);

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 there is no equal character, then ignore everything
auto equal = content.find('=');
if (equal == std::string_view::npos) {
break;
}

// Default undefined or null to an empty string
std::string value = match[2].str();
key = content.substr(0, equal);
content.remove_prefix(equal + 1);
key = trim_spaces(key);

// Remove leading whitespaces
value.erase(0, value.find_first_not_of(" \t"));
if (key.empty()) {
break;
}

// Remove trailing whitespaces
if (!value.empty()) {
value.erase(value.find_last_not_of(" \t") + 1);
// Remove export prefix from key
auto have_export = key.compare(0, 7, "export ") == 0;
if (have_export) {
key = key.substr(7);
}

if (!value.empty() && value.front() == '"') {
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
// SAFETY: Content is guaranteed to have at least one character
if (content.empty()) {
break;
}

// Remove surrounding quotes
value = trim_quotes(value);
// 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);
value = content.substr(1, closing_quote - 1);
if (closing_quote != std::string_view::npos) {
auto multi_line_value =
std::regex_replace(std::string(value), std::regex("\\\\n"), "\n");
store_.insert_or_assign(std::string(key), multi_line_value);
content.remove_prefix(content.find('\n', closing_quote + 1));
continue;
}
}

store_.insert_or_assign(std::string(key), value);
lines = match.suffix();
// 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);
}
value = trim_spaces(value);
content.remove_prefix(newline);
store_.insert_or_assign(std::string(key), value);
} else {
break;
}
}
}
}

Expand Down Expand Up @@ -179,13 +255,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
1 change: 0 additions & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion test/fixtures/dotenv/valid.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
BASIC=basic

# COMMENTS=work
#BASIC=basic2
#BASIC=basic3

# previous line intentionally left blank
AFTER_LINE=after_line
EMPTY=
Expand Down Expand Up @@ -55,7 +59,16 @@ IS
A
"MULTILINE'S"
STRING`
export EXPORT_EXAMPLE = ignore export

MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
u4QuUoobAgMBAAE=
-----END PUBLIC KEY-----"

MULTI_NOT_VALID_QUOTE="
MULTI_NOT_VALID=THIS
IS NOT MULTILINE
export EXAMPLE = ignore export
15 changes: 10 additions & 5 deletions test/parallel/test-dotenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -83,4 +79,13 @@ 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');

const multiPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
u4QuUoobAgMBAAE=
-----END PUBLIC KEY-----`;
assert.strictEqual(process.env.MULTI_PEM_DOUBLE_QUOTED, multiPem);
11 changes: 10 additions & 1 deletion test/parallel/util-parse-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ const assert = require('node:assert');
const util = require('node:util');
const fs = require('node:fs');

const multiPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
u4QuUoobAgMBAAE=
-----END PUBLIC KEY-----`;

{
const validEnvFilePath = fixtures.path('dotenv/valid.env');
const validContent = fs.readFileSync(validEnvFilePath, 'utf8');
Expand All @@ -32,7 +40,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',
Expand All @@ -53,6 +61,7 @@ const fs = require('node:fs');
SINGLE_QUOTES_SPACED: ' single quotes ',
SPACED_KEY: 'parsed',
TRIM_SPACE_FROM_UNQUOTED: 'some spaced out string',
MULTI_PEM_DOUBLE_QUOTED: multiPem,
});
}

Expand Down

0 comments on commit c42b6a0

Please sign in to comment.