Skip to content

Commit e4b98d9

Browse files
committed
dotenv: make parsing compatible with motdotla/dotenv package
Fixes: nodejs#54134
1 parent 67f7137 commit e4b98d9

File tree

5 files changed

+367
-110
lines changed

5 files changed

+367
-110
lines changed

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@
419419
'test/cctest/test_traced_value.cc',
420420
'test/cctest/test_util.cc',
421421
'test/cctest/test_dataqueue.cc',
422+
'test/cctest/test_dotenv.cc',
422423
],
423424
'node_cctest_openssl_sources': [
424425
'test/cctest/test_crypto_clienthello.cc',

src/node_dotenv.cc

+100-108
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ Local<Object> Dotenv::ToObject(Environment* env) const {
8787
return result;
8888
}
8989

90+
std::string_view trim_quotes(std::string_view input) {
91+
if (input.empty()) return "";
92+
auto first = input.front();
93+
if ((first == '\'' || first == '"' || first == '`') &&
94+
input.back() == first) {
95+
input = input.substr(1, input.size() - 2);
96+
}
97+
98+
return input;
99+
}
100+
90101
std::string_view trim_spaces(std::string_view input) {
91102
if (input.empty()) return "";
92103
if (input.front() == ' ') {
@@ -98,127 +109,99 @@ std::string_view trim_spaces(std::string_view input) {
98109
return input;
99110
}
100111

101-
void Dotenv::ParseContent(const std::string_view input) {
102-
std::string lines(input);
112+
std::string_view parse_key(std::string_view key) {
113+
key = trim_spaces(key);
114+
if (key.empty()) return key;
103115

104-
// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
105-
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
116+
if (key.starts_with("export ")) {
117+
key.remove_prefix(7);
118+
}
119+
return key;
120+
}
106121

107-
std::string_view content = lines;
108-
content = trim_spaces(content);
122+
std::string parse_value(std::string_view value) {
123+
value = trim_spaces(value);
124+
if (value.empty()) return "";
125+
126+
auto trimmed = trim_quotes(value);
127+
if (value.front() == '\"' && value.back() == '\"') {
128+
// Expand \n to newline in double-quote strings
129+
size_t pos = 0;
130+
auto expanded = std::string(trimmed);
131+
while ((pos = expanded.find("\\n", pos)) != std::string_view::npos) {
132+
expanded.replace(pos, 2, "\n");
133+
pos += 1;
134+
}
135+
return expanded;
136+
} else {
137+
return std::string(trimmed);
138+
}
139+
}
109140

141+
/**
142+
* Parse the content of a .env file.
143+
* We want to be compatible with motdotla/dotenv js package,
144+
* so some edge-cases might be handled differently than you expect.
145+
*
146+
* Check the test cases in test/cctest/test_dotenv.cc for more details.
147+
*/
148+
void Dotenv::ParseContent(const std::string_view input) {
110149
std::string_view key;
111150
std::string_view value;
112151

113-
while (!content.empty()) {
114-
// Skip empty lines and comments
115-
if (content.front() == '\n' || content.front() == '#') {
116-
auto newline = content.find('\n');
117-
if (newline != std::string_view::npos) {
118-
content.remove_prefix(newline + 1);
119-
continue;
152+
char quote = 0;
153+
bool inComment = false;
154+
std::string::size_type start = 0;
155+
std::string::size_type end = 0;
156+
157+
for (std::string::size_type i = 0; i < input.size(); i++) {
158+
char c = input[i];
159+
// Finished parsing a new key
160+
if (!inComment && c == '=' && key.empty()) {
161+
key = parse_key(input.substr(start, i - start));
162+
while (i + 1 < input.size() && input[i + 1] == ' ') {
163+
// Skip whitespace after key
164+
i++;
120165
}
121-
}
122-
123-
// If there is no equal character, then ignore everything
124-
auto equal = content.find('=');
125-
if (equal == std::string_view::npos) {
126-
break;
127-
}
128-
129-
key = content.substr(0, equal);
130-
content.remove_prefix(equal + 1);
131-
key = trim_spaces(key);
132-
content = trim_spaces(content);
133-
134-
if (key.empty()) {
135-
break;
136-
}
137-
138-
// Remove export prefix from key
139-
if (key.starts_with("export ")) {
140-
key.remove_prefix(7);
141-
}
142-
143-
// SAFETY: Content is guaranteed to have at least one character
144-
if (content.empty()) {
145-
// In case the last line is a single key without value
146-
// Example: KEY= (without a newline at the EOF)
147-
store_.insert_or_assign(std::string(key), "");
148-
break;
149-
}
150-
151-
// Expand new line if \n it's inside double quotes
152-
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
153-
if (content.front() == '"') {
154-
auto closing_quote = content.find(content.front(), 1);
155-
if (closing_quote != std::string_view::npos) {
156-
value = content.substr(1, closing_quote - 1);
157-
std::string multi_line_value = std::string(value);
158-
159-
size_t pos = 0;
160-
while ((pos = multi_line_value.find("\\n", pos)) !=
161-
std::string_view::npos) {
162-
multi_line_value.replace(pos, 2, "\n");
163-
pos += 1;
164-
}
165-
166-
store_.insert_or_assign(std::string(key), multi_line_value);
167-
content.remove_prefix(content.find('\n', closing_quote + 1));
168-
continue;
166+
start = i + 1;
167+
end = i + 1;
168+
continue;
169+
} else if (!inComment && (c == '"' || c == '\'' || c == '`')) {
170+
if (start == i) {
171+
quote = c;
172+
} else if (quote == c) {
173+
quote = 0;
169174
}
170-
}
171175

172-
// Check if the value is wrapped in quotes, single quotes or backticks
173-
if ((content.front() == '\'' || content.front() == '"' ||
174-
content.front() == '`')) {
175-
auto closing_quote = content.find(content.front(), 1);
176-
177-
// Check if the closing quote is not found
178-
// Example: KEY="value
179-
if (closing_quote == std::string_view::npos) {
180-
// Check if newline exist. If it does, take the entire line as the value
181-
// Example: KEY="value\nKEY2=value2
182-
// The value pair should be `"value`
183-
auto newline = content.find('\n');
184-
if (newline != std::string_view::npos) {
185-
value = content.substr(0, newline);
186-
store_.insert_or_assign(std::string(key), value);
187-
content.remove_prefix(newline);
188-
}
189-
} else {
190-
// Example: KEY="value"
191-
value = content.substr(1, closing_quote - 1);
192-
store_.insert_or_assign(std::string(key), value);
193-
// Select the first newline after the closing quotation mark
194-
// since there could be newline characters inside the value.
195-
content.remove_prefix(content.find('\n', closing_quote + 1));
196-
}
197-
} else {
198-
// Regular key value pair.
199-
// Example: `KEY=this is value`
200-
auto newline = content.find('\n');
201-
202-
if (newline != std::string_view::npos) {
203-
value = content.substr(0, newline);
204-
auto hash_character = value.find('#');
205-
// Check if there is a comment in the line
206-
// Example: KEY=value # comment
207-
// The value pair should be `value`
208-
if (hash_character != std::string_view::npos) {
209-
value = content.substr(0, hash_character);
210-
}
211-
content.remove_prefix(newline);
212-
} else {
213-
// In case the last line is a single key/value pair
214-
// Example: KEY=VALUE (without a newline at the EOF)
215-
value = content.substr(0);
176+
end++;
177+
} else if (!inComment && c == '#' && quote == 0) {
178+
end = i;
179+
inComment = true;
180+
} else if ((c == '\n' || c == '\r') && quote == 0) {
181+
if (!key.empty()) {
182+
auto value_str = parse_value(input.substr(start, end - start));
183+
store_.insert_or_assign(std::string(key), value_str);
216184
}
217185

218-
value = trim_spaces(value);
219-
store_.insert_or_assign(std::string(key), value);
186+
// Skip \n if it is a part of a \r
187+
if (i + 1 < input.size() && input[i + 1] == '\n') {
188+
i++;
189+
}
190+
start = i + 1;
191+
end = start;
192+
value = "";
193+
key = "";
194+
quote = 0;
195+
inComment = false;
196+
} else if (!inComment) {
197+
end++;
220198
}
221199
}
200+
201+
if (!key.empty()) {
202+
auto value_str = parse_value(input.substr(start, end - start));
203+
store_.insert_or_assign(std::string(key), value_str);
204+
}
222205
}
223206

224207
Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) {
@@ -267,4 +250,13 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) const {
267250
}
268251
}
269252

253+
std::optional<std::string> Dotenv::GetValue(const std::string_view key) const {
254+
auto match = store_.find(key.data());
255+
256+
if (match != store_.end()) {
257+
return std::optional<std::string>{match->second};
258+
}
259+
return std::nullopt;
260+
}
261+
270262
} // namespace node

src/node_dotenv.h

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Dotenv {
2222
~Dotenv() = default;
2323

2424
void ParseContent(const std::string_view content);
25+
std::optional<std::string> GetValue(const std::string_view key) const;
2526
ParseResult ParsePath(const std::string_view path);
2627
void AssignNodeOptionsIfAvailable(std::string* node_options) const;
2728
void SetEnvironment(Environment* env);

0 commit comments

Comments
 (0)