mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
Improves the performance of the dotenv parser ToObject method. Also, switch to a null prototype object to avoid potential prototype pollution issues. PR-URL: https://github.com/nodejs/node/pull/60038 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
364 lines
11 KiB
C++
364 lines
11 KiB
C++
#include "node_dotenv.h"
|
|
#include <unordered_set>
|
|
#include "env-inl.h"
|
|
#include "node_file.h"
|
|
#include "uv.h"
|
|
|
|
namespace node {
|
|
|
|
using v8::EscapableHandleScope;
|
|
using v8::JustVoid;
|
|
using v8::Local;
|
|
using v8::LocalVector;
|
|
using v8::Maybe;
|
|
using v8::MaybeLocal;
|
|
using v8::Name;
|
|
using v8::Nothing;
|
|
using v8::Object;
|
|
using v8::String;
|
|
using v8::Value;
|
|
|
|
std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs(
|
|
const std::vector<std::string>& args) {
|
|
const std::string_view optional_env_file_flag = "--env-file-if-exists";
|
|
|
|
const auto find_match = [](const std::string& arg) {
|
|
return arg == "--" || arg == "--env-file" ||
|
|
arg.starts_with("--env-file=") || arg == "--env-file-if-exists" ||
|
|
arg.starts_with("--env-file-if-exists=");
|
|
};
|
|
|
|
std::vector<Dotenv::env_file_data> env_files;
|
|
// This will be an iterator, pointing to args.end() if no matches are found
|
|
auto matched_arg = std::ranges::find_if(args, find_match);
|
|
|
|
while (matched_arg != args.end()) {
|
|
if (*matched_arg == "--") {
|
|
return env_files;
|
|
}
|
|
|
|
auto equal_char_index = matched_arg->find('=');
|
|
|
|
if (equal_char_index != std::string::npos) {
|
|
// `--env-file=path`
|
|
auto flag = matched_arg->substr(0, equal_char_index);
|
|
auto file_path = matched_arg->substr(equal_char_index + 1);
|
|
|
|
struct env_file_data env_file_data = {
|
|
file_path, flag.starts_with(optional_env_file_flag)};
|
|
env_files.push_back(env_file_data);
|
|
} else {
|
|
// `--env-file path`
|
|
auto file_path = std::next(matched_arg);
|
|
|
|
if (file_path == args.end()) {
|
|
return env_files;
|
|
}
|
|
|
|
struct env_file_data env_file_data = {
|
|
*file_path, matched_arg->starts_with(optional_env_file_flag)};
|
|
env_files.push_back(env_file_data);
|
|
}
|
|
|
|
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
|
|
}
|
|
|
|
return env_files;
|
|
}
|
|
|
|
Maybe<void> Dotenv::SetEnvironment(node::Environment* env) {
|
|
auto context = env->context();
|
|
auto env_vars = env->env_vars();
|
|
|
|
for (const auto& entry : store_) {
|
|
auto existing = env_vars->Get(entry.first.data());
|
|
if (!existing.has_value()) {
|
|
Local<Value> name;
|
|
Local<Value> val;
|
|
if (!ToV8Value(context, entry.first).ToLocal(&name) ||
|
|
!ToV8Value(context, entry.second).ToLocal(&val)) {
|
|
return Nothing<void>();
|
|
}
|
|
env_vars->Set(env->isolate(), name.As<String>(), val.As<String>());
|
|
}
|
|
}
|
|
|
|
return JustVoid();
|
|
}
|
|
|
|
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());
|
|
auto context = env->context();
|
|
|
|
Local<Value> tmp;
|
|
|
|
int n = 0;
|
|
for (const auto& entry : store_) {
|
|
if (!ToV8Value(context, entry.first).ToLocal(&tmp)) {
|
|
return MaybeLocal<Object>();
|
|
}
|
|
names[n] = tmp.As<Name>();
|
|
if (!ToV8Value(context, entry.second).ToLocal(&tmp)) {
|
|
return MaybeLocal<Object>();
|
|
}
|
|
values[n++] = tmp;
|
|
}
|
|
Local<Object> result = Object::New(env->isolate(),
|
|
Null(env->isolate()),
|
|
names.data(),
|
|
values.data(),
|
|
values.size());
|
|
return scope.Escape(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 "";
|
|
|
|
auto pos_start = input.find_first_not_of(" \t\n");
|
|
if (pos_start == std::string_view::npos) {
|
|
return "";
|
|
}
|
|
|
|
auto pos_end = input.find_last_not_of(" \t\n");
|
|
if (pos_end == std::string_view::npos) {
|
|
return input.substr(pos_start);
|
|
}
|
|
|
|
return input.substr(pos_start, pos_end - pos_start + 1);
|
|
}
|
|
|
|
void Dotenv::ParseContent(const std::string_view input) {
|
|
std::string lines(input);
|
|
|
|
// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
|
|
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
|
|
|
|
std::string_view content = lines;
|
|
content = trim_spaces(content);
|
|
|
|
std::string_view key;
|
|
std::string_view value;
|
|
|
|
while (!content.empty()) {
|
|
// Skip empty lines and comments
|
|
if (content.front() == '\n' || content.front() == '#') {
|
|
// Check if the first character of the content is a newline or a hash
|
|
auto newline = content.find('\n');
|
|
if (newline != std::string_view::npos) {
|
|
// Remove everything up to and including the newline character
|
|
content.remove_prefix(newline + 1);
|
|
} else {
|
|
// If no newline is found, clear the content
|
|
content = {};
|
|
}
|
|
|
|
// Skip the remaining code in the loop and continue with the next
|
|
// iteration.
|
|
continue;
|
|
}
|
|
|
|
// Find the next equals sign or newline in a single pass.
|
|
// This optimizes the search by avoiding multiple iterations.
|
|
auto equal_or_newline = content.find_first_of("=\n");
|
|
|
|
// If we found nothing or found a newline before equals, the line is invalid
|
|
if (equal_or_newline == std::string_view::npos ||
|
|
content.at(equal_or_newline) == '\n') {
|
|
if (equal_or_newline != std::string_view::npos) {
|
|
content.remove_prefix(equal_or_newline + 1);
|
|
content = trim_spaces(content);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// We found an equals sign, extract the key
|
|
key = content.substr(0, equal_or_newline);
|
|
content.remove_prefix(equal_or_newline + 1);
|
|
key = trim_spaces(key);
|
|
|
|
// 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), "");
|
|
continue;
|
|
}
|
|
|
|
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()) continue;
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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) {
|
|
multi_line_value.replace(pos, 2, "\n");
|
|
pos += 1;
|
|
}
|
|
|
|
store_.insert_or_assign(std::string(key), multi_line_value);
|
|
auto newline = content.find('\n', closing_quote + 1);
|
|
if (newline != std::string_view::npos) {
|
|
content.remove_prefix(newline + 1);
|
|
} else {
|
|
// In case the last line is a single key/value pair
|
|
// Example: KEY=VALUE (without a newline at the EOF
|
|
content = {};
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// 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 + 1);
|
|
} else {
|
|
// No newline - take rest of content
|
|
value = content;
|
|
store_.insert_or_assign(std::string(key), 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);
|
|
auto newline = content.find('\n', closing_quote + 1);
|
|
if (newline != std::string_view::npos) {
|
|
// Use +1 to discard the '\n' itself => next line
|
|
content.remove_prefix(newline + 1);
|
|
} else {
|
|
content = {};
|
|
}
|
|
// No valid data here, skip to next line
|
|
continue;
|
|
}
|
|
} 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 = value.substr(0, hash_character);
|
|
}
|
|
value = trim_spaces(value);
|
|
store_.insert_or_assign(std::string(key), std::string(value));
|
|
content.remove_prefix(newline + 1);
|
|
} else {
|
|
// Last line without newline
|
|
value = content;
|
|
auto hash_char = value.find('#');
|
|
if (hash_char != std::string_view::npos) {
|
|
value = content.substr(0, hash_char);
|
|
}
|
|
store_.insert_or_assign(std::string(key), trim_spaces(value));
|
|
content = {};
|
|
}
|
|
}
|
|
|
|
content = trim_spaces(content);
|
|
}
|
|
}
|
|
|
|
Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) {
|
|
uv_fs_t req;
|
|
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
|
|
|
|
uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
|
|
if (req.result < 0) {
|
|
// req will be cleaned up by scope leave.
|
|
return ParseResult::FileError;
|
|
}
|
|
uv_fs_req_cleanup(&req);
|
|
|
|
auto defer_close = OnScopeLeave([file]() {
|
|
uv_fs_t close_req;
|
|
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
|
|
uv_fs_req_cleanup(&close_req);
|
|
});
|
|
|
|
std::string result{};
|
|
char buffer[8192];
|
|
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));
|
|
|
|
while (true) {
|
|
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
|
|
if (req.result < 0) {
|
|
// req will be cleaned up by scope leave.
|
|
return ParseResult::InvalidContent;
|
|
}
|
|
uv_fs_req_cleanup(&req);
|
|
if (r <= 0) {
|
|
break;
|
|
}
|
|
result.append(buf.base, r);
|
|
}
|
|
|
|
ParseContent(result);
|
|
return ParseResult::Valid;
|
|
}
|
|
|
|
void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) const {
|
|
auto match = store_.find("NODE_OPTIONS");
|
|
|
|
if (match != store_.end()) {
|
|
*node_options = match->second;
|
|
}
|
|
}
|
|
|
|
} // namespace node
|