use core::ffi::c_char;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
use bun_alloc::AllocError;
use bun_collections::{ArrayHashMapExt, GetOrPutResult, StringArrayHashMap};
use bun_core::{self, Output};
use bun_core::{ZStr, strings};
use bun_paths::{self, MAX_PATH_BYTES, PathBuffer};
use bun_sys;
use bun_url::URL;
use bun_which::which;
use bun_core::analytics;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum DotEnvFileSuffix {
Development,
Production,
Test,
}
pub type Kind = DotEnvFileSuffix;
pub type Mode = DotEnvFileSuffix;
pub trait DirEntryProbe {
fn has_comptime_query(&self, query_lower: &'static [u8]) -> bool;
}
#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
#[allow(non_camel_case_types)]
pub enum DotEnvBehavior {
#[default]
_none = 0,
disable = 1,
prefix = 2,
load_all = 3,
load_all_without_inlining = 4,
}
#[allow(non_upper_case_globals)]
impl DotEnvBehavior {
pub const None: Self = Self::_none;
pub const Disable: Self = Self::disable;
pub const Prefix: Self = Self::prefix;
pub const LoadAll: Self = Self::load_all;
pub const LoadAllWithoutInlining: Self = Self::load_all_without_inlining;
pub fn parse_str(s: &[u8]) -> Result<(Self, Option<&[u8]>), ()> {
if s == b"inline" {
Ok((Self::load_all, None))
} else if s == b"disable" {
Ok((Self::disable, None))
} else if let Some(asterisk) = s.iter().position(|&b| b == b'*') {
if asterisk > 0 {
Ok((Self::prefix, Some(&s[..asterisk])))
} else {
Ok((Self::load_all, None))
}
} else {
Err(())
}
}
}
#[derive(Clone, Default)]
pub struct S3Credentials {
pub access_key_id: Box<[u8]>,
pub secret_access_key: Box<[u8]>,
pub region: Box<[u8]>,
pub endpoint: Box<[u8]>,
pub bucket: Box<[u8]>,
pub session_token: Box<[u8]>,
pub insecure_http: bool,
}
pub struct Loader<'a> {
pub map: &'a mut Map,
pub env_local: Option<bun_ast::Source>,
pub env_development: Option<bun_ast::Source>,
pub env_production: Option<bun_ast::Source>,
pub env_test: Option<bun_ast::Source>,
pub env_development_local: Option<bun_ast::Source>,
pub env_production_local: Option<bun_ast::Source>,
pub env_test_local: Option<bun_ast::Source>,
pub env: Option<bun_ast::Source>,
pub custom_files_loaded: StringArrayHashMap<bun_ast::Source>,
pub quiet: bool,
pub did_load_process: bool,
pub reject_unauthorized: Option<bool>,
aws_credentials: Option<S3Credentials>,
}
static DID_LOAD_CCACHE_PATH: AtomicBool = AtomicBool::new(false);
static NODE_PATH_TO_USE_SET_ONCE: bun_core::RwLock<Option<Box<[u8]>>> = bun_core::RwLock::new(None);
pub static HAS_NO_CLEAR_SCREEN_CLI_FLAG: OnceLock<bool> = OnceLock::new();
impl<'a> Loader<'a> {
#[inline]
fn is_emptyish(v: &[u8]) -> bool {
v.is_empty() || v == b"\"\"" || v == b"''"
}
#[inline]
fn get_lower_then_upper(&self, lower: &[u8], upper: &[u8]) -> Option<&[u8]> {
if let Some(p) = self.get(lower) {
if !p.is_empty() {
return Some(p);
}
}
self.get(upper)
}
pub fn iterator(&mut self) -> <HashTable as ArrayHashMapExt>::Iterator<'_> {
self.map.iterator()
}
pub fn has(&self, input: &[u8]) -> bool {
let Some(value) = self.get(input) else {
return false;
};
!Self::is_emptyish(value) && value != b"0" && value != b"false"
}
pub fn get_node_env(&self) -> Option<&[u8]> {
self.get(b"BUN_ENV").or_else(|| self.get(b"NODE_ENV"))
}
pub fn is_production(&self) -> bool {
self.get_node_env() == Some(b"production")
}
pub fn is_test(&self) -> bool {
self.get_node_env() == Some(b"test")
}
pub fn get_node_path<'b>(
&mut self,
fs: &bun_paths::fs::FileSystem,
buf: &'b mut PathBuffer,
) -> Option<&'b ZStr> {
let env_len = self
.get(b"NODE")
.or_else(|| self.get(b"npm_node_execpath"))
.filter(|n| !n.is_empty() && n.len() < MAX_PATH_BYTES)
.map(|node| {
buf[..node.len()].copy_from_slice(node);
buf[node.len()] = 0;
node.len()
});
if let Some(len) = env_len {
if bun_sys::is_executable_file_path(ZStr::from_buf(&buf[..], len)) {
return Some(ZStr::from_buf(&buf[..], len));
}
}
let path = self.get(b"PATH")?;
if let Some(node) = which(buf, path, fs.top_level_dir(), b"node") {
return Some(node);
}
None
}
pub fn is_ci(&self) -> bool {
self.get(b"CI")
.or_else(|| self.get(b"TDDIUM"))
.or_else(|| self.get(b"GITHUB_ACTIONS"))
.or_else(|| self.get(b"JENKINS_URL"))
.or_else(|| self.get(b"bamboo.buildKey"))
.is_some()
}
pub fn load_tracy(&self) {}
pub fn get_s3_credentials(&mut self) -> &S3Credentials {
if self.aws_credentials.is_none() {
let access_key_id: Box<[u8]> = self
.get(b"S3_ACCESS_KEY_ID")
.or_else(|| self.get(b"AWS_ACCESS_KEY_ID"))
.map(Box::from)
.unwrap_or_default();
let secret_access_key: Box<[u8]> = self
.get(b"S3_SECRET_ACCESS_KEY")
.or_else(|| self.get(b"AWS_SECRET_ACCESS_KEY"))
.map(Box::from)
.unwrap_or_default();
let region: Box<[u8]> = self
.get(b"S3_REGION")
.or_else(|| self.get(b"AWS_REGION"))
.map(Box::from)
.unwrap_or_default();
let mut endpoint: Box<[u8]> = Box::default();
let mut insecure_http = false;
if let Some(endpoint_) = self
.get(b"S3_ENDPOINT")
.or_else(|| self.get(b"AWS_ENDPOINT"))
{
let url = URL::parse(endpoint_);
endpoint = Box::from(url.host_with_path());
insecure_http = url.is_http();
}
let bucket: Box<[u8]> = self
.get(b"S3_BUCKET")
.or_else(|| self.get(b"AWS_BUCKET"))
.map(Box::from)
.unwrap_or_default();
let session_token: Box<[u8]> = self
.get(b"S3_SESSION_TOKEN")
.or_else(|| self.get(b"AWS_SESSION_TOKEN"))
.map(Box::from)
.unwrap_or_default();
self.aws_credentials = Some(S3Credentials {
access_key_id,
secret_access_key,
region,
endpoint,
bucket,
session_token,
insecure_http,
});
}
self.aws_credentials.as_ref().unwrap()
}
pub fn get_tls_reject_unauthorized(&mut self) -> bool {
if let Some(reject_unauthorized) = self.reject_unauthorized {
return reject_unauthorized;
}
if let Some(reject) = self.get(b"NODE_TLS_REJECT_UNAUTHORIZED") {
if reject == b"0" {
self.reject_unauthorized = Some(false);
return false;
}
if reject == b"false" {
self.reject_unauthorized = Some(false);
return false;
}
}
self.reject_unauthorized = Some(true);
true
}
pub fn get_http_proxy_for(&mut self, url: &URL<'_>) -> Option<URL<'a>> {
self.get_http_proxy(url.is_http(), Some(url.hostname), Some(url.host))
}
pub fn has_http_proxy(&self) -> bool {
self.has(b"http_proxy")
|| self.has(b"HTTP_PROXY")
|| self.has(b"https_proxy")
|| self.has(b"HTTPS_PROXY")
}
pub fn get_http_proxy(
&mut self,
is_http: bool,
hostname: Option<&[u8]>,
host: Option<&[u8]>,
) -> Option<URL<'a>> {
let extend = |s: &[u8]| -> &'a [u8] {
unsafe { core::slice::from_raw_parts(s.as_ptr(), s.len()) }
};
let mut http_proxy: Option<URL<'a>> = None;
let proxy = if is_http {
self.get_lower_then_upper(b"http_proxy", b"HTTP_PROXY")
} else {
self.get_lower_then_upper(b"https_proxy", b"HTTPS_PROXY")
};
if let Some(p) = proxy {
if !Self::is_emptyish(p) {
http_proxy = Some(URL::parse(extend(p)));
}
}
if http_proxy.is_some() && hostname.is_some() {
if self.is_no_proxy(hostname, host) {
return None;
}
}
http_proxy
}
pub fn is_no_proxy(&self, hostname: Option<&[u8]>, host: Option<&[u8]>) -> bool {
let Some(hn) = hostname else { return false };
let Some(no_proxy_text) = self.get_lower_then_upper(b"no_proxy", b"NO_PROXY") else {
return false;
};
if Self::is_emptyish(no_proxy_text) {
return false;
}
for no_proxy_item in no_proxy_text.split(|&b| b == b',') {
let mut no_proxy_entry = strings::trim(no_proxy_item, &strings::WHITESPACE_CHARS);
if no_proxy_entry.is_empty() {
continue;
}
if no_proxy_entry == b"*" {
return true;
}
if strings::starts_with_char(no_proxy_entry, b'.') {
no_proxy_entry = &no_proxy_entry[1..];
if no_proxy_entry.is_empty() {
continue;
}
}
let colon_count = no_proxy_entry.iter().filter(|&&b| b == b':').count();
let is_bracketed_ipv6 = strings::starts_with_char(no_proxy_entry, b'[');
let has_port = 'blk: {
if is_bracketed_ipv6 {
if strings::index_of(no_proxy_entry, b"]:").is_some() {
break 'blk true;
}
break 'blk false;
} else if colon_count == 1 {
break 'blk true;
}
break 'blk false;
};
if has_port {
if let Some(h) = host {
if strings::eql_case_insensitive_ascii(h, no_proxy_entry, true) {
return true;
}
}
} else {
let entry_len = no_proxy_entry.len();
if hn.len() == entry_len {
if strings::eql_case_insensitive_ascii(hn, no_proxy_entry, true) {
return true;
}
} else if hn.len() > entry_len
&& hn[hn.len() - entry_len - 1] == b'.'
&& strings::eql_case_insensitive_ascii(
&hn[hn.len() - entry_len..],
no_proxy_entry,
true,
)
{
return true;
}
}
}
false
}
pub fn load_ccache_path(&mut self, fs: &bun_paths::fs::FileSystem) {
if DID_LOAD_CCACHE_PATH.load(Ordering::Relaxed) {
return;
}
DID_LOAD_CCACHE_PATH.store(true, Ordering::Relaxed);
let _ = self.load_ccache_path_impl(fs);
}
fn load_ccache_path_impl(&mut self, fs: &bun_paths::fs::FileSystem) -> Result<(), AllocError> {
let mut buf = PathBuffer::uninit();
let path = match self.get(b"PATH") {
Some(p) => p,
None => return Ok(()),
};
let ccache_path: Box<[u8]> = which(&mut buf, path, fs.top_level_dir(), b"ccache")
.map(|z| Box::<[u8]>::from(z.as_bytes()))
.unwrap_or_default();
if !ccache_path.is_empty() {
let cxx_gop = self
.map
.get_or_put_without_value(b"CMAKE_CXX_COMPILER_LAUNCHER")?;
if !cxx_gop.found_existing {
*cxx_gop.key_ptr = Box::<[u8]>::from(&**cxx_gop.key_ptr);
*cxx_gop.value_ptr = HashTableValue {
value: ccache_path.clone(),
conditional: false,
};
}
let c_gop = self
.map
.get_or_put_without_value(b"CMAKE_C_COMPILER_LAUNCHER")?;
if !c_gop.found_existing {
*c_gop.key_ptr = Box::<[u8]>::from(&**c_gop.key_ptr);
*c_gop.value_ptr = HashTableValue {
value: ccache_path,
conditional: false,
};
}
}
Ok(())
}
pub fn load_node_js_config(
&mut self,
fs: &bun_paths::fs::FileSystem,
override_node: &[u8],
) -> Result<bool, bun_core::Error> {
let mut buf = PathBuffer::uninit();
let node_path_to_use: Box<[u8]> = if !override_node.is_empty() {
Box::from(override_node)
} else {
let cached: Option<Box<[u8]>> = NODE_PATH_TO_USE_SET_ONCE
.read()
.as_ref()
.filter(|b| !b.is_empty())
.cloned();
if let Some(c) = cached {
c
} else {
let Some(node) = self.get_node_path(fs, &mut buf) else {
return Ok(false);
};
Box::from(node.as_bytes())
}
};
*NODE_PATH_TO_USE_SET_ONCE.write() = Some(node_path_to_use.clone());
self.map.put(b"NODE", &node_path_to_use)?;
self.map.put(b"npm_node_execpath", &node_path_to_use)?;
Ok(true)
}
pub fn get_as_bool(&self, key: &[u8]) -> Option<bool> {
let value = self.get(key)?;
if value == b"" {
return Some(false);
}
if value == b"0" {
return Some(false);
}
if value == b"NO" {
return Some(false);
}
if value == b"OFF" {
return Some(false);
}
if value == b"false" {
return Some(false);
}
Some(true)
}
pub fn has_set_no_clear_terminal_on_reload(&self, default_value: bool) -> bool {
HAS_NO_CLEAR_SCREEN_CLI_FLAG
.get()
.copied()
.or_else(|| self.get_as_bool(b"BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD"))
.unwrap_or(default_value)
}
pub fn get(&self, key: &[u8]) -> Option<&[u8]> {
let mut _key = key;
if !_key.is_empty() && _key[0] == b'$' {
_key = &key[1..];
}
if _key.is_empty() {
return None;
}
self.map.get(_key)
}
pub fn get_auto<'b>(&'b self, key: &'b [u8]) -> &'b [u8] {
if key.len() < 2 || key[0] != b'$' {
return key;
}
self.get(&key[1..]).unwrap_or(key)
}
pub fn init(map: &'a mut Map) -> Loader<'a> {
Loader {
map,
env_local: None,
env_development: None,
env_production: None,
env_test: None,
env_development_local: None,
env_production_local: None,
env_test_local: None,
env: None,
custom_files_loaded: StringArrayHashMap::default(),
quiet: false,
did_load_process: false,
reject_unauthorized: None,
aws_credentials: None,
}
}
pub fn load_process(&mut self) -> Result<(), AllocError> {
if self.did_load_process {
return Ok(());
}
let environ: &[*const c_char] = bun_sys::environ();
self.map.map.ensure_total_capacity(environ.len())?;
for &_env in environ {
let env = unsafe { bun_core::ffi::cstr(_env) }.to_bytes();
if let Some(i) = strings::index_of_char(env, b'=') {
let key = &env[..i as usize];
let value = &env[i as usize + 1..];
if !key.is_empty() {
self.map.put(key, value)?;
}
} else {
if !env.is_empty() {
self.map.put(env, b"")?;
}
}
}
self.did_load_process = true;
Ok(())
}
pub fn load_from_string<const OVERWRITE: bool, const EXPAND: bool>(
&mut self,
str: &[u8],
) -> Result<(), AllocError> {
let mut value_buffer: Vec<u8> = Vec::new();
Parser::parse_bytes::<OVERWRITE, false, EXPAND>(str, self.map, &mut value_buffer)
}
pub fn load<D: DirEntryProbe + ?Sized>(
&mut self,
dir: &D,
env_files: &[&[u8]],
suffix: DotEnvFileSuffix,
skip_default_env: bool,
) -> Result<(), bun_core::Error> {
let start = bun_core::time::nano_timestamp();
let mut value_buffer: Vec<u8> = Vec::new();
if !env_files.is_empty() {
self.load_explicit_files(env_files, &mut value_buffer)?;
} else {
if !skip_default_env {
self.load_default_files(suffix, dir, &mut value_buffer)?;
}
}
if !self.quiet {
self.print_loaded(start);
}
Ok(())
}
fn load_explicit_files(
&mut self,
env_files: &[&[u8]],
value_buffer: &mut Vec<u8>,
) -> Result<(), bun_core::Error> {
let mut i: usize = env_files.len();
while i > 0 {
let arg_value = strings::trim(env_files[i - 1], b" ");
if !arg_value.is_empty() {
for file_path in arg_value.rsplit(|&b| b == b',') {
if !file_path.is_empty() {
self.load_env_file_dynamic::<false>(file_path, value_buffer)?;
analytics::Features::dotenv_inc();
}
}
}
i -= 1;
}
Ok(())
}
fn load_default_files<D: DirEntryProbe + ?Sized>(
&mut self,
suffix: DotEnvFileSuffix,
dir: &D,
value_buffer: &mut Vec<u8>,
) -> Result<(), bun_core::Error> {
let dir_handle = bun_sys::Fd::cwd();
match suffix {
DotEnvFileSuffix::Development => {
self.try_load_default(dir, dir_handle, b".env.development.local", value_buffer)?
}
DotEnvFileSuffix::Production => {
self.try_load_default(dir, dir_handle, b".env.production.local", value_buffer)?
}
DotEnvFileSuffix::Test => {
self.try_load_default(dir, dir_handle, b".env.test.local", value_buffer)?
}
}
if suffix != DotEnvFileSuffix::Test {
self.try_load_default(dir, dir_handle, b".env.local", value_buffer)?;
}
match suffix {
DotEnvFileSuffix::Development => {
self.try_load_default(dir, dir_handle, b".env.development", value_buffer)?
}
DotEnvFileSuffix::Production => {
self.try_load_default(dir, dir_handle, b".env.production", value_buffer)?
}
DotEnvFileSuffix::Test => {
self.try_load_default(dir, dir_handle, b".env.test", value_buffer)?
}
}
self.try_load_default(dir, dir_handle, b".env", value_buffer)
}
#[inline]
fn try_load_default<D: DirEntryProbe + ?Sized>(
&mut self,
dir: &D,
dir_handle: bun_sys::Fd,
name: &'static [u8],
value_buffer: &mut Vec<u8>,
) -> Result<(), bun_core::Error> {
if dir.has_comptime_query(name) {
self.load_env_file::<false>(dir_handle, name, value_buffer)?;
analytics::Features::dotenv_inc();
}
Ok(())
}
pub fn print_loaded(&self, start: i128) {
let count: usize = (self.env_development_local.is_some() as usize)
+ (self.env_production_local.is_some() as usize)
+ (self.env_test_local.is_some() as usize)
+ (self.env_local.is_some() as usize)
+ (self.env_development.is_some() as usize)
+ (self.env_production.is_some() as usize)
+ (self.env_test.is_some() as usize)
+ (self.env.is_some() as usize)
+ self.custom_files_loaded.count();
if count == 0 {
return;
}
let elapsed = (bun_core::time::nano_timestamp() - start) as f64 / 1_000_000.0;
const ALL: [&[u8]; 8] = [
b".env.development.local",
b".env.production.local",
b".env.test.local",
b".env.local",
b".env.development",
b".env.production",
b".env.test",
b".env",
];
let loaded: [bool; 8] = [
self.env_development_local.is_some(),
self.env_production_local.is_some(),
self.env_test_local.is_some(),
self.env_local.is_some(),
self.env_development.is_some(),
self.env_production.is_some(),
self.env_test.is_some(),
self.env.is_some(),
];
let mut loaded_i: usize = 0;
Output::print_elapsed(elapsed);
bun_core::pretty_error!(" <d>");
for (i, &yes) in loaded.iter().enumerate() {
if yes {
loaded_i += 1;
if count == 1 || (loaded_i >= count && count > 1) {
bun_core::pretty_error!("\"{}\"", bstr::BStr::new(ALL[i]));
} else {
bun_core::pretty_error!("\"{}\", ", bstr::BStr::new(ALL[i]));
}
}
}
for k in self.custom_files_loaded.keys() {
loaded_i += 1;
if count == 1 || (loaded_i >= count && count > 1) {
bun_core::pretty_error!("\"{}\"", bstr::BStr::new(k));
} else {
bun_core::pretty_error!("\"{}\", ", bstr::BStr::new(k));
}
}
bun_core::pretty_errorln!("<r>");
Output::flush();
}
fn default_file_slot(&mut self, base: &'static [u8]) -> &mut Option<bun_ast::Source> {
match base {
b".env.local" => &mut self.env_local,
b".env.development" => &mut self.env_development,
b".env.production" => &mut self.env_production,
b".env.test" => &mut self.env_test,
b".env.development.local" => &mut self.env_development_local,
b".env.production.local" => &mut self.env_production_local,
b".env.test.local" => &mut self.env_test_local,
b".env" => &mut self.env,
_ => unreachable!(),
}
}
pub fn load_env_file<const OVERRIDE: bool>(
&mut self,
dir: bun_sys::Fd,
base: &'static [u8],
value_buffer: &mut Vec<u8>,
) -> Result<(), bun_core::Error> {
if self.default_file_slot(base).is_some() {
return Ok(());
}
let file =
match bun_sys::File::openat(dir, base, bun_sys::O::RDONLY | bun_sys::O::CLOEXEC, 0) {
Ok(file) => file,
Err(err) => {
use bun_sys::E;
match err.get_errno() {
E::EISDIR | E::ENOENT => {
*self.default_file_slot(base) =
Some(bun_ast::Source::init_path_string(base, b""));
return Ok(());
}
E::EBUSY | E::EACCES => {
if !self.quiet {
bun_core::pretty_errorln!(
"<r><red>{}<r> error loading {} file",
bstr::BStr::new(err.name()),
bstr::BStr::new(base)
);
}
*self.default_file_slot(base) =
Some(bun_ast::Source::init_path_string(base, b""));
return Ok(());
}
_ => return Err(err.into()),
}
}
};
match read_env_file_contents(&file)? {
ReadEnvFile::Empty => {}
ReadEnvFile::ReadErr(err) => {
if !self.quiet {
bun_core::pretty_errorln!(
"<r><red>{}<r> error loading {} file",
bstr::BStr::new(err.name()),
bstr::BStr::new(base)
);
}
}
ReadEnvFile::Bytes(buf) => {
Parser::parse_bytes::<OVERRIDE, false, true>(&buf, &mut *self.map, value_buffer)?;
}
}
*self.default_file_slot(base) = Some(bun_ast::Source::init_path_string(base, b""));
Ok(())
}
pub fn load_env_file_dynamic<const OVERRIDE: bool>(
&mut self,
file_path: &[u8],
value_buffer: &mut Vec<u8>,
) -> Result<(), bun_core::Error> {
if self.custom_files_loaded.contains(file_path) {
return Ok(());
}
let file = match bun_sys::open_file(file_path, bun_sys::OpenFlags::READ_ONLY) {
Ok(f) => f,
Err(_) => {
self.custom_files_loaded
.put(file_path, bun_ast::Source::default())?;
return Ok(());
}
};
match read_env_file_contents(&file)? {
ReadEnvFile::Empty => {}
ReadEnvFile::ReadErr(err) => {
if !self.quiet {
bun_core::pretty_errorln!(
"<r><red>{}<r> error loading {} file",
bstr::BStr::new(err.name()),
bstr::BStr::new(file_path)
);
}
}
ReadEnvFile::Bytes(buf) => {
Parser::parse_bytes::<OVERRIDE, false, true>(&buf, &mut *self.map, value_buffer)?;
}
}
self.custom_files_loaded
.put(file_path, bun_ast::Source::default())?;
Ok(())
}
}
enum ReadEnvFile {
Empty,
ReadErr(bun_sys::Error),
Bytes(Vec<u8>),
}
fn read_env_file_contents(file: &bun_sys::File) -> Result<ReadEnvFile, bun_core::Error> {
match file.read_to_end() {
Ok(buf) if buf.is_empty() => Ok(ReadEnvFile::Empty),
Ok(buf) => Ok(ReadEnvFile::Bytes(buf)),
Err(err) => {
use bun_sys::E;
match err.get_errno() {
E::ENOMEM | E::EPIPE | E::EACCES | E::EISDIR => Ok(ReadEnvFile::ReadErr(err)),
_ => Err(err.into()),
}
}
}
}
struct Parser<'a> {
pos: usize,
src: &'a [u8],
value_buffer: &'a mut Vec<u8>,
}
const WHITESPACE_CHARS: &[u8] = b"\t\x0B\x0C \xA0\n\r";
impl<'a> Parser<'a> {
fn skip_line(&mut self) {
if let Some(i) = strings::index_of_any(&self.src[self.pos..], b"\n\r") {
self.pos += i + 1;
} else {
self.pos = self.src.len();
}
}
fn skip_whitespaces(&mut self) {
let mut i = self.pos;
while i < self.src.len() {
if strings::index_of_char(WHITESPACE_CHARS, self.src[i]).is_none() {
break;
}
i += 1;
}
self.pos = i;
}
fn parse_key<const CHECK_EXPORT: bool>(&mut self) -> Option<&'a [u8]> {
if CHECK_EXPORT {
self.skip_whitespaces();
}
let start = self.pos;
let mut end = start;
while end < self.src.len() {
match self.src[end] {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.' => {
end += 1;
continue;
}
_ => break,
}
}
if end < self.src.len() && start < end {
self.pos = end;
self.skip_whitespaces();
if self.pos < self.src.len() {
if CHECK_EXPORT {
if end < self.pos && &self.src[start..end] == b"export" {
if let Some(key) = self.parse_key::<false>() {
return Some(key);
}
}
}
match self.src[self.pos] {
b'=' => {
self.pos += 1;
return Some(&self.src[start..end]);
}
b':' => {
let next = self.pos + 1;
if next < self.src.len()
&& strings::index_of_char(WHITESPACE_CHARS, self.src[next]).is_some()
{
self.pos += 2;
return Some(&self.src[start..end]);
}
}
_ => {}
}
}
}
self.pos = start;
None
}
fn parse_quoted<const QUOTE: u8>(&mut self) -> Result<Option<&[u8]>, AllocError> {
if cfg!(debug_assertions) {
debug_assert!(self.src[self.pos] == QUOTE);
}
let start = self.pos;
self.value_buffer.clear(); let mut end = start + 1;
while end < self.src.len() {
match self.src[end] {
b'\\' => end += 1,
q if q == QUOTE => {
end += 1;
self.pos = end;
self.skip_whitespaces();
if self.pos >= self.src.len()
|| self.src[self.pos] == b'#'
|| strings::index_of_char(&self.src[end..self.pos], b'\n').is_some()
|| strings::index_of_char(&self.src[end..self.pos], b'\r').is_some()
{
let mut i = start;
while i < end {
match self.src[i] {
b'\\' => {
if QUOTE == b'"' {
if cfg!(debug_assertions) {
debug_assert!(i + 1 < end);
}
match self.src[i + 1] {
b'n' => {
self.value_buffer.push(b'\n');
i += 2;
}
b'r' => {
self.value_buffer.push(b'\r');
i += 2;
}
_ => {
self.value_buffer
.extend_from_slice(&self.src[i..i + 2]);
i += 2;
}
}
} else {
self.value_buffer.push(b'\\');
i += 1;
}
}
b'\r' => {
i += 1;
if i >= end || self.src[i] != b'\n' {
self.value_buffer.push(b'\n');
}
}
c => {
self.value_buffer.push(c);
i += 1;
}
}
}
return Ok(Some(self.value_buffer.as_slice()));
}
self.pos = start;
}
_ => {}
}
end += 1;
}
Ok(None)
}
fn parse_value<const IS_PROCESS: bool>(&mut self) -> Result<&[u8], AllocError> {
let start = self.pos;
self.skip_whitespaces();
let mut end = self.pos;
if end >= self.src.len() {
return Ok(&self.src[self.src.len()..]);
}
let quoted_len: Option<usize> = match self.src[end] {
b'`' => self.parse_quoted::<b'`'>()?.map(|v| v.len()),
b'"' => self.parse_quoted::<b'"'>()?.map(|v| v.len()),
b'\'' => self.parse_quoted::<b'\''>()?.map(|v| v.len()),
_ => None,
};
if let Some(len) = quoted_len {
let value = &self.value_buffer[..len];
return Ok(if IS_PROCESS {
value
} else {
&value[1..value.len() - 1]
});
}
end = start;
while end < self.src.len() {
match self.src[end] {
b'#' | b'\r' | b'\n' => break,
_ => {}
}
end += 1;
}
self.pos = end;
Ok(strings::trim(&self.src[start..end], WHITESPACE_CHARS))
}
fn expand_value(&mut self, map: &Map, value: &[u8]) -> Result<Option<&[u8]>, AllocError> {
if value.len() < 2 {
return Ok(None);
}
self.value_buffer.clear();
let mut pos = value.len() - 2;
let mut last = value.len();
loop {
if value[pos] == b'$' {
if pos > 0 && value[pos - 1] == b'\\' {
self.value_buffer
.splice(0..0, value[pos..last].iter().copied());
pos -= 1;
} else {
let mut end = if value[pos + 1] == b'{' {
pos + 2
} else {
pos + 1
};
let key_start = end;
while end < value.len() {
match value[end] {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => {
end += 1;
continue;
}
_ => break,
}
}
let lookup_value = map.get(&value[key_start..end]);
let default_value: &[u8] = if value[end..].starts_with(b":-") {
end += b":-".len();
let value_start = end;
while end < value.len() {
match value[end] {
b'}' | b'\\' => break,
_ => {
end += 1;
continue;
}
}
}
&value[value_start..end]
} else {
b""
};
if end < value.len() && value[end] == b'}' {
end += 1;
}
self.value_buffer
.splice(0..0, value[end..last].iter().copied());
self.value_buffer
.splice(0..0, lookup_value.unwrap_or(default_value).iter().copied());
}
last = pos;
}
if pos == 0 {
if last == value.len() {
return Ok(None);
}
break;
}
pos -= 1;
}
if last > 0 {
self.value_buffer
.splice(0..0, value[..last].iter().copied());
}
Ok(Some(self.value_buffer.as_slice()))
}
fn _parse<const OVERRIDE: bool, const IS_PROCESS: bool, const EXPAND: bool>(
&mut self,
map: &mut Map,
) -> Result<(), AllocError> {
let mut count = map.map.count();
while self.pos < self.src.len() {
let Some(key) = self.parse_key::<true>() else {
self.skip_line();
continue;
};
let value = self.parse_value::<IS_PROCESS>()?;
let value_owned: Box<[u8]> = Box::from(value);
let entry = map.map.get_or_put(key)?;
if entry.found_existing {
if entry.index < count {
if !OVERRIDE {
continue;
}
}
}
*entry.value_ptr = HashTableValue {
value: value_owned,
conditional: false,
};
}
if !IS_PROCESS && EXPAND {
let total = map.map.count();
let mut idx = count;
while idx < total {
let current: Box<[u8]> = Box::from(&*map.map.values()[idx].value);
if let Some(expanded) = self.expand_value(map, ¤t)? {
map.map.values_mut()[idx] = HashTableValue {
value: Box::from(expanded),
conditional: false,
};
}
idx += 1;
}
count = 0;
}
let _ = count;
Ok(())
}
pub(crate) fn parse_bytes<const OVERRIDE: bool, const IS_PROCESS: bool, const EXPAND: bool>(
src: &[u8],
map: &mut Map,
value_buffer: &mut Vec<u8>,
) -> Result<(), AllocError> {
value_buffer.clear();
let mut parser = Parser {
pos: 0,
src,
value_buffer,
};
parser._parse::<OVERRIDE, IS_PROCESS, EXPAND>(map)
}
}
pub type Value = HashTableValue;
#[derive(Default, Clone)]
pub struct HashTableValue {
pub value: Box<[u8]>,
pub conditional: bool,
}
#[cfg(not(windows))]
pub type HashTable = bun_collections::StringArrayHashMap<HashTableValue>;
#[cfg(windows)]
pub type HashTable = bun_collections::CaseInsensitiveAsciiStringArrayHashMap<HashTableValue>;
pub struct Map {
pub map: HashTable,
}
impl Default for Map {
fn default() -> Self {
Self::init()
}
}
impl Map {
pub fn create_null_delimited_env_map(&mut self) -> Result<NullDelimitedEnvMap, AllocError> {
let envp_count = self.map.count();
let mut storage: Vec<Box<[u8]>> = Vec::with_capacity(envp_count);
let mut envp_buf: Vec<*const c_char> = Vec::with_capacity(envp_count + 1);
{
let mut it = self.map.iterator();
while let Some(pair) = it.next() {
let klen = pair.key_ptr.len();
let vlen = pair.value_ptr.value.len();
let mut env_buf = vec![0u8; klen + vlen + 2].into_boxed_slice();
env_buf[..klen].copy_from_slice(pair.key_ptr);
env_buf[klen] = b'=';
env_buf[klen + 1..klen + 1 + vlen].copy_from_slice(&pair.value_ptr.value);
envp_buf.push(env_buf.as_ptr().cast::<c_char>());
storage.push(env_buf);
}
debug_assert!(envp_buf.len() == envp_count);
}
envp_buf.push(core::ptr::null()); Ok(NullDelimitedEnvMap {
_storage: storage,
envp: envp_buf.into_boxed_slice(),
})
}
pub fn std_env_map(&mut self) -> Result<StdEnvMapWrapper, AllocError> {
let mut env_map = bun_sys::EnvMap::default();
let mut it = self.map.iterator();
while let Some(entry) = it.next() {
env_map.insert(
String::from_utf8_lossy(entry.key_ptr).into_owned(),
String::from_utf8_lossy(&entry.value_ptr.value).into_owned(),
);
}
Ok(StdEnvMapWrapper {
unsafe_map: env_map,
})
}
pub fn write_windows_env_block(
&mut self,
result: &mut [u16; 32767],
) -> Result<*const u16, bun_core::Error> {
let mut i: usize = 0;
let mut it = self.map.iterator();
while let Some(pair) = it.next() {
if i + pair.key_ptr.len() + 7 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
i += strings::convert_utf8_to_utf16_in_buffer(&mut result[i..], pair.key_ptr).len();
if i + 7 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
result[i] = b'=' as u16;
i += 1;
if i + pair.value_ptr.value.len() + 5 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
i += strings::convert_utf8_to_utf16_in_buffer(&mut result[i..], &pair.value_ptr.value)
.len();
if i + 5 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
result[i] = 0;
i += 1;
}
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;
Ok(result.as_ptr())
}
pub fn iterator(&mut self) -> <HashTable as ArrayHashMapExt>::Iterator<'_> {
self.map.iterator()
}
#[inline]
pub fn iter(
&self,
) -> core::iter::Zip<core::slice::Iter<'_, Box<[u8]>>, core::slice::Iter<'_, HashTableValue>>
{
self.map.iter()
}
#[inline]
pub fn count(&self) -> usize {
self.map.count()
}
#[inline]
pub fn init() -> Map {
Map {
map: HashTable::default(),
}
}
#[inline]
pub fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
#[cfg(all(windows, debug_assertions))]
{
debug_assert!(strings::index_of_char(key, b'\x00').is_none());
}
self.map.put(
key,
HashTableValue {
value: Box::from(value),
conditional: false,
},
)
}
pub fn ensure_unused_capacity(&mut self, additional_count: usize) -> Result<(), AllocError> {
self.map.ensure_unused_capacity(additional_count)
}
pub fn put_assume_capacity(&mut self, key: &[u8], value: &[u8]) {
#[cfg(all(windows, debug_assertions))]
{
debug_assert!(strings::index_of_char(key, b'\x00').is_none());
}
self.map.put_assume_capacity(
key,
HashTableValue {
value: Box::from(value),
conditional: false,
},
);
}
#[inline]
pub fn put_alloc_key_and_value(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
let gop = self.map.get_or_put(key)?;
*gop.value_ptr = HashTableValue {
value: Box::from(value),
conditional: false,
};
if !gop.found_existing {
*gop.key_ptr = Box::from(key);
}
Ok(())
}
#[inline]
pub fn put_alloc_key(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
self.put_alloc_key_and_value(key, value)
}
#[inline]
pub fn put_alloc_value(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
self.put(key, value)
}
#[inline]
pub fn get_or_put_without_value(
&mut self,
key: &[u8],
) -> Result<GetOrPutResult<'_, Box<[u8]>, HashTableValue>, AllocError> {
self.map.get_or_put(key)
}
pub fn json_stringify(&self, writer: &mut impl core::fmt::Write) -> core::fmt::Result {
let count = self.map.count();
writer.write_str("{")?;
for (i, (k, v)) in self
.map
.keys()
.iter()
.zip(self.map.values().iter())
.enumerate()
{
writer.write_str("\n ")?;
writer.write_str(&String::from_utf8_lossy(k))?;
writer.write_str(": ")?;
writer.write_str(&String::from_utf8_lossy(&v.value))?;
if i < count - 1 {
writer.write_str(", ")?;
}
}
writer.write_str("\n}")
}
#[inline]
pub fn get(&self, key: &[u8]) -> Option<&[u8]> {
self.map.get(key).map(|entry| entry.value.as_ref())
}
#[inline]
pub fn put_default(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
let _ = self.map.get_or_put_value(
key,
HashTableValue {
value: Box::from(value),
conditional: false,
},
)?;
Ok(())
}
#[inline]
pub fn get_or_put(&mut self, key: &[u8], value: &[u8]) -> Result<(), AllocError> {
self.put_default(key, value)
}
pub fn remove(&mut self, key: &[u8]) {
let _ = self.map.swap_remove(key);
}
pub fn clone_with_allocator(&self) -> Result<Map, AllocError> {
Ok(Map {
map: self.map.clone()?,
})
}
}
pub struct NullDelimitedEnvMap {
_storage: Vec<Box<[u8]>>,
envp: Box<[*const c_char]>,
}
impl NullDelimitedEnvMap {
#[inline]
pub fn as_slice(&self) -> &[*const c_char] {
&self.envp
}
#[inline]
pub fn as_ptr(&self) -> *const *const c_char {
self.envp.as_ptr()
}
}
pub struct StdEnvMapWrapper {
pub unsafe_map: bun_sys::EnvMap,
}
impl StdEnvMapWrapper {
pub fn get(&self) -> &bun_sys::EnvMap {
&self.unsafe_map
}
}
pub static INSTANCE: AtomicPtr<Loader<'static>> = AtomicPtr::new(core::ptr::null_mut());
#[inline]
pub fn instance() -> Option<*mut Loader<'static>> {
let ptr = INSTANCE.load(Ordering::Acquire);
if ptr.is_null() { None } else { Some(ptr) }
}
#[inline]
pub fn set_instance(loader: *mut Loader<'static>) {
INSTANCE.store(loader, Ordering::Release);
}