-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
util: fix parseEnv handling of invalid lines #56778
base: main
Are you sure you want to change the base?
Changes from 6 commits
4ca51e8
8dd566f
7a89bdb
f12d4a1
ddf3b76
65e42e3
ba372fb
f1c07c7
db57145
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -105,6 +105,11 @@ Local<Object> Dotenv::ToObject(Environment* env) const { | |||||||
return result; | ||||||||
} | ||||||||
|
||||||||
// Removes leading and trailing spaces from a string_view | ||||||||
// Returns an empty string_view if the input is empty | ||||||||
// Example: | ||||||||
// trim_spaces(" hello ") -> "hello" | ||||||||
// trim_spaces("") -> "" | ||||||||
std::string_view trim_spaces(std::string_view input) { | ||||||||
if (input.empty()) return ""; | ||||||||
if (input.front() == ' ') { | ||||||||
|
@@ -130,50 +135,81 @@ void Dotenv::ParseContent(const std::string_view input) { | |||||||
|
||||||||
while (!content.empty()) { | ||||||||
// Skip empty lines and comments | ||||||||
// Example: | ||||||||
// # This is a comment | ||||||||
// | ||||||||
// KEY=value | ||||||||
if (content.front() == '\n' || content.front() == '#') { | ||||||||
auto newline = content.find('\n'); | ||||||||
if (newline != std::string_view::npos) { | ||||||||
content.remove_prefix(newline + 1); | ||||||||
// Trim spaces after skipping comments or empty lines to handle | ||||||||
// cases where there might be trailing whitespace | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you give an example to this statement? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure but:
Suggested change
|
||||||||
content = trim_spaces(content); | ||||||||
continue; | ||||||||
} else { | ||||||||
// If no newline is found, we've reached the end of content | ||||||||
AugustinMauroy marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
break; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment here. Explain it as much as you can. Possibly with examples so it is more readable/understandable to reviewers/contributors. |
||||||||
} | ||||||||
} | ||||||||
|
||||||||
// If there is no equal character, then ignore everything | ||||||||
auto equal = content.find('='); | ||||||||
if (equal == std::string_view::npos) { | ||||||||
// Find the next equals sign or newline in a single pass | ||||||||
// This optimizes the search by avoiding multiple iterations | ||||||||
auto pos = content.find_first_of("=\n"); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the variable name to be something more readable?
Suggested change
|
||||||||
|
||||||||
// If we found nothing or found a newline before equals, the line is invalid | ||||||||
if (pos == std::string_view::npos || content[pos] == '\n') { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is diff bettween |
||||||||
if (pos != std::string_view::npos) { | ||||||||
content.remove_prefix(pos + 1); | ||||||||
content = trim_spaces(content); | ||||||||
continue; | ||||||||
} | ||||||||
break; | ||||||||
} | ||||||||
|
||||||||
key = content.substr(0, equal); | ||||||||
content.remove_prefix(equal + 1); | ||||||||
// We found an equals sign, extract the key | ||||||||
key = content.substr(0, pos); | ||||||||
content.remove_prefix(pos + 1); | ||||||||
key = trim_spaces(key); | ||||||||
content = trim_spaces(content); | ||||||||
|
||||||||
// Skip lines with empty keys after trimming spaces | ||||||||
// Examples of invalid keys that would be skipped: | ||||||||
// =value | ||||||||
// " "=value | ||||||||
if (key.empty()) { | ||||||||
auto newline = content.find('\n'); | ||||||||
if (newline != std::string_view::npos) { | ||||||||
content.remove_prefix(newline + 1); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if |
||||||||
content = trim_spaces(content); | ||||||||
continue; | ||||||||
} | ||||||||
break; | ||||||||
} | ||||||||
|
||||||||
// Remove export prefix from key | ||||||||
// Remove export prefix from key and ensure proper spacing | ||||||||
// Example: export FOO=bar -> FOO=bar | ||||||||
if (key.starts_with("export ")) { | ||||||||
key.remove_prefix(7); | ||||||||
// Trim spaces after removing export prefix to handle cases like: | ||||||||
// export FOO=bar | ||||||||
key = trim_spaces(key); | ||||||||
AugustinMauroy marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
} | ||||||||
|
||||||||
// SAFETY: Content is guaranteed to have at least one character | ||||||||
// If content is empty after the equals sign, store empty value and break | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is unneccessary. It explains the code, not how it got end up here. I recommend this article: https://swimm.io/learn/code-collaboration/comments-in-code-best-practices-and-mistakes-to-avoid Specifically:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it |
||||||||
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' | ||||||||
// Handle different types of value formats (quoted, multi-line, etc) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you revert this code change. The previous comment was better for understanding where we are at the parser. |
||||||||
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); | ||||||||
|
||||||||
// Replace \n with actual newlines in double-quoted strings | ||||||||
size_t pos = 0; | ||||||||
while ((pos = multi_line_value.find("\\n", pos)) != | ||||||||
std::string_view::npos) { | ||||||||
|
@@ -190,58 +226,57 @@ void Dotenv::ParseContent(const std::string_view input) { | |||||||
} | ||||||||
} | ||||||||
|
||||||||
// Check if the value is wrapped in quotes, single quotes or backticks | ||||||||
if ((content.front() == '\'' || content.front() == '"' || | ||||||||
content.front() == '`')) { | ||||||||
// Handle quoted values (single quotes, double quotes, 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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you revert comments for the changes that are unrelated to your code? This change for example doesn't make things better. |
||||||||
// The value pair should be `"value` | ||||||||
// No closing quote - take until newline | ||||||||
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 { | ||||||||
// No newline - take rest of content | ||||||||
value = content; | ||||||||
store_.insert_or_assign(std::string(key), value); | ||||||||
break; | ||||||||
} | ||||||||
} else { | ||||||||
// Example: KEY="value" | ||||||||
// Found closing quote - take content between quotes | ||||||||
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. | ||||||||
auto newline = content.find('\n', closing_quote + 1); | ||||||||
if (newline != std::string_view::npos) { | ||||||||
content.remove_prefix(newline); | ||||||||
} else { | ||||||||
break; | ||||||||
} | ||||||||
} | ||||||||
} else { | ||||||||
// Regular key value pair. | ||||||||
// Example: `KEY=this is value` | ||||||||
// Handle unquoted values | ||||||||
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); | ||||||||
value = value.substr(0, hash_character); | ||||||||
} | ||||||||
value = trim_spaces(value); | ||||||||
store_.insert_or_assign(std::string(key), std::string(value)); | ||||||||
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); | ||||||||
// Last line without newline | ||||||||
value = content; | ||||||||
value = trim_spaces(value); | ||||||||
store_.insert_or_assign(std::string(key), std::string(value)); | ||||||||
break; | ||||||||
} | ||||||||
|
||||||||
value = trim_spaces(value); | ||||||||
store_.insert_or_assign(std::string(key), value); | ||||||||
} | ||||||||
|
||||||||
content = trim_spaces(content); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
foo | ||
|
||
bar | ||
baz=whatever | ||
VALID_AFTER_INVALID=test | ||
multiple_invalid | ||
lines_without_equals | ||
ANOTHER_VALID=value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If content.front() is \n this will always return 0 (the first character). I wonder why we did it like this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simply replace line 144 with
if (content.front() == '\n')
and remove the content.find('\n') check I guess...