current commit
260926.27 · src,lib: add --allow-env permission
#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();
}

std::vector<std::string> Dotenv::GetKeys() const {
  std::vector<std::string> keys;
  keys.reserve(store_.size());
  for (const auto& entry : store_) {
    keys.push_back(entry.first);
  }
  return keys;
}

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