Skip to content

Commit 83f47a6

Browse files
util: fix parseEnv handling of invalid lines
1 parent 17c65d1 commit 83f47a6

2 files changed

Lines changed: 85 additions & 37 deletions

File tree

src/node_dotenv.cc

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@ MaybeLocal<Object> Dotenv::ToObject(Environment* env) const {
102102
return scope.Escape(result);
103103
}
104104

105-
// Removes space characters (spaces, tabs and newlines) from
106-
// the start and end of a given input string
105+
// Removes leading and trailing spaces from a string_view.
106+
// Returns an empty string_view if the input is empty.
107+
// Example:
108+
// trim_spaces(" hello ") -> "hello"
109+
// trim_spaces("") -> ""
107110
std::string_view trim_spaces(std::string_view input) {
108111
if (input.empty()) return "";
109112

@@ -134,48 +137,69 @@ void Dotenv::ParseContent(const std::string_view input) {
134137

135138
while (!content.empty()) {
136139
// Skip empty lines and comments
140+
// Example:
141+
// # This is a comment
137142
if (content.front() == '\n' || content.front() == '#') {
138-
auto newline = content.find('\n');
139-
if (newline != std::string_view::npos) {
140-
content.remove_prefix(newline + 1);
141-
continue;
143+
// Check if the first character of the content is a newline or a hash
144+
if (content.front() == '\n') {
145+
// If the first character is a newline, remove it
146+
content.remove_prefix(1);
147+
} else {
148+
// If the first character is a hash, find the next newline character
149+
auto newline = content.find('\n');
150+
if (newline != std::string_view::npos) {
151+
// If a newline is found, remove the comment line including the
152+
// newline character.
153+
content.remove_prefix(newline + 1);
154+
}
142155
}
156+
157+
// Skip the remaining code in the loop and continue with the next
158+
// iteration.
159+
continue;
143160
}
144161

145-
// If there is no equal character, then ignore everything
146-
auto equal = content.find('=');
147-
if (equal == std::string_view::npos) {
148-
auto newline = content.find('\n');
149-
if (newline != std::string_view::npos) {
150-
// If we used `newline` only,
151-
// the '\n' might remain and cause an empty-line parse
152-
content.remove_prefix(newline + 1);
153-
} else {
154-
content = {};
162+
// Find the next equals sign or newline in a single pass.
163+
// This optimizes the search by avoiding multiple iterations.
164+
auto equal_or_newline = content.find_first_of("=\n");
165+
166+
// If we found nothing or found a newline before equals, the line is invalid
167+
if (equal_or_newline == std::string_view::npos ||
168+
content.at(equal_or_newline) == '\n') {
169+
if (equal_or_newline != std::string_view::npos) {
170+
content.remove_prefix(equal_or_newline + 1);
171+
content = trim_spaces(content);
172+
continue;
155173
}
156-
// No valid data here, skip to next line
157-
continue;
174+
break;
158175
}
159176

160-
key = content.substr(0, equal);
161-
content.remove_prefix(equal + 1);
177+
// We found an equals sign, extract the key
178+
key = content.substr(0, equal_or_newline);
179+
content.remove_prefix(equal_or_newline + 1);
162180
key = trim_spaces(key);
163181

164-
// If the value is not present (e.g. KEY=) set is to an empty string
182+
// If the value is not present (e.g. KEY=) set it to an empty string
165183
if (content.empty() || content.front() == '\n') {
166184
store_.insert_or_assign(std::string(key), "");
167185
continue;
168186
}
169187

170188
content = trim_spaces(content);
171189

172-
if (key.empty()) {
173-
break;
174-
}
190+
// Skip lines with empty keys after trimming spaces.
191+
// Examples of invalid keys that would be skipped:
192+
// =value
193+
// " "=value
194+
if (key.empty()) continue;
175195

176-
// Remove export prefix from key
196+
// Remove export prefix from key and ensure proper spacing.
197+
// Example: export FOO=bar -> FOO=bar
177198
if (key.starts_with("export ")) {
178199
key.remove_prefix(7);
200+
// Trim spaces after removing export prefix to handle cases like:
201+
// export FOO=bar
202+
key = trim_spaces(key);
179203
}
180204

181205
// SAFETY: Content is guaranteed to have at least one character
@@ -194,6 +218,7 @@ void Dotenv::ParseContent(const std::string_view input) {
194218
value = content.substr(1, closing_quote - 1);
195219
std::string multi_line_value = std::string(value);
196220

221+
// Replace \n with actual newlines in double-quoted strings
197222
size_t pos = 0;
198223
while ((pos = multi_line_value.find("\\n", pos)) !=
199224
std::string_view::npos) {
@@ -212,9 +237,9 @@ void Dotenv::ParseContent(const std::string_view input) {
212237
}
213238
}
214239

215-
// Check if the value is wrapped in quotes, single quotes or backticks
216-
if ((content.front() == '\'' || content.front() == '"' ||
217-
content.front() == '`')) {
240+
// Handle quoted values (single quotes, double quotes, backticks)
241+
if (content.front() == '\'' || content.front() == '"' ||
242+
content.front() == '`') {
218243
auto closing_quote = content.find(content.front(), 1);
219244

220245
// Check if the closing quote is not found
@@ -228,13 +253,16 @@ void Dotenv::ParseContent(const std::string_view input) {
228253
value = content.substr(0, newline);
229254
store_.insert_or_assign(std::string(key), value);
230255
content.remove_prefix(newline + 1);
256+
} else {
257+
// No newline - take rest of content
258+
value = content;
259+
store_.insert_or_assign(std::string(key), value);
260+
break;
231261
}
232262
} else {
233-
// Example: KEY="value"
263+
// Found closing quote - take content between quotes
234264
value = content.substr(1, closing_quote - 1);
235265
store_.insert_or_assign(std::string(key), value);
236-
// Select the first newline after the closing quotation mark
237-
// since there could be newline characters inside the value.
238266
auto newline = content.find('\n', closing_quote + 1);
239267
if (newline != std::string_view::npos) {
240268
// Use +1 to discard the '\n' itself => next line
@@ -257,13 +285,13 @@ void Dotenv::ParseContent(const std::string_view input) {
257285
// Example: KEY=value # comment
258286
// The value pair should be `value`
259287
if (hash_character != std::string_view::npos) {
260-
value = content.substr(0, hash_character);
288+
value = value.substr(0, hash_character);
261289
}
262-
store_.insert_or_assign(std::string(key), trim_spaces(value));
290+
value = trim_spaces(value);
291+
store_.insert_or_assign(std::string(key), std::string(value));
263292
content.remove_prefix(newline + 1);
264293
} else {
265-
// In case the last line is a single key/value pair
266-
// Example: KEY=VALUE (without a newline at the EOF)
294+
// Last line without newline
267295
value = content;
268296
auto hash_char = value.find('#');
269297
if (hash_char != std::string_view::npos) {
@@ -272,9 +300,9 @@ void Dotenv::ParseContent(const std::string_view input) {
272300
store_.insert_or_assign(std::string(key), trim_spaces(value));
273301
content = {};
274302
}
275-
276-
store_.insert_or_assign(std::string(key), trim_spaces(value));
277303
}
304+
305+
content = trim_spaces(content);
278306
}
279307
}
280308

test/parallel/test-dotenv-edge-cases.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const common = require('../common');
44
const assert = require('node:assert');
55
const path = require('node:path');
66
const { describe, it } = require('node:test');
7+
const { parseEnv } = require('node:util');
78
const fixtures = require('../common/fixtures');
89

910
const validEnvFilePath = '../fixtures/dotenv/valid.env';
@@ -200,4 +201,23 @@ describe('.env supports edge cases', () => {
200201
assert.strictEqual(child.code, 9);
201202
assert.match(child.stderr, /bad option: --env-file-ABCD/);
202203
});
204+
205+
it('should handle invalid multiline syntax', () => {
206+
const result = parseEnv([
207+
'foo',
208+
'',
209+
'bar',
210+
'baz=whatever',
211+
'VALID_AFTER_INVALID=test',
212+
'multiple_invalid',
213+
'lines_without_equals',
214+
'ANOTHER_VALID=value',
215+
].join('\n'));
216+
217+
assert.deepStrictEqual(result, {
218+
baz: 'whatever',
219+
VALID_AFTER_INVALID: 'test',
220+
ANOTHER_VALID: 'value',
221+
});
222+
});
203223
});

0 commit comments

Comments
 (0)