Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,24 @@ Maybe<void> Dotenv::SetEnvironment(node::Environment* env) {
MaybeLocal<Object> Dotenv::ToObject(Environment* env) const {
EscapableHandleScope scope(env->isolate());

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

Local<Value> tmp;

int n = 0;
for (const auto& entry : store_) {
if (!ToV8Value(context, entry.first).ToLocal(&tmp)) {
for (const auto& key : keys_order_) {
auto entry = store_.find(key);
if (entry == store_.end()) {
continue;
}

if (!ToV8Value(context, entry->first).ToLocal(&tmp)) {
return MaybeLocal<Object>();
}
names[n] = tmp.As<Name>();
if (!ToV8Value(context, entry.second).ToLocal(&tmp)) {
if (!ToV8Value(context, entry->second).ToLocal(&tmp)) {
return MaybeLocal<Object>();
}
values[n++] = tmp;
Expand Down Expand Up @@ -138,6 +143,17 @@ std::string_view trim_spaces(std::string_view input) {
void Dotenv::ParseContent(const std::string_view input) {
std::string lines(input);

const auto set_entry = [this](std::string_view entry_key,
std::string entry_value) {
auto [it, inserted] =
store_.insert_or_assign(std::string(entry_key),
std::move(entry_value));

if (inserted) {
keys_order_.push_back(it->first);
}
};

// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());

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

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

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

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

store_.insert_or_assign(std::string(key), multi_line_value);
set_entry(key, std::move(multi_line_value));
auto newline = content.find('\n', closing_quote + 1);
if (newline != std::string_view::npos) {
content.remove_prefix(newline + 1);
Expand All @@ -259,18 +275,18 @@ void Dotenv::ParseContent(const std::string_view input) {
auto newline = content.find('\n');
if (newline != std::string_view::npos) {
value = content.substr(0, newline);
store_.insert_or_assign(std::string(key), value);
set_entry(key, std::string(value));
content.remove_prefix(newline + 1);
} else {
// No newline - take rest of content
value = content;
store_.insert_or_assign(std::string(key), value);
set_entry(key, std::string(value));
break;
}
} else {
// Found closing quote - take content between quotes
value = content.substr(1, closing_quote - 1);
store_.insert_or_assign(std::string(key), value);
set_entry(key, std::string(value));
auto newline = content.find('\n', closing_quote + 1);
if (newline != std::string_view::npos) {
// Use +1 to discard the '\n' itself => next line
Expand All @@ -296,7 +312,7 @@ void Dotenv::ParseContent(const std::string_view input) {
value = value.substr(0, hash_character);
}
value = trim_spaces(value);
store_.insert_or_assign(std::string(key), std::string(value));
set_entry(key, std::string(value));
content.remove_prefix(newline + 1);
} else {
// Last line without newline
Expand All @@ -305,7 +321,7 @@ void Dotenv::ParseContent(const std::string_view input) {
if (hash_char != std::string_view::npos) {
value = content.substr(0, hash_char);
}
store_.insert_or_assign(std::string(key), trim_spaces(value));
set_entry(key, std::string(trim_spaces(value)));
content = {};
}
}
Expand Down
1 change: 1 addition & 0 deletions src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Dotenv {

private:
std::map<std::string, std::string> store_;
std::vector<std::string> keys_order_;
};

} // namespace node
Expand Down
24 changes: 24 additions & 0 deletions test/parallel/test-util-parse-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,27 @@ assert.throws(() => {
}, {
code: 'ERR_INVALID_ARG_TYPE',
});

// Test parse envs keep the order of keys as they appear in the input string
{
const input = `
PASSWORD="s1mpl3"
DB_PASS=$PASSWORD
`.trim();

const parsed = util.parseEnv(input);
const keys = Object.keys(parsed);

assert.deepStrictEqual(keys, ['PASSWORD', 'DB_PASS']);
}

// Test that when a key appears multiple times, the last value is used,
// but the order of keys is determined by the first occurrence
{
const input = 'A=1\nB=2\nA=3';
const parsed = util.parseEnv(input);
const keys = Object.keys(parsed);

assert.deepStrictEqual(keys, ['A', 'B']);
assert.deepStrictEqual(parsed, { A: '3', B: '2', __proto__: null });
}
Loading