Skip to content

Commit c42b6a0

Browse files
committed
src: remove regex usage for env file parsing
1 parent 756acd0 commit c42b6a0

File tree

5 files changed

+139
-46
lines changed

5 files changed

+139
-46
lines changed

src/node_dotenv.cc

+105-38
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@ using v8::NewStringType;
1212
using v8::Object;
1313
using v8::String;
1414

15-
/**
16-
* The inspiration for this implementation comes from the original dotenv code,
17-
* available at https://github.com/motdotla/dotenv
18-
*/
19-
const std::regex LINE(
20-
"\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^']"
21-
")*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\r\n]+)?\\s*(?"
22-
":#.*)?"); // NOLINT(whitespace/line_length)
23-
2415
std::vector<std::string> Dotenv::GetPathFromArgs(
2516
const std::vector<std::string>& args) {
2617
const auto find_match = [](const std::string& arg) {
@@ -101,35 +92,120 @@ Local<Object> Dotenv::ToObject(Environment* env) {
10192
return result;
10293
}
10394

104-
void Dotenv::ParseContent(const std::string_view content) {
105-
std::string lines = std::string(content);
106-
lines = std::regex_replace(lines, std::regex("\r\n?"), "\n");
95+
std::string_view trim_spaces(std::string_view input) {
96+
if (input.empty()) return "";
97+
if (input.front() == ' ') {
98+
input.remove_prefix(input.find_first_not_of(' '));
99+
}
100+
if (!input.empty() && input.back() == ' ') {
101+
input = input.substr(0, input.find_last_not_of(' ') + 1);
102+
}
103+
return input;
104+
}
105+
106+
void Dotenv::ParseContent(const std::string_view input) {
107+
std::string_view content = input;
108+
109+
std::string_view key;
110+
std::string_view value;
107111

108-
std::smatch match;
109-
while (std::regex_search(lines, match, LINE)) {
110-
const std::string key = match[1].str();
112+
content = trim_spaces(content);
113+
114+
while (!content.empty()) {
115+
// Skip empty lines and comments
116+
if (content.front() == '\n' || content.front() == '#') {
117+
auto newline = content.find('\n');
118+
if (newline != std::string_view::npos) {
119+
content.remove_prefix(newline + 1);
120+
continue;
121+
}
122+
}
123+
124+
// If there is no equal character, then ignore everything
125+
auto equal = content.find('=');
126+
if (equal == std::string_view::npos) {
127+
break;
128+
}
111129

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

115-
// Remove leading whitespaces
116-
value.erase(0, value.find_first_not_of(" \t"));
134+
if (key.empty()) {
135+
break;
136+
}
117137

118-
// Remove trailing whitespaces
119-
if (!value.empty()) {
120-
value.erase(value.find_last_not_of(" \t") + 1);
138+
// Remove export prefix from key
139+
auto have_export = key.compare(0, 7, "export ") == 0;
140+
if (have_export) {
141+
key = key.substr(7);
121142
}
122143

123-
if (!value.empty() && value.front() == '"') {
124-
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
125-
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
144+
// SAFETY: Content is guaranteed to have at least one character
145+
if (content.empty()) {
146+
break;
126147
}
127148

128-
// Remove surrounding quotes
129-
value = trim_quotes(value);
149+
// Expand new line if \n it's inside double quotes
150+
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
151+
if (content.front() == '"') {
152+
auto closing_quote = content.find(content.front(), 1);
153+
value = content.substr(1, closing_quote - 1);
154+
if (closing_quote != std::string_view::npos) {
155+
auto multi_line_value =
156+
std::regex_replace(std::string(value), std::regex("\\\\n"), "\n");
157+
store_.insert_or_assign(std::string(key), multi_line_value);
158+
content.remove_prefix(content.find('\n', closing_quote + 1));
159+
continue;
160+
}
161+
}
130162

131-
store_.insert_or_assign(std::string(key), value);
132-
lines = match.suffix();
163+
// Check if the value is wrapped in quotes, single quotes or backticks
164+
if ((content.front() == '\'' || content.front() == '"' ||
165+
content.front() == '`')) {
166+
auto closing_quote = content.find(content.front(), 1);
167+
168+
// Check if the closing quote is not found
169+
// Example: KEY="value
170+
if (closing_quote == std::string_view::npos) {
171+
// Check if newline exist. If it does, take the entire line as the value
172+
// Example: KEY="value\nKEY2=value2
173+
// The value pair should be `"value`
174+
auto newline = content.find('\n');
175+
if (newline != std::string_view::npos) {
176+
value = content.substr(0, newline);
177+
store_.insert_or_assign(std::string(key), value);
178+
content.remove_prefix(newline);
179+
}
180+
} else {
181+
// Example: KEY="value"
182+
value = content.substr(1, closing_quote - 1);
183+
store_.insert_or_assign(std::string(key), value);
184+
// Select the first newline after the closing quotation mark
185+
// since there could be newline characters inside the value.
186+
content.remove_prefix(content.find('\n', closing_quote + 1));
187+
}
188+
} else {
189+
// Regular key value pair.
190+
// Example: `KEY=this is value`
191+
auto newline = content.find('\n');
192+
193+
if (newline != std::string_view::npos) {
194+
value = content.substr(0, newline);
195+
auto hash_character = value.find('#');
196+
// Check if there is a comment in the line
197+
// Example: KEY=value # comment
198+
// The value pair should be `value`
199+
if (hash_character != std::string_view::npos) {
200+
value = content.substr(0, hash_character);
201+
}
202+
value = trim_spaces(value);
203+
content.remove_prefix(newline);
204+
store_.insert_or_assign(std::string(key), value);
205+
} else {
206+
break;
207+
}
208+
}
133209
}
134210
}
135211

@@ -179,13 +255,4 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
179255
}
180256
}
181257

182-
std::string_view Dotenv::trim_quotes(std::string_view str) {
183-
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
184-
if (str.size() >= 2 && quotes.count(str.front()) &&
185-
quotes.count(str.back())) {
186-
str = str.substr(1, str.size() - 2);
187-
}
188-
return str;
189-
}
190-
191258
} // namespace node

src/node_dotenv.h

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class Dotenv {
3232

3333
private:
3434
std::map<std::string, std::string> store_;
35-
std::string_view trim_quotes(std::string_view str);
3635
};
3736

3837
} // namespace node

test/fixtures/dotenv/valid.env

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
BASIC=basic
22

3+
# COMMENTS=work
4+
#BASIC=basic2
5+
#BASIC=basic3
6+
37
# previous line intentionally left blank
48
AFTER_LINE=after_line
59
EMPTY=
@@ -55,7 +59,16 @@ IS
5559
A
5660
"MULTILINE'S"
5761
STRING`
62+
export EXPORT_EXAMPLE = ignore export
63+
64+
MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
65+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
66+
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
67+
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
68+
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
69+
u4QuUoobAgMBAAE=
70+
-----END PUBLIC KEY-----"
71+
5872
MULTI_NOT_VALID_QUOTE="
5973
MULTI_NOT_VALID=THIS
6074
IS NOT MULTILINE
61-
export EXAMPLE = ignore export

test/parallel/test-dotenv.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ assert.strictEqual(process.env.COMMENTS, undefined);
5858
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==');
5959
// Retains inner quotes
6060
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}');
61-
// Respects equals signs in values
62-
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==');
63-
// Retains inner quotes
64-
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}');
6561
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}');
6662
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_BACKTICKS, '{"foo": "bar\'s"}');
6763
// Retains spaces in string
@@ -83,4 +79,13 @@ assert.strictEqual(process.env.EXPAND_NEWLINES, 'expand\nnew\nlines');
8379
assert.strictEqual(process.env.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines');
8480
assert.strictEqual(process.env.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines');
8581
// Ignore export before key
86-
assert.strictEqual(process.env.EXAMPLE, 'ignore export');
82+
assert.strictEqual(process.env.EXPORT_EXAMPLE, 'ignore export');
83+
84+
const multiPem = `-----BEGIN PUBLIC KEY-----
85+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
86+
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
87+
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
88+
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
89+
u4QuUoobAgMBAAE=
90+
-----END PUBLIC KEY-----`;
91+
assert.strictEqual(process.env.MULTI_PEM_DOUBLE_QUOTED, multiPem);

test/parallel/util-parse-env.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ const assert = require('node:assert');
66
const util = require('node:util');
77
const fs = require('node:fs');
88

9+
const multiPem = `-----BEGIN PUBLIC KEY-----
10+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
11+
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
12+
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
13+
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
14+
u4QuUoobAgMBAAE=
15+
-----END PUBLIC KEY-----`;
16+
917
{
1018
const validEnvFilePath = fixtures.path('dotenv/valid.env');
1119
const validContent = fs.readFileSync(validEnvFilePath, 'utf8');
@@ -32,7 +40,7 @@ const fs = require('node:fs');
3240
EMPTY_DOUBLE_QUOTES: '',
3341
EMPTY_SINGLE_QUOTES: '',
3442
EQUAL_SIGNS: 'equals==',
35-
EXAMPLE: 'ignore export',
43+
EXPORT_EXAMPLE: 'ignore export',
3644
EXPAND_NEWLINES: 'expand\nnew\nlines',
3745
INLINE_COMMENTS: 'inline comments',
3846
INLINE_COMMENTS_BACKTICKS: 'inline comments outside of #backticks',
@@ -53,6 +61,7 @@ const fs = require('node:fs');
5361
SINGLE_QUOTES_SPACED: ' single quotes ',
5462
SPACED_KEY: 'parsed',
5563
TRIM_SPACE_FROM_UNQUOTED: 'some spaced out string',
64+
MULTI_PEM_DOUBLE_QUOTED: multiPem,
5665
});
5766
}
5867

0 commit comments

Comments
 (0)