Skip to content

Commit 29f2478

Browse files
src: preserve dotenv key insertion order
Keep parseEnv output keys in the same order they first appear in the input Fixes: #62736 Signed-off-by: Jonathan Lopes <[email protected]>
1 parent bee1087 commit 29f2478

3 files changed

Lines changed: 54 additions & 13 deletions

File tree

src/node_dotenv.cc

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,24 @@ Maybe<void> Dotenv::SetEnvironment(node::Environment* env) {
8989
MaybeLocal<Object> Dotenv::ToObject(Environment* env) const {
9090
EscapableHandleScope scope(env->isolate());
9191

92-
LocalVector<Name> names(env->isolate(), store_.size());
93-
LocalVector<Value> values(env->isolate(), store_.size());
92+
LocalVector<Name> names(env->isolate(), keys_order_.size());
93+
LocalVector<Value> values(env->isolate(), keys_order_.size());
9494
auto context = env->context();
9595

9696
Local<Value> tmp;
9797

9898
int n = 0;
99-
for (const auto& entry : store_) {
100-
if (!ToV8Value(context, entry.first).ToLocal(&tmp)) {
99+
for (const auto& key : keys_order_) {
100+
auto entry = store_.find(key);
101+
if (entry == store_.end()) {
102+
continue;
103+
}
104+
105+
if (!ToV8Value(context, entry->first).ToLocal(&tmp)) {
101106
return MaybeLocal<Object>();
102107
}
103108
names[n] = tmp.As<Name>();
104-
if (!ToV8Value(context, entry.second).ToLocal(&tmp)) {
109+
if (!ToV8Value(context, entry->second).ToLocal(&tmp)) {
105110
return MaybeLocal<Object>();
106111
}
107112
values[n++] = tmp;
@@ -138,6 +143,17 @@ std::string_view trim_spaces(std::string_view input) {
138143
void Dotenv::ParseContent(const std::string_view input) {
139144
std::string lines(input);
140145

146+
const auto set_entry = [this](std::string_view entry_key,
147+
std::string entry_value) {
148+
auto [it, inserted] =
149+
store_.insert_or_assign(std::string(entry_key),
150+
std::move(entry_value));
151+
152+
if (inserted) {
153+
keys_order_.push_back(it->first);
154+
}
155+
};
156+
141157
// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
142158
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
143159

@@ -187,7 +203,7 @@ void Dotenv::ParseContent(const std::string_view input) {
187203

188204
// If the value is not present (e.g. KEY=) set it to an empty string
189205
if (content.empty() || content.front() == '\n') {
190-
store_.insert_or_assign(std::string(key), "");
206+
set_entry(key, "");
191207
continue;
192208
}
193209

@@ -212,7 +228,7 @@ void Dotenv::ParseContent(const std::string_view input) {
212228
if (content.empty()) {
213229
// In case the last line is a single key without value
214230
// Example: KEY= (without a newline at the EOF)
215-
store_.insert_or_assign(std::string(key), "");
231+
set_entry(key, "");
216232
break;
217233
}
218234

@@ -232,7 +248,7 @@ void Dotenv::ParseContent(const std::string_view input) {
232248
pos += 1;
233249
}
234250

235-
store_.insert_or_assign(std::string(key), multi_line_value);
251+
set_entry(key, std::move(multi_line_value));
236252
auto newline = content.find('\n', closing_quote + 1);
237253
if (newline != std::string_view::npos) {
238254
content.remove_prefix(newline + 1);
@@ -259,18 +275,18 @@ void Dotenv::ParseContent(const std::string_view input) {
259275
auto newline = content.find('\n');
260276
if (newline != std::string_view::npos) {
261277
value = content.substr(0, newline);
262-
store_.insert_or_assign(std::string(key), value);
278+
set_entry(key, std::string(value));
263279
content.remove_prefix(newline + 1);
264280
} else {
265281
// No newline - take rest of content
266282
value = content;
267-
store_.insert_or_assign(std::string(key), value);
283+
set_entry(key, std::string(value));
268284
break;
269285
}
270286
} else {
271287
// Found closing quote - take content between quotes
272288
value = content.substr(1, closing_quote - 1);
273-
store_.insert_or_assign(std::string(key), value);
289+
set_entry(key, std::string(value));
274290
auto newline = content.find('\n', closing_quote + 1);
275291
if (newline != std::string_view::npos) {
276292
// Use +1 to discard the '\n' itself => next line
@@ -296,7 +312,7 @@ void Dotenv::ParseContent(const std::string_view input) {
296312
value = value.substr(0, hash_character);
297313
}
298314
value = trim_spaces(value);
299-
store_.insert_or_assign(std::string(key), std::string(value));
315+
set_entry(key, std::string(value));
300316
content.remove_prefix(newline + 1);
301317
} else {
302318
// Last line without newline
@@ -305,7 +321,7 @@ void Dotenv::ParseContent(const std::string_view input) {
305321
if (hash_char != std::string_view::npos) {
306322
value = content.substr(0, hash_char);
307323
}
308-
store_.insert_or_assign(std::string(key), trim_spaces(value));
324+
set_entry(key, std::string(trim_spaces(value)));
309325
content = {};
310326
}
311327
}

src/node_dotenv.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Dotenv {
3636

3737
private:
3838
std::map<std::string, std::string> store_;
39+
std::vector<std::string> keys_order_;
3940
};
4041

4142
} // namespace node

test/parallel/test-util-parse-env.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,27 @@ assert.throws(() => {
7474
}, {
7575
code: 'ERR_INVALID_ARG_TYPE',
7676
});
77+
78+
// Test parse envs keep the order of keys as they appear in the input string
79+
{
80+
const input = `
81+
PASSWORD="s1mpl3"
82+
DB_PASS=$PASSWORD
83+
`.trim();
84+
85+
const parsed = util.parseEnv(input);
86+
const keys = Object.keys(parsed);
87+
88+
assert.deepStrictEqual(keys, ['PASSWORD', 'DB_PASS']);
89+
}
90+
91+
// Test that when a key appears multiple times, the last value is used,
92+
// but the order of keys is determined by the first occurrence
93+
{
94+
const input = 'A=1\nB=2\nA=3';
95+
const parsed = util.parseEnv(input);
96+
const keys = Object.keys(parsed);
97+
98+
assert.deepStrictEqual(keys, ['A', 'B']);
99+
assert.deepStrictEqual(parsed, { A: '3', B: '2', __proto__: null });
100+
}

0 commit comments

Comments
 (0)