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:
91
third_party/pi/packages/env/daemon/src/sys/mod.rs
vendored
Normal file
91
third_party/pi/packages/env/daemon/src/sys/mod.rs
vendored
Normal 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()
|
||||
}
|
||||
323
third_party/pi/packages/env/daemon/src/sys/unix.rs
vendored
Normal file
323
third_party/pi/packages/env/daemon/src/sys/unix.rs
vendored
Normal 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)
|
||||
}
|
||||
665
third_party/pi/packages/env/daemon/src/sys/windows.rs
vendored
Normal file
665
third_party/pi/packages/env/daemon/src/sys/windows.rs
vendored
Normal 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\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user