Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
'test/cctest/test_traced_value.cc',
'test/cctest/test_util.cc',
'test/cctest/test_dataqueue.cc',
'test/cctest/test_dotenv.cc',
],
'node_cctest_openssl_sources': [
'test/cctest/test_crypto_clienthello.cc',
Expand Down
208 changes: 100 additions & 108 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ Local<Object> Dotenv::ToObject(Environment* env) const {
return result;
}

std::string_view trim_quotes(std::string_view input) {
Comment thread
anonrig marked this conversation as resolved.
if (input.empty()) return "";
auto first = input.front();
if ((first == '\'' || first == '"' || first == '`') &&
input.back() == first) {
input = input.substr(1, input.size() - 2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more readable way:

input.remove_prefix(1);
input.remove_suffix(2);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

return input;
}

std::string_view trim_spaces(std::string_view input) {
if (input.empty()) return "";
if (input.front() == ' ') {
Expand All @@ -98,127 +109,99 @@ std::string_view trim_spaces(std::string_view input) {
return input;
}

void Dotenv::ParseContent(const std::string_view input) {
std::string lines(input);
std::string_view parse_key(std::string_view key) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment to what this function does?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

key = trim_spaces(key);
if (key.empty()) return key;

// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
if (key.starts_with("export ")) {
key.remove_prefix(7);
}
return key;
}

std::string_view content = lines;
content = trim_spaces(content);
std::string parse_value(std::string_view value) {
value = trim_spaces(value);
if (value.empty()) return "";

auto trimmed = trim_quotes(value);
if (value.front() == '\"' && value.back() == '\"') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a documentation to here?
Returning the else statement early will make this function much more readable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// Expand \n to newline in double-quote strings
size_t pos = 0;
auto expanded = std::string(trimmed);
while ((pos = expanded.find("\\n", pos)) != std::string_view::npos) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expanded is a string now. std::string::npos is the correct return value here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍Done, thanks

expanded.replace(pos, 2, "\n");
pos += 1;
}
return expanded;
} else {
return std::string(trimmed);
}
}

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

while (!content.empty()) {
// Skip empty lines and comments
if (content.front() == '\n' || content.front() == '#') {
auto newline = content.find('\n');
if (newline != std::string_view::npos) {
content.remove_prefix(newline + 1);
continue;
char quote = 0;
bool inComment = false;
std::string::size_type start = 0;
std::string::size_type end = 0;

for (std::string::size_type i = 0; i < input.size(); i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed all of the comments in this function, and it is a lot less readable right now. I can't review it with the current state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically a new function. Old one was doing a lot of back & forth searching for chars and newlines in string and it was hard for me to fit in a fix. This goes through the content of file only once and parses it char by char.

char c = input[i];
// Finished parsing a new key
if (!inComment && c == '=' && key.empty()) {
key = parse_key(input.substr(start, i - start));
while (i + 1 < input.size() && input[i + 1] == ' ') {
// Skip whitespace after key
i++;
}
}

// If there is no equal character, then ignore everything
auto equal = content.find('=');
if (equal == std::string_view::npos) {
break;
}

key = content.substr(0, equal);
content.remove_prefix(equal + 1);
key = trim_spaces(key);
content = trim_spaces(content);

if (key.empty()) {
break;
}

// Remove export prefix from key
if (key.starts_with("export ")) {
key.remove_prefix(7);
}

// SAFETY: Content is guaranteed to have at least one character
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), "");
break;
}

// Expand new line if \n it's inside double quotes
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
if (content.front() == '"') {
auto closing_quote = content.find(content.front(), 1);
if (closing_quote != std::string_view::npos) {
value = content.substr(1, closing_quote - 1);
std::string multi_line_value = std::string(value);

size_t pos = 0;
while ((pos = multi_line_value.find("\\n", pos)) !=
std::string_view::npos) {
multi_line_value.replace(pos, 2, "\n");
pos += 1;
}

store_.insert_or_assign(std::string(key), multi_line_value);
content.remove_prefix(content.find('\n', closing_quote + 1));
continue;
start = i + 1;
end = i + 1;
continue;
} else if (!inComment && (c == '"' || c == '\'' || c == '`')) {
if (start == i) {
quote = c;
} else if (quote == c) {
quote = 0;
}
}

// Check if the value is wrapped in quotes, single quotes or backticks
if ((content.front() == '\'' || content.front() == '"' ||
content.front() == '`')) {
auto closing_quote = content.find(content.front(), 1);

// Check if the closing quote is not found
// Example: KEY="value
if (closing_quote == std::string_view::npos) {
// Check if newline exist. If it does, take the entire line as the value
// Example: KEY="value\nKEY2=value2
// The value pair should be `"value`
auto newline = content.find('\n');
if (newline != std::string_view::npos) {
value = content.substr(0, newline);
store_.insert_or_assign(std::string(key), value);
content.remove_prefix(newline);
}
} else {
// Example: KEY="value"
value = content.substr(1, closing_quote - 1);
store_.insert_or_assign(std::string(key), value);
// Select the first newline after the closing quotation mark
// since there could be newline characters inside the value.
content.remove_prefix(content.find('\n', closing_quote + 1));
}
} else {
// Regular key value pair.
// Example: `KEY=this is value`
auto newline = content.find('\n');

if (newline != std::string_view::npos) {
value = content.substr(0, newline);
auto hash_character = value.find('#');
// Check if there is a comment in the line
// Example: KEY=value # comment
// The value pair should be `value`
if (hash_character != std::string_view::npos) {
value = content.substr(0, hash_character);
}
content.remove_prefix(newline);
} else {
// In case the last line is a single key/value pair
// Example: KEY=VALUE (without a newline at the EOF)
value = content.substr(0);
end++;
} else if (!inComment && c == '#' && quote == 0) {
end = i;
inComment = true;
} else if ((c == '\n' || c == '\r') && quote == 0) {
if (!key.empty()) {
auto value_str = parse_value(input.substr(start, end - start));
store_.insert_or_assign(std::string(key), value_str);
}

value = trim_spaces(value);
store_.insert_or_assign(std::string(key), value);
// Skip \n if it is a part of a \r
if (i + 1 < input.size() && input[i + 1] == '\n') {
i++;
}
start = i + 1;
end = start;
value = "";
key = "";
quote = 0;
inComment = false;
} else if (!inComment) {
end++;
}
}

if (!key.empty()) {
auto value_str = parse_value(input.substr(start, end - start));
store_.insert_or_assign(std::string(key), value_str);
}
}

Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) {
Expand Down Expand Up @@ -267,4 +250,13 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) const {
}
}

std::optional<std::string> Dotenv::GetValue(const std::string_view key) const {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can return std::string_view since the underlying store will not be disposed.

Suggested change
std::optional<std::string> Dotenv::GetValue(const std::string_view key) const {
std::optional<std::string_view> Dotenv::GetValue(const std::string_view key) const {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

auto match = store_.find(key.data());

if (match != store_.end()) {
return std::optional<std::string>{match->second};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return std::optional<std::string>{match->second};
return match->second;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
return std::nullopt;
}

} // namespace node
1 change: 1 addition & 0 deletions src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Dotenv {
~Dotenv() = default;

void ParseContent(const std::string_view content);
std::optional<std::string> GetValue(const std::string_view key) const;
ParseResult ParsePath(const std::string_view path);
void AssignNodeOptionsIfAvailable(std::string* node_options) const;
void SetEnvironment(Environment* env);
Expand Down
Loading