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 7, 2024
1 parent 756acd0 commit 9df5358
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 37 deletions.
142 changes: 110 additions & 32 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 @@ -102,34 +93,93 @@ Local<Object> Dotenv::ToObject(Environment* env) {
}

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::smatch match;
while (std::regex_search(lines, match, LINE)) {
const std::string key = match[1].str();
std::string lines =
std::regex_replace(std::string(content), std::regex("\r\n?"), "\n");

std::istringstream contentStream(lines);
std::string currentLine;
std::string multiLineKey;
std::string multiLineValue;
bool isMultiLine = false;
char quoteChar = '\0';

while (std::getline(contentStream, currentLine)) {
// Check if we are currently in a multi-line value
if (isMultiLine) {
// Check if the current line ends the multi-line value
if (currentLine.back() == quoteChar) {
// append multi-line value and trim quotes
multiLineValue += "\n" + currentLine.substr(0, currentLine.size() - 1);
multiLineValue = trimQuotes(multiLineValue);
// add multi-line key/value
store_.insert_or_assign(multiLineKey, multiLineValue);

// Reset multi-line trackers
isMultiLine = false;
quoteChar = '\0';

} else {
// If the last char of currentLine is not the same as
// multi-line first quote just append the value
multiLineValue += "\n" + currentLine;
}

// Default undefined or null to an empty string
std::string value = match[2].str();
continue;

// Remove leading whitespaces
value.erase(0, value.find_first_not_of(" \t"));
} else {
bool isInQuotes = false;
for (size_t i = 0; i < currentLine.length(); ++i) {
char c = currentLine[i];

// If we found comment outside quotes ignore it
if (c == '#' && !isInQuotes) {
currentLine = currentLine.substr(0, i);
break;
}

// Handle entering/exiting quotes
if ((c == '"' || c == '\'' || c == '`')) {
isInQuotes = !isInQuotes;
if (isInQuotes) {
quoteChar = c;
}
}
}

// Remove trailing whitespaces
if (!value.empty()) {
value.erase(value.find_last_not_of(" \t") + 1);
// Trim the line from whitespace at both ends.
currentLine = trimWhitespace(currentLine);
}

if (!value.empty() && value.front() == '"') {
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
size_t equalPos = currentLine.find('=');
if (equalPos != std::string::npos) {
auto value = currentLine.substr(equalPos + 1);
auto key = currentLine.substr(0, equalPos);
// Remove export prefix if found
key = removeExport(key);

// Check for multi-line value start
if ((value.front() == '"' || value.front() == '\'' ||
value.front() == '`') &&
value.back() != value.front()) {
isMultiLine = true;
multiLineKey = key;
// Remove the opening quote for multiline value
multiLineValue = value.substr(1);
// Track the quote character used
quoteChar = value.front();

} else {
if (!value.empty() && value.front() == '"') {
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
}

key = trimWhitespace(key);
value = trimWhitespace(value);
value = trimQuotes(value);
store_.insert_or_assign(key, value);
}
}

// Remove surrounding quotes
value = trim_quotes(value);

store_.insert_or_assign(std::string(key), value);
lines = match.suffix();
}
}

Expand Down Expand Up @@ -179,7 +229,7 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
}
}

std::string_view Dotenv::trim_quotes(std::string_view str) {
std::string_view Dotenv::trimQuotes(std::string_view str) {
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
if (str.size() >= 2 && quotes.count(str.front()) &&
quotes.count(str.back())) {
Expand All @@ -188,4 +238,32 @@ std::string_view Dotenv::trim_quotes(std::string_view str) {
return str;
}

std::string_view Dotenv::removeComment(std::string_view line) {
auto firstNonWhitespace = line.find_first_not_of(" \t");
// Check if line is empty or starts with '#'
if (firstNonWhitespace == std::string::npos ||
line[firstNonWhitespace] == '#') {
return "";
}
return line;
}

std::string_view Dotenv::trimWhitespace(std::string_view value) {
size_t first = value.find_first_not_of(" \t");
if (first == std::string::npos) {
return "";
}
size_t last = value.find_last_not_of(" \t");
return value.substr(first, (last - first + 1));
}

std::string_view Dotenv::removeExport(std::string_view str) {
// Check if "export " is at the beginning
if (str.substr(0, 7) == "export ") {
return str.substr(7);
}

return str;
}

} // namespace node
5 changes: 4 additions & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ class Dotenv {

private:
std::map<std::string, std::string> store_;
std::string_view trim_quotes(std::string_view str);
std::string_view trimQuotes(const std::string_view str);
std::string_view removeComment(const std::string_view line);
std::string_view trimWhitespace(const std::string_view str);
std::string_view removeExport(std::string_view str);
};

} // namespace node
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/dotenv/valid.env
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ MULTI_NOT_VALID_QUOTE="
MULTI_NOT_VALID=THIS
IS NOT MULTILINE
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-----"
13 changes: 9 additions & 4 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 @@ -84,3 +80,12 @@ 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');

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);
9 changes: 9 additions & 0 deletions 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 Down Expand Up @@ -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 9df5358

Please sign in to comment.