| #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 |