#![warn(unused_must_use)]
use bstr::BStr;
#[cfg(windows)]
use bun_core::{WStr, w};
use bun_core::{ZStr, strings};
#[cfg(not(windows))]
use bun_paths::DELIMITER;
#[cfg(windows)]
use bun_paths::resolve_path::PosixToWinNormalizer;
use bun_paths::resolve_path::posix_to_platform_in_place;
#[cfg(windows)]
use bun_paths::w_path_buffer_pool;
use bun_paths::{MAX_PATH_BYTES, PathBuffer, SEP, WPathBuffer, is_absolute, path_buffer_pool};
#[allow(non_upper_case_globals)]
mod scope {
bun_core::declare_scope!(which, hidden);
}
use scope::which as which_log;
#[cfg(not(windows))]
fn is_valid(buf: &mut PathBuffer, segment: &[u8], bin: &[u8]) -> Option<u16> {
let prefix_len = segment.len() + 1; let len = prefix_len + bin.len();
let len_z = len + 1; if len_z > MAX_PATH_BYTES {
return None;
}
buf[..segment.len()].copy_from_slice(segment);
buf[segment.len()] = SEP;
buf[prefix_len..prefix_len + bin.len()].copy_from_slice(bin);
buf[len] = 0;
let filepath = ZStr::from_buf(&buf[..], len);
if !bun_sys::is_executable_file_path(filepath) {
return None;
}
Some(u16::try_from(filepath.len()).expect("int cast"))
}
pub fn which<'a>(buf: &'a mut PathBuffer, path: &[u8], cwd: &[u8], bin: &[u8]) -> Option<&'a ZStr> {
if bin.len() > MAX_PATH_BYTES {
return None;
}
bun_core::scoped_log!(
which_log,
"path={} cwd={} bin={}",
BStr::new(path),
BStr::new(cwd),
BStr::new(bin)
);
#[cfg(windows)]
{
let mut convert_buf = w_path_buffer_pool::get();
let result = which_win(&mut *convert_buf, path, cwd, bin)?;
let result_converted =
bun_core::strings::convert_utf16_to_utf8_in_buffer(&mut buf[..], result);
let result_converted_len = result_converted.len();
let result_converted_ptr = result_converted.as_ptr();
buf[result_converted_len] = 0;
debug_assert!(result_converted_ptr == buf.as_ptr());
return Some(ZStr::from_buf(&buf[..], result_converted_len));
}
#[cfg(not(windows))]
{
if bin.is_empty() {
return None;
}
if is_absolute(bin) {
buf[..bin.len()].copy_from_slice(bin);
buf[bin.len()] = 0;
let bin_z = unsafe { ZStr::from_raw_mut(buf.as_mut_ptr(), bin.len()) };
if bun_sys::is_executable_file_path(&*bin_z) {
return Some(&*bin_z);
}
return None;
}
if strings::index_of_char(bin, b'/').is_some() {
if !cwd.is_empty() {
let mut cwd_trimmed = cwd;
while cwd_trimmed.last() == Some(&SEP) {
cwd_trimmed = &cwd_trimmed[..cwd_trimmed.len() - 1];
}
if let Some(len) = is_valid(
buf,
cwd_trimmed,
strings::without_prefix_comptime(bin, b"./"),
) {
return Some(ZStr::from_buf(&buf[..], len as usize));
}
}
return None;
}
for segment in path.split(|b| *b == DELIMITER).filter(|s| !s.is_empty()) {
if let Some(len) = is_valid(buf, segment, bin) {
return Some(ZStr::from_buf(&buf[..], len as usize));
}
}
None
}
}
#[cfg(windows)]
static WIN_EXTENSIONS_W: [&[u16]; 3] = [w!("exe"), w!("cmd"), w!("bat")];
const WIN_EXTENSIONS: [&[u8]; 3] = [b"exe", b"cmd", b"bat"];
pub fn ends_with_extension(str: &[u8]) -> bool {
if str.len() < 4 {
return false;
}
if str[str.len() - 4] != b'.' {
return false;
}
let file_ext = &str[str.len() - 3..];
for ext in WIN_EXTENSIONS {
if strings::eql_case_insensitive_asciii_check_length(file_ext, ext) {
return true;
}
}
false
}
pub fn is_batch_file(path: &[u8]) -> bool {
let mut end = path.len();
while end > 0 && matches!(path[end - 1], b' ' | b'.') {
end -= 1;
}
if end < 4 || path[end - 4] != b'.' {
return false;
}
let file_ext = &path[end - 3..end];
strings::eql_case_insensitive_asciii_check_length(file_ext, b"cmd")
|| strings::eql_case_insensitive_asciii_check_length(file_ext, b"bat")
}
pub fn batch_arg_has_cmd_metachars(arg: &[u8]) -> bool {
arg.iter().any(|&c| {
matches!(
c,
b'"' | b'%' | b'&' | b'|' | b'<' | b'>' | b'^' | b'\r' | b'\n'
)
})
}
fn search_bin(
buf: &mut WPathBuffer,
path_size: usize,
check_windows_extensions: bool,
) -> Option<&mut [u16]> {
#[cfg(windows)]
{
if !check_windows_extensions {
if bun_sys::exists_os_path(WStr::from_buf(&buf[..], path_size), true) {
return Some(&mut buf[..path_size]);
}
}
if check_windows_extensions {
buf[path_size] = b'.' as u16;
buf[path_size + 1 + 3] = 0;
for ext in WIN_EXTENSIONS_W {
buf[path_size + 1..path_size + 1 + 3].copy_from_slice(ext);
if bun_sys::exists_os_path(
WStr::from_buf(&buf[..], path_size + 1 + ext.len()),
true,
) {
return Some(&mut buf[..path_size + 1 + ext.len()]);
}
}
}
None
}
#[cfg(not(windows))]
{
let _ = (buf, path_size, check_windows_extensions);
None
}
}
fn search_bin_in_path<'a>(
buf: &'a mut WPathBuffer,
path_buf: &mut PathBuffer,
path: &[u8],
bin: &[u8],
check_windows_extensions: bool,
) -> Option<&'a mut [u16]> {
if path.is_empty() {
return None;
}
#[cfg(windows)]
let segment: &[u8] = if is_absolute(path) {
match PosixToWinNormalizer::resolve_cwd_with_external_buf(path_buf, path) {
Ok(s) => s,
Err(_) => return None,
}
} else {
path
};
#[cfg(not(windows))]
let segment: &[u8] = {
let _ = path_buf;
path
};
let tail_units = if check_windows_extensions { 5 } else { 1 };
if segment.len() + 1 + bin.len() + tail_units > buf.len()
&& bun_core::strings::element_length_utf8_into_utf16(segment)
+ 1
+ bun_core::strings::element_length_utf8_into_utf16(bin)
+ tail_units
> buf.len()
{
return None;
}
let segment_utf16 = bun_core::strings::convert_utf8_to_utf16_in_buffer(
&mut buf[..],
bun_core::strings::without_trailing_slash(segment),
);
let segment_utf16_len = segment_utf16.len();
buf[segment_utf16_len] = SEP as u16;
let bin_utf16 =
bun_core::strings::convert_utf8_to_utf16_in_buffer(&mut buf[segment_utf16_len + 1..], bin);
let path_size = segment_utf16_len + 1 + bin_utf16.len();
buf[path_size] = 0;
search_bin(buf, path_size, check_windows_extensions)
}
pub fn which_win<'a>(
buf: &'a mut WPathBuffer,
path: &[u8],
cwd: &[u8],
bin: &[u8],
) -> Option<&'a [u16]> {
if bin.is_empty() {
return None;
}
let mut path_buf = path_buffer_pool::get();
let check_windows_extensions = !ends_with_extension(bin);
if is_absolute(bin) {
#[cfg(windows)]
let normalized_bin =
match PosixToWinNormalizer::resolve_cwd_with_external_buf(&mut *path_buf, bin) {
Ok(s) => s,
Err(_) => return None,
};
#[cfg(not(windows))]
let normalized_bin = bin;
let bin_utf16 =
bun_core::strings::convert_utf8_to_utf16_in_buffer(&mut buf[..], normalized_bin);
let bin_utf16_len = bin_utf16.len();
buf[bin_utf16_len] = 0;
return search_bin(buf, bin_utf16_len, check_windows_extensions).map(|w| &*w);
}
if strings::index_of_char(bin, b'/').is_some() || strings::index_of_char(bin, b'\\').is_some() {
let buf_reborrow: &'a mut WPathBuffer =
unsafe { &mut *std::ptr::from_mut::<WPathBuffer>(buf) };
if let Some(bin_path) = search_bin_in_path(
buf_reborrow,
&mut *path_buf,
cwd,
strings::without_prefix_comptime(bin, b"./"),
check_windows_extensions,
) {
posix_to_platform_in_place(bin_path);
return Some(&*bin_path);
}
return None;
}
for segment_part in path.split(|b| *b == b';').filter(|s| !s.is_empty()) {
let buf_reborrow: &'a mut WPathBuffer =
unsafe { &mut *std::ptr::from_mut::<WPathBuffer>(buf) };
if let Some(bin_path) = search_bin_in_path(
buf_reborrow,
&mut *path_buf,
segment_part,
bin,
check_windows_extensions,
) {
return Some(&*bin_path);
}
}
None
}