chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -0,0 +1,91 @@
//! What differs between operating systems: error names, positional reads, temporary directories, opening readers,
//! and starting and killing commands. Each implementation follows what Node (libuv) does on that system.
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::*;
/// How a string command runs: the shell, its arguments before the command, and whether the command goes to its
/// stdin instead of its arguments (legacy WSL `bash.exe`).
pub struct ShellConfig {
pub program: String,
pub args: Vec<String>,
pub command_on_stdin: bool,
}
/// How Node opens files: `r`, `writeFile` (`w`), `appendFile` (`a`), and `r+` for truncate and fsync.
#[derive(Clone, Copy)]
pub enum OpenMode {
/// Node's `open(path, "r")`: any kind of file, blocking reads.
Read,
Write,
Append,
ReadWrite,
}
/// An error code libuv reports where the system call itself reported another one, such as `ENOTDIR` from a recursive
/// `mkdir` through a file, or `EISDIR` when Windows creates a file where a directory exists.
#[derive(Debug)]
pub struct Synthetic(pub &'static str);
impl std::fmt::Display for Synthetic {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.0)
}
}
impl std::error::Error for Synthetic {}
pub fn synthetic(code: &'static str) -> std::io::Error {
std::io::Error::other(Synthetic(code))
}
/// The libuv name of an error: a synthetic code, or the system's code translated per platform.
pub fn error_name(error: &std::io::Error) -> &'static str {
if let Some(Synthetic(code)) = error
.get_ref()
.and_then(|inner| inner.downcast_ref::<Synthetic>())
{
return code;
}
os_error_name(error)
}
/// Whether a path exists, following symbolic links, like Node's `access(path, F_OK)`.
pub fn path_exists(path: &str) -> bool {
std::path::Path::new(path).metadata().is_ok()
}
/// Sixteen random bytes for temporary names (not for secrets).
pub fn random_bytes() -> [u8; 16] {
use std::hash::{BuildHasher, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let state = std::collections::hash_map::RandomState::new();
let mut bytes = [0u8; 16];
for half in 0..2 {
let mut hasher = state.build_hasher();
hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
hasher.write_u128(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|time| time.as_nanos())
.unwrap_or(0),
);
bytes[half * 8..half * 8 + 8].copy_from_slice(&hasher.finish().to_le_bytes());
}
bytes
}
/// The daemon's working directory, the `process.cwd()` Node's `path.resolve` falls back to.
pub fn cwd() -> String {
std::env::current_dir()
.map(|cwd| cwd.to_string_lossy().into_owned())
.unwrap_or_default()
}

View File

@@ -0,0 +1,323 @@
//! Linux, macOS and Android.
use super::{OpenMode, ShellConfig, path_exists};
use crate::errors::Failure;
use serde_json::{Map, Value};
use std::ffi::{CString, OsStr};
use std::fs::{File, Metadata, OpenOptions};
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
/// libuv's name for an OS error.
pub fn os_error_name(error: &io::Error) -> &'static str {
let Some(errno) = error.raw_os_error() else {
return "UNKNOWN";
};
match errno {
libc::EPERM => "EPERM",
libc::ENOENT => "ENOENT",
libc::EIO => "EIO",
libc::EBADF => "EBADF",
libc::EAGAIN => "EAGAIN",
libc::ENOMEM => "ENOMEM",
libc::EACCES => "EACCES",
libc::EBUSY => "EBUSY",
libc::EEXIST => "EEXIST",
libc::EXDEV => "EXDEV",
libc::ENOTDIR => "ENOTDIR",
libc::EISDIR => "EISDIR",
libc::EINVAL => "EINVAL",
libc::ENFILE => "ENFILE",
libc::EMFILE => "EMFILE",
libc::ETXTBSY => "ETXTBSY",
libc::EFBIG => "EFBIG",
libc::ENOSPC => "ENOSPC",
libc::EROFS => "EROFS",
libc::EMLINK => "EMLINK",
libc::ENAMETOOLONG => "ENAMETOOLONG",
libc::ENOTEMPTY => "ENOTEMPTY",
libc::ELOOP => "ELOOP",
libc::ENOSYS => "ENOSYS",
libc::ENOTSUP => "ENOTSUP",
_ => "UNKNOWN",
}
}
pub const PATH_SEPARATOR: &str = "/";
pub fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
file.read_at(buffer, offset)
}
/// Modification time as seconds and nanoseconds since the epoch.
pub fn mtime(metadata: &Metadata) -> (i64, i64) {
(metadata.mtime(), metadata.mtime_nsec())
}
/// Device and inode, which identify a file across renames.
pub fn identity(metadata: &Metadata) -> (u64, u64) {
(metadata.dev(), metadata.ino())
}
/// Node's `os.tmpdir()`; Termux's Node falls back to `$PREFIX/tmp`.
pub fn tmpdir() -> String {
let configured = ["TMPDIR", "TMP", "TEMP"]
.iter()
.find_map(|key| std::env::var(key).ok().filter(|value| !value.is_empty()));
let mut path = configured.unwrap_or_else(|| {
if cfg!(target_os = "android") {
format!(
"{}/tmp",
std::env::var("PREFIX")
.unwrap_or_else(|_| "/data/data/com.termux/files/usr".into())
)
} else {
"/tmp".into()
}
});
if path.len() > 1 && path.ends_with('/') {
path.pop();
}
path
}
pub fn home() -> String {
std::env::var("HOME").unwrap_or_default()
}
/// `mkdtemp(3)` of `prefix`, as libuv calls it: six random characters from `[A-Za-z0-9]`.
pub fn mkdtemp(prefix: &str) -> io::Result<String> {
let mut template = prefix.as_bytes().to_vec();
template.extend_from_slice(b"XXXXXX");
let template =
CString::new(template).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
let mut buffer = template.into_bytes_with_nul();
// SAFETY: `buffer` is a NUL-terminated, writable template that mkdtemp fills in place.
let created = unsafe { libc::mkdtemp(buffer.as_mut_ptr().cast()) };
if created.is_null() {
return Err(io::Error::last_os_error());
}
buffer.pop();
Ok(OsStr::from_bytes(&buffer).to_string_lossy().into_owned())
}
/// Open a regular file for positional reads, without blocking on FIFOs and, with `no_follow`, without following a
/// final symbolic link.
pub fn open_reader(path: &str, no_follow: bool) -> Result<File, Failure> {
let mut flags = libc::O_NONBLOCK;
if no_follow {
flags |= libc::O_NOFOLLOW;
}
OpenOptions::new()
.read(true)
.custom_flags(flags)
.open(path)
.map_err(|error| {
if no_follow && matches!(error.raw_os_error(), Some(libc::ELOOP) | Some(libc::EMLINK)) {
Failure::new("SYMLINK", "Refusing to follow a symbolic link").path(path)
} else {
Failure::io(&error, "open", path)
}
})
}
/// Open a file in one of Node's write modes.
pub fn open(path: &str, mode: OpenMode) -> io::Result<File> {
let mut options = OpenOptions::new();
match mode {
OpenMode::Read => options.read(true),
OpenMode::Write => options.write(true).create(true).truncate(true),
OpenMode::Append => options.append(true).create(true),
OpenMode::ReadWrite => options.read(true).write(true),
};
options.open(path)
}
pub fn realpath(path: &str) -> io::Result<String> {
std::fs::canonicalize(path).map(|resolved| resolved.to_string_lossy().into_owned())
}
pub fn rename(from: &str, to: &str) -> io::Result<()> {
std::fs::rename(from, to)
}
pub fn remove_file(path: &str) -> io::Result<()> {
std::fs::remove_file(path)
}
/// A pipe a reader thread can poll, so that it stops when the command settles.
pub trait Pipe: io::Read + std::os::fd::AsRawFd {}
impl<T: io::Read + std::os::fd::AsRawFd> Pipe for T {}
/// Read `pipe` until it ends, `on_data` returns false, or `stop` is set; the pipe closes when this returns.
pub fn read_pipe(mut pipe: impl Pipe, stop: &AtomicBool, mut on_data: impl FnMut(&[u8]) -> bool) {
let fd = pipe.as_raw_fd();
// SAFETY: fcntl on a descriptor this thread owns.
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let mut buffer = vec![0u8; 64 * 1024];
while !stop.load(Ordering::SeqCst) {
let mut poll = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
// SAFETY: one valid pollfd.
let ready = unsafe { libc::poll(&mut poll, 1, 100) };
if ready == 0
|| (ready < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted)
{
continue;
}
if ready < 0 {
return;
}
match pipe.read(&mut buffer) {
Ok(0) => return,
Ok(read) => {
if !on_data(&buffer[..read]) {
return;
}
}
Err(error)
if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
) => {}
Err(_) => return,
}
}
}
/// Readers poll `stop` every 100 ms.
pub fn interrupt_reader(_reader: &std::thread::JoinHandle<()>) {}
/// The program an argv command runs; `Command` searches `PATH` like `execvp`, as libuv does.
pub fn resolve_program(
program: &str,
_cwd: &str,
_env: &Map<String, Value>,
) -> Result<String, Failure> {
Ok(program.to_string())
}
fn which_bash() -> Option<String> {
let output = Command::new("which")
.arg("bash")
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let first = String::from_utf8_lossy(&output.stdout)
.trim()
.lines()
.next()?
.to_string();
path_exists(&first).then_some(first)
}
/// Node's shell resolution: a configured shell must exist; otherwise `/bin/bash`, `which bash`, then `sh`.
pub fn shell_config(shell_path: Option<&str>) -> Result<ShellConfig, Failure> {
let shell = |program: &str| ShellConfig {
program: program.into(),
args: vec!["-c".into()],
command_on_stdin: false,
};
if let Some(configured) = shell_path {
if path_exists(configured) {
return Ok(shell(configured));
}
return Err(Failure::new(
"shell_unavailable",
format!("Custom shell path not found: {configured}"),
));
}
if path_exists("/bin/bash") {
return Ok(shell("/bin/bash"));
}
Ok(shell(&which_bash().unwrap_or_else(|| "sh".into())))
}
/// Arguments, environment and process setup as libuv applies them.
pub fn configure(
command: &mut Command,
args: &[String],
env: &Map<String, Value>,
inherit_env: bool,
) {
command.args(args);
if !inherit_env {
command.env_clear();
}
for (key, value) in env {
if let Some(value) = value.as_str() {
command.env(key, value);
}
}
// SAFETY: only async-signal-safe calls between fork and exec.
unsafe {
command.pre_exec(|| {
// A new session, so a kill reaches every descendant; default signal handling and an empty mask, as libuv sets.
libc::setsid();
for signal in 1..32 {
if signal != libc::SIGKILL && signal != libc::SIGSTOP {
libc::signal(signal, libc::SIG_DFL);
}
}
let mut mask: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut mask);
libc::sigprocmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut());
Ok(())
});
}
}
pub fn after_spawn(_child: &Child) {}
/// Kill the command's process group, or the process if the group is gone.
pub fn kill_tree(pid: u32) {
let pid = pid as i32;
// SAFETY: plain signal delivery.
unsafe {
if libc::kill(-pid, libc::SIGKILL) != 0 {
libc::kill(pid, libc::SIGKILL);
}
}
}
/// A process killed by a signal has no exit code; report 128 + the signal, as the shell does.
pub fn exit_code(status: ExitStatus) -> i64 {
i64::from(
status
.code()
.unwrap_or_else(|| 128 + status.signal().unwrap_or(0)),
)
}
/// The bytes of a file name, which libuv sorts directory listings by.
pub fn raw_name(name: &OsStr) -> Option<Vec<u8>> {
Some(name.as_bytes().to_vec())
}
/// Windows only: the per-drive working directories of the `=C:` variables.
pub fn drive_cwds() -> Map<String, Value> {
Map::new()
}
pub fn read_dir(path: &str) -> io::Result<std::fs::ReadDir> {
std::fs::read_dir(path)
}
/// Device and inode of `path`; the metadata already has them.
pub fn path_identity(_path: &str, metadata: &Metadata, _follow: bool) -> (u64, u64) {
identity(metadata)
}

View File

@@ -0,0 +1,665 @@
//! Windows, following libuv (`src/win/error.c`, `src/win/process.c`, `src/win/fs.c`) and Node's `child_process`.
use super::{OpenMode, ShellConfig, path_exists, synthetic};
use crate::errors::Failure;
use serde_json::{Map, Value};
use std::collections::hash_map::RandomState;
use std::ffi::OsStr;
use std::fs::{File, Metadata};
use std::hash::{BuildHasher, Hasher};
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::FileExt;
use std::os::windows::io::{AsRawHandle, FromRawHandle};
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::UNIX_EPOCH;
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CreateFileW, FILE_APPEND_DATA,
FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
FILE_WRITE_DATA, GetFileInformationByHandle, MOVEFILE_REPLACE_EXISTING, MoveFileExW,
OPEN_ALWAYS, OPEN_EXISTING,
};
use windows_sys::Win32::System::IO::CancelSynchronousIo;
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, SetInformationJobObject,
};
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
/// libuv's `uv_translate_sys_error` for the errors file and process operations produce.
pub fn os_error_name(error: &io::Error) -> &'static str {
let Some(code) = error.raw_os_error() else {
return "UNKNOWN";
};
match code {
1 => "EISDIR", // ERROR_INVALID_FUNCTION
2 | 3 | 15 | 123 | 161 | 4392 => "ENOENT", // FILE_NOT_FOUND, PATH_NOT_FOUND, INVALID_DRIVE, INVALID_NAME, BAD_PATHNAME, INVALID_REPARSE_DATA
4 => "EMFILE", // ERROR_TOO_MANY_OPEN_FILES
5 | 1314 => "EPERM", // ACCESS_DENIED, PRIVILEGE_NOT_HELD
6 => "EBADF", // ERROR_INVALID_HANDLE
8 | 14 => "ENOMEM", // NOT_ENOUGH_MEMORY, OUTOFMEMORY
17 => "EXDEV", // ERROR_NOT_SAME_DEVICE
19 => "EROFS", // ERROR_WRITE_PROTECT
32 | 33 => "EBUSY", // SHARING_VIOLATION, LOCK_VIOLATION
39 | 82 | 112 => "ENOSPC", // HANDLE_DISK_FULL, CANNOT_MAKE, DISK_FULL
50 => "ENOTSUP", // ERROR_NOT_SUPPORTED
80 | 183 => "EEXIST", // FILE_EXISTS, ALREADY_EXISTS
13 | 87 | 122 | 131 | 1464 => "EINVAL", // INVALID_DATA, INVALID_PARAMETER, INSUFFICIENT_BUFFER, NEGATIVE_SEEK, SYMLINK_NOT_SUPPORTED
126 => "ENOENT", // ERROR_MOD_NOT_FOUND
145 => "ENOTEMPTY", // ERROR_DIR_NOT_EMPTY
206 => "ENAMETOOLONG", // ERROR_FILENAME_EXCED_RANGE
267 => "ENOENT", // ERROR_DIRECTORY (libuv maps it to ENOENT)
998 => "EFAULT", // ERROR_NOACCESS
740 | 1920 => "EACCES", // ELEVATION_REQUIRED, CANT_ACCESS_FILE
1921 => "ELOOP", // ERROR_CANT_RESOLVE_FILENAME
_ => "UNKNOWN",
}
}
pub const PATH_SEPARATOR: &str = "\\";
pub fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
file.seek_read(buffer, offset)
}
/// Modification time as seconds and nanoseconds since the epoch; before 1970 the seconds are negative.
pub fn mtime(metadata: &Metadata) -> (i64, i64) {
let Ok(time) = metadata.modified() else {
return (0, 0);
};
match time.duration_since(UNIX_EPOCH) {
Ok(after) => (after.as_secs() as i64, i64::from(after.subsec_nanos())),
Err(before) => {
let before = before.duration();
let nanos = i64::from(before.subsec_nanos());
if nanos == 0 {
(-(before.as_secs() as i64), 0)
} else {
(-(before.as_secs() as i64) - 1, 1_000_000_000 - nanos)
}
}
}
}
/// File index numbers need a handle per file; the creation time tells a replaced file or directory apart instead.
pub fn identity(metadata: &Metadata) -> (u64, u64) {
use std::os::windows::fs::MetadataExt;
(0, metadata.creation_time())
}
/// Node's `os.tmpdir()` on Windows.
pub fn tmpdir() -> String {
let variable = |key: &str| std::env::var(key).ok().filter(|value| !value.is_empty());
let mut path = variable("TEMP")
.or_else(|| variable("TMP"))
.unwrap_or_else(|| {
format!(
"{}\\temp",
variable("SystemRoot")
.or_else(|| variable("windir"))
.unwrap_or_default()
)
});
if path.len() > 1 && path.ends_with('\\') && !path.ends_with(":\\") {
path.pop();
}
path
}
/// libuv's `uv_os_homedir`: `USERPROFILE` first.
pub fn home() -> String {
std::env::var("USERPROFILE").unwrap_or_default()
}
/// A NUL-terminated wide path; an embedded NUL fails like the standard library's path conversion (Node rejects such
/// paths before any system call, which the client reports with the same code).
fn wide(text: &str) -> io::Result<Vec<u16>> {
if text.contains('\0') {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path contains a NUL byte",
));
}
Ok(OsStr::new(text)
.encode_wide()
.chain(std::iter::once(0))
.collect())
}
/// libuv's `fs__mkdtemp`: `prefix` plus six characters from `[a-zA-Z0-9]`, retried while the name exists.
pub fn mkdtemp(prefix: &str) -> io::Result<String> {
const CHARACTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
static COUNTER: AtomicU64 = AtomicU64::new(0);
let state = RandomState::new();
for _ in 0..1000 {
let mut hasher = state.build_hasher();
hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
let mut random = hasher.finish();
let mut path = prefix.to_string();
for _ in 0..6 {
path.push(CHARACTERS[(random % CHARACTERS.len() as u64) as usize] as char);
random /= CHARACTERS.len() as u64;
}
match std::fs::create_dir(&path) {
Ok(()) => return Ok(path),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
Err(io::Error::from_raw_os_error(80))
}
/// Windows cannot open a directory as a file and has no `O_NOFOLLOW`: check the path first, as `NodeExecutionEnv` does
/// there (not race-free).
pub fn open_reader(path: &str, no_follow: bool) -> Result<File, Failure> {
if no_follow
&& std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
{
return Err(Failure::new("SYMLINK", "Refusing to follow a symbolic link").path(path));
}
if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) {
return Err(
Failure::new("EISDIR", "EISDIR: illegal operation on a directory, read").path(path),
);
}
File::open(path).map_err(|error| Failure::io(&error, "open", path))
}
/// libuv's `fs__open` for Node's write modes: `CreateFileW` with its access, sharing and disposition, always with
/// `FILE_FLAG_BACKUP_SEMANTICS`, so a directory opens and later calls on it fail as in Node; creating a file where a
/// directory exists reports `EISDIR`. (Rust's `File::create` opens with `OPEN_ALWAYS` and truncates afterwards.)
pub fn open(path: &str, mode: OpenMode) -> io::Result<File> {
let (access, disposition) = match mode {
OpenMode::Read => (FILE_GENERIC_READ, OPEN_EXISTING),
OpenMode::Write => (FILE_GENERIC_WRITE, CREATE_ALWAYS),
OpenMode::Append => (
(FILE_GENERIC_WRITE & !FILE_WRITE_DATA) | FILE_APPEND_DATA,
OPEN_ALWAYS,
),
OpenMode::ReadWrite => (FILE_GENERIC_READ | FILE_GENERIC_WRITE, OPEN_EXISTING),
};
let name = wide(path)?;
// SAFETY: a NUL-terminated wide path; the returned handle is owned by the File on success.
let handle = unsafe {
CreateFileW(
name.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
disposition,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let error = io::Error::last_os_error();
// ERROR_FILE_EXISTS while creating means the path is a directory.
if error.raw_os_error() == Some(80) && matches!(mode, OpenMode::Write | OpenMode::Append) {
return Err(synthetic("EISDIR"));
}
return Err(error);
}
// SAFETY: `handle` is a valid, owned file handle.
Ok(unsafe { File::from_raw_handle(handle as _) })
}
/// libuv's `uv_fs_realpath`: the final path without the `\\?\` prefix.
pub fn realpath(path: &str) -> io::Result<String> {
let resolved = std::fs::canonicalize(path)?.to_string_lossy().into_owned();
Ok(if let Some(unc) = resolved.strip_prefix("\\\\?\\UNC\\") {
format!("\\\\{unc}")
} else {
resolved
.strip_prefix("\\\\?\\")
.map(str::to_string)
.unwrap_or(resolved)
})
}
/// libuv's `fs__rename`: `MoveFileExW` replacing an existing destination.
pub fn rename(from: &str, to: &str) -> io::Result<()> {
let (from, to) = (wide(from)?, wide(to)?);
// SAFETY: both arguments are NUL-terminated wide strings that outlive the call.
if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), MOVEFILE_REPLACE_EXISTING) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
/// Node's `rm` on Windows: a symbolic link or junction to a directory is removed as a directory entry (libuv's
/// `fs__unlink`), and a read-only file is retried after making it writable.
pub fn remove_file(path: &str) -> io::Result<()> {
use std::os::windows::fs::FileTypeExt;
if std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink_dir()) {
return std::fs::remove_dir(path);
}
match std::fs::remove_file(path) {
Err(error) if error.kind() == io::ErrorKind::PermissionDenied => {
let mut permissions = std::fs::metadata(path)?.permissions();
#[allow(clippy::permissions_set_readonly_false)]
permissions.set_readonly(false);
std::fs::set_permissions(path, permissions)?;
std::fs::remove_file(path)
}
other => other,
}
}
/// A pipe a reader thread reads with blocking calls; `interrupt_reader` cancels a blocked read.
pub trait Pipe: io::Read {}
impl<T: io::Read> Pipe for T {}
/// Read `pipe` until it ends, `on_data` returns false, or `stop` is set; the pipe closes when this returns.
pub fn read_pipe(mut pipe: impl Pipe, stop: &AtomicBool, mut on_data: impl FnMut(&[u8]) -> bool) {
let mut buffer = vec![0u8; 64 * 1024];
while !stop.load(Ordering::SeqCst) {
match pipe.read(&mut buffer) {
Ok(0) | Err(_) => return,
Ok(read) => {
if !on_data(&buffer[..read]) {
return;
}
}
}
}
}
/// Cancel a reader blocked in `ReadFile`, so it sees `stop` and closes its pipe.
pub fn interrupt_reader(reader: &std::thread::JoinHandle<()>) {
// SAFETY: the handle of a live or finished thread; cancelling no pending I/O is harmless.
unsafe {
CancelSynchronousIo(reader.as_raw_handle() as _);
}
}
/// libuv's `search_path` for an argv command: a name with a directory is tried as given, otherwise the working
/// directory and then each `PATH` entry; a name without an extension also tries `.com` and `.exe`. Node refuses
/// batch files without a shell.
pub fn resolve_program(
program: &str,
cwd: &str,
env: &Map<String, Value>,
) -> Result<String, Failure> {
let not_found = || Failure::new("spawn_error", format!("spawn {program} ENOENT"));
let has_extension = Path::new(program).extension().is_some();
let candidates = |base: &Path| -> Vec<std::path::PathBuf> {
let mut names = Vec::new();
if has_extension {
names.push(base.to_path_buf());
}
for extension in ["com", "exe"] {
let mut name = base.as_os_str().to_owned();
name.push(".");
name.push(extension);
names.push(name.into());
}
names
};
let found = if program.contains(['\\', '/', ':']) {
candidates(&Path::new(cwd).join(program))
.into_iter()
.find(|path| path.is_file())
} else {
let path_variable = env
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case("PATH"))
.and_then(|(_, value)| value.as_str().map(str::to_string))
.or_else(|| std::env::var("PATH").ok())
.unwrap_or_default();
std::iter::once(std::path::PathBuf::from(cwd))
.chain(std::env::split_paths(&path_variable))
.flat_map(|directory| candidates(&directory.join(program)))
.find(|path| path.is_file())
};
let found = found.ok_or_else(not_found)?;
let lower = found.to_string_lossy().to_lowercase();
if lower.ends_with(".bat") || lower.ends_with(".cmd") {
return Err(Failure::new(
"spawn_error",
format!("spawn {program} EINVAL"),
));
}
Ok(found.to_string_lossy().into_owned())
}
/// Node's `isLegacyWslBashPath`: `^[a-z]:\\windows\\(system32|sysnative)\\bash\.exe$`, case-insensitive.
fn is_legacy_wsl_bash(path: &str) -> bool {
let normalized = path.replace('/', "\\").to_lowercase();
let mut characters = normalized.chars();
characters
.next()
.is_some_and(|drive| drive.is_ascii_lowercase())
&& matches!(
characters.as_str(),
":\\windows\\system32\\bash.exe" | ":\\windows\\sysnative\\bash.exe"
)
}
fn bash_config(program: &str) -> ShellConfig {
if is_legacy_wsl_bash(program) {
ShellConfig {
program: program.into(),
args: vec!["-s".into()],
command_on_stdin: true,
}
} else {
ShellConfig {
program: program.into(),
args: vec!["-c".into()],
command_on_stdin: false,
}
}
}
fn where_bash() -> Option<String> {
let output = Command::new("where")
.arg("bash.exe")
.creation_flags(CREATE_NO_WINDOW)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let first = String::from_utf8_lossy(&output.stdout)
.trim()
.lines()
.next()?
.trim()
.to_string();
path_exists(&first).then_some(first)
}
/// Node's shell resolution on Windows: a configured shell, Git Bash, `where bash.exe`, or `shell_unavailable`.
pub fn shell_config(shell_path: Option<&str>) -> Result<ShellConfig, Failure> {
if let Some(configured) = shell_path {
if path_exists(configured) {
return Ok(bash_config(configured));
}
return Err(Failure::new(
"shell_unavailable",
format!("Custom shell path not found: {configured}"),
));
}
let mut candidates = Vec::new();
for variable in ["ProgramFiles", "ProgramFiles(x86)"] {
if let Ok(directory) = std::env::var(variable) {
candidates.push(format!("{directory}\\Git\\bin\\bash.exe"));
}
}
if let Some(found) = candidates.iter().find(|candidate| path_exists(candidate)) {
return Ok(bash_config(found));
}
if let Some(found) = where_bash() {
return Ok(bash_config(&found));
}
let searched: String = candidates
.iter()
.map(|candidate| format!(" {candidate}\n"))
.collect();
Err(Failure::new(
"shell_unavailable",
format!(
"No bash shell found. Options:\n 1. Install Git for Windows: https://git-scm.com/download/win\n 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n 3. Configure an explicit shellPath\n\nSearched Git Bash in:\n{}",
searched.trim_end()
),
))
}
/// libuv's `quote_cmd_arg`: how one argument is written into the command line the child splits again.
pub fn quote_argument(argument: &str) -> String {
if argument.is_empty() {
return "\"\"".into();
}
if !argument.contains([' ', '\t', '"']) {
return argument.into();
}
if !argument.contains(['"', '\\']) {
return format!("\"{argument}\"");
}
// Walk backwards: backslashes before a quote (or the closing quote) are doubled, and quotes are escaped.
let mut reversed = Vec::new();
let mut quote_hit = true;
for character in argument.chars().rev() {
reversed.push(character);
if quote_hit && character == '\\' {
reversed.push('\\');
} else if character == '"' {
quote_hit = true;
reversed.push('\\');
} else {
quote_hit = false;
}
}
let mut quoted = String::from("\"");
quoted.extend(reversed.into_iter().rev());
quoted.push('"');
quoted
}
/// Variables libuv copies from the parent into every child environment that lacks them.
const REQUIRED_VARIABLES: [&str; 11] = [
"HOMEDRIVE",
"HOMEPATH",
"LOGONSERVER",
"PATH",
"SYSTEMDRIVE",
"SYSTEMROOT",
"TEMP",
"USERDOMAIN",
"USERNAME",
"USERPROFILE",
"WINDIR",
];
/// The child environment as Node builds it on Windows: the variables in object order, sorted by UTF-16 code units,
/// keeping the first of names that differ only in case; then libuv adds required variables the result lacks.
fn child_environment(env: &Map<String, Value>, inherit_env: bool) -> Vec<(String, String)> {
let mut entries: Vec<(String, String)> = Vec::new();
let mut set =
|key: String, value: String| match entries.iter_mut().find(|(name, _)| *name == key) {
Some(entry) => entry.1 = value,
None => entries.push((key, value)),
};
if inherit_env {
for (key, value) in std::env::vars_os() {
set(
key.to_string_lossy().into_owned(),
value.to_string_lossy().into_owned(),
);
}
}
for (key, value) in env {
if let Some(value) = value.as_str() {
set(key.clone(), value.to_string());
}
}
entries.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
let mut seen = std::collections::HashSet::new();
entries.retain(|(name, _)| seen.insert(name.to_uppercase()));
for required in REQUIRED_VARIABLES {
if !entries
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(required))
&& let Ok(value) = std::env::var(required)
{
entries.push((required.to_string(), value));
}
}
entries
}
/// Arguments quoted as libuv quotes them, Node's environment, and a hidden console.
pub fn configure(
command: &mut Command,
args: &[String],
env: &Map<String, Value>,
inherit_env: bool,
) {
for argument in args {
command.raw_arg(quote_argument(argument));
}
command.env_clear();
for (key, value) in child_environment(env, inherit_env) {
command.env(key, value);
}
command.creation_flags(CREATE_NO_WINDOW);
}
/// libuv puts every child it starts into one job that ends its processes when the daemon exits.
fn job() -> Option<isize> {
static JOB: OnceLock<Option<isize>> = OnceLock::new();
*JOB.get_or_init(|| {
// SAFETY: plain Win32 calls with a zeroed, correctly sized information struct.
unsafe {
let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
if job.is_null() {
return None;
}
let mut information: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
information.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_BREAKAWAY_OK
| JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
| JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
| JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
(&information as *const JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
);
Some(job as isize)
}
})
}
pub fn after_spawn(child: &Child) {
if let Some(job) = job() {
// SAFETY: both handles are valid for the duration of the call.
unsafe {
AssignProcessToJobObject(job as _, child.as_raw_handle() as _);
}
}
}
/// Node's `killProcessTree` on Windows: `taskkill /F /T`, not waited for.
pub fn kill_tree(pid: u32) {
let system_root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
let _ = Command::new(format!("{system_root}\\System32\\taskkill.exe"))
.args(["/F", "/T", "/PID", &pid.to_string()])
.creation_flags(CREATE_NO_WINDOW)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
/// Exit codes are unsigned 32-bit values on Windows (an access violation is 3221225477).
pub fn exit_code(status: ExitStatus) -> i64 {
i64::from(status.code().unwrap_or(1) as u32)
}
/// Windows listings keep the file system's order; nothing sorts by raw names.
pub fn raw_name(_name: &OsStr) -> Option<Vec<u8>> {
None
}
/// The per-drive working directories of the `=C:` variables, which Node's `path.resolve` uses for drive-relative paths.
pub fn drive_cwds() -> Map<String, Value> {
std::env::vars_os()
.filter_map(|(key, value)| {
let key = key.to_str()?;
let bytes = key.as_bytes();
(bytes.len() == 3
&& bytes[0] == b'='
&& bytes[1].is_ascii_alphabetic()
&& bytes[2] == b':')
.then(|| {
(
key[1..].to_ascii_uppercase(),
Value::String(value.to_string_lossy().into_owned()),
)
})
})
.collect()
}
/// libuv's `scandir` and `opendir` report a file as `ENOTDIR` (elsewhere `ERROR_DIRECTORY` is `ENOENT`).
pub fn read_dir(path: &str) -> io::Result<std::fs::ReadDir> {
std::fs::read_dir(path).map_err(|error| {
if error.raw_os_error() == Some(267) {
synthetic("ENOTDIR")
} else {
error
}
})
}
/// Volume serial number and file index of `path`, as libuv's `stat` reports `dev` and `ino`: the creation time is not
/// enough, because NTFS gives a file created under a recently removed name the old one's creation time ("tunneling").
/// Opens a handle, like libuv; falls back to the metadata when the path cannot be opened.
pub fn path_identity(path: &str, metadata: &Metadata, follow: bool) -> (u64, u64) {
let Ok(name) = wide(path) else {
return identity(metadata);
};
let flags = FILE_FLAG_BACKUP_SEMANTICS
| if follow {
0
} else {
FILE_FLAG_OPEN_REPARSE_POINT
};
// SAFETY: a NUL-terminated wide path; the handle is closed below.
let handle = unsafe {
CreateFileW(
name.as_ptr(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
flags,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
return identity(metadata);
}
// SAFETY: a valid handle and a zeroed buffer for the call to fill.
let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let found = unsafe { GetFileInformationByHandle(handle, &mut information) } != 0;
// SAFETY: the handle opened above.
unsafe { CloseHandle(handle) };
if !found {
return identity(metadata);
}
(
u64::from(information.dwVolumeSerialNumber),
(u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow),
)
}
#[cfg(test)]
mod tests {
use super::quote_argument;
#[test]
fn quotes_arguments_like_libuv() {
assert_eq!(quote_argument(""), "\"\"");
assert_eq!(quote_argument("plain"), "plain");
assert_eq!(quote_argument("a b"), "\"a b\"");
assert_eq!(quote_argument("*.txt"), "*.txt");
assert_eq!(quote_argument("say \"hi\""), "\"say \\\"hi\\\"\"");
assert_eq!(
quote_argument("C:\\dir with space\\"),
"\"C:\\dir with space\\\\\""
);
assert_eq!(quote_argument("a\\\\b c"), "\"a\\\\b c\"");
}
}