Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
//! Filesystem utilities
|
||||
|
||||
pub use super::platform::fs::*;
|
||||
|
||||
/// Extension trait for path-related filesystem operations.
|
||||
pub trait PathExt {
|
||||
/// Returns true if the path exists and is readable by the current user.
|
||||
fn readable(&self) -> bool;
|
||||
/// Returns true if the path exists and is writable by the current user.
|
||||
fn writable(&self) -> bool;
|
||||
/// Returns true if the path exists and is executable by the current user.
|
||||
///
|
||||
/// On Windows, this returns true if *either* the path itself is a file with
|
||||
/// a `PATHEXT` extension *or* appending some `PATHEXT` extension resolves
|
||||
/// to an existing file. To recover the actual on-disk path in the
|
||||
/// latter case, use [`resolve_executable`] which takes ownership
|
||||
/// and avoids copies on platforms where no resolution is needed.
|
||||
fn executable(&self) -> bool;
|
||||
|
||||
/// Returns true if the path exists and is a block device.
|
||||
fn exists_and_is_block_device(&self) -> bool;
|
||||
/// Returns true if the path exists and is a character device.
|
||||
fn exists_and_is_char_device(&self) -> bool;
|
||||
/// Returns true if the path exists and is a FIFO (named pipe).
|
||||
fn exists_and_is_fifo(&self) -> bool;
|
||||
/// Returns true if the path exists and is a socket.
|
||||
fn exists_and_is_socket(&self) -> bool;
|
||||
/// Returns true if the path exists and has the setgid bit set.
|
||||
fn exists_and_is_setgid(&self) -> bool;
|
||||
/// Returns true if the path exists and has the setuid bit set.
|
||||
fn exists_and_is_setuid(&self) -> bool;
|
||||
/// Returns true if the path exists and has the sticky bit set.
|
||||
fn exists_and_is_sticky_bit(&self) -> bool;
|
||||
|
||||
/// Returns the device ID and inode number for the path.
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get() -> std::io::Result<std::ffi::OsString> {
|
||||
hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::missing_const_for_fn)]
|
||||
#![allow(clippy::needless_pass_by_ref_mut)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
#![allow(clippy::unnecessary_wraps)]
|
||||
#![allow(clippy::unused_async)]
|
||||
#![allow(clippy::unused_self)]
|
||||
|
||||
pub mod async_pipe;
|
||||
pub mod commands;
|
||||
pub(crate) mod env;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod input;
|
||||
pub(crate) mod network;
|
||||
pub(crate) mod pipes;
|
||||
pub mod poll;
|
||||
pub mod process;
|
||||
pub mod resource;
|
||||
pub mod signal;
|
||||
pub mod terminal;
|
||||
pub(crate) mod users;
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Async pipe reading utilities for non-Unix platforms.
|
||||
//!
|
||||
//! Uses `spawn_blocking` internally for the I/O operation only,
|
||||
//! not for the entire subshell execution.
|
||||
|
||||
use std::io::{self, Read};
|
||||
|
||||
pub(crate) struct AsyncPipeReader {
|
||||
inner: Option<std::io::PipeReader>,
|
||||
}
|
||||
|
||||
impl AsyncPipeReader {
|
||||
pub(crate) fn new(fd: std::io::PipeReader) -> io::Result<Self> {
|
||||
Ok(Self { inner: Some(fd) })
|
||||
}
|
||||
|
||||
pub(crate) async fn read_to_string(&mut self) -> io::Result<String> {
|
||||
let Some(reader) = self.inner.take() else {
|
||||
return Ok(String::new());
|
||||
};
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut s = String::new();
|
||||
{ reader }.read_to_string(&mut s)?;
|
||||
Ok(s)
|
||||
})
|
||||
.await
|
||||
.map_err(io::Error::other)?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Command execution utilities.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
/// Extension trait for Unix-like command extensions.
|
||||
pub trait CommandExt {
|
||||
/// Sets the zeroth argument (argv[0]) of the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `arg` - The argument to set as argv[0].
|
||||
fn arg0<S>(&mut self, arg: S) -> &mut Self
|
||||
where
|
||||
S: AsRef<OsStr>;
|
||||
|
||||
/// Sets the process group ID of the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pgroup` - The process group ID to set.
|
||||
fn process_group(&mut self, pgroup: i32) -> &mut Self;
|
||||
}
|
||||
|
||||
impl CommandExt for std::process::Command {
|
||||
fn arg0<S>(&mut self, _arg: S) -> &mut Self
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
// NOTE: no-op.
|
||||
self
|
||||
}
|
||||
|
||||
fn process_group(&mut self, _pgroup: i32) -> &mut Self {
|
||||
// NOTE: no-op.
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for Unix-like exit status extensions.
|
||||
pub trait ExitStatusExt {
|
||||
/// Returns the signal that terminated the process, if any.
|
||||
fn signal(&self) -> Option<i32>;
|
||||
}
|
||||
|
||||
impl ExitStatusExt for std::process::ExitStatus {
|
||||
fn signal(&self) -> Option<i32> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for injecting file descriptors into commands.
|
||||
pub trait CommandFdInjectionExt {
|
||||
/// Injects the given open files as file descriptors into the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `open_files` - A mapping of child file descriptors to open files.
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error>;
|
||||
}
|
||||
|
||||
impl CommandFdInjectionExt for std::process::Command {
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
mut open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error> {
|
||||
if open_files.next().is_some() {
|
||||
return Err(error::ErrorKind::NotSupportedOnThisPlatform("fd redirections").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for arranging for commands to take the foreground.
|
||||
pub trait CommandFgControlExt {
|
||||
/// Arranges for the command to take the foreground when it is executed.
|
||||
fn take_foreground(&mut self);
|
||||
/// Arranges for the command to become a session leader when it is executed.
|
||||
fn lead_session(&mut self);
|
||||
}
|
||||
|
||||
impl CommandFgControlExt for std::process::Command {
|
||||
fn take_foreground(&mut self) {
|
||||
// NOTE: This is a no-op.
|
||||
}
|
||||
|
||||
fn lead_session(&mut self) {
|
||||
// NOTE: This is a no-op.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Environment variable retrieval (stub implementation).
|
||||
|
||||
/// Retrieves environment variables from the host process.
|
||||
///
|
||||
/// Stub implementation that returns no variables.
|
||||
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
|
||||
std::iter::empty()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! File descriptor utilities.
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
/// Stub implementation for platforms that do not support enumerating file descriptors.
|
||||
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
/// Stub implementation for platforms that do not support opening file descriptors.
|
||||
pub fn try_get_file_for_open_fd(_fd: ShellFd) -> Option<openfiles::OpenFile> {
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Filesystem utilities (stubs).
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub(crate) trait MetadataExt {
|
||||
fn gid(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn uid(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl MetadataExt for std::fs::Metadata {}
|
||||
|
||||
pub(crate) fn get_default_executable_search_paths() -> Vec<std::path::PathBuf> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the default paths where standard Unix utilities are typically installed.
|
||||
/// This is a stub implementation that returns an empty vector.
|
||||
pub fn get_default_standard_utils_paths() -> Vec<std::path::PathBuf> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Opens a null file that will discard all I/O.
|
||||
///
|
||||
/// This is a stub implementation that returns an error.
|
||||
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("opening null file").into())
|
||||
}
|
||||
|
||||
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
|
||||
//
|
||||
// This is a stub implementation that returns no result.
|
||||
pub fn try_open_special_file(
|
||||
_path: &std::path::Path,
|
||||
) -> Option<Result<std::fs::File, std::io::Error>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell profile script.
|
||||
///
|
||||
/// Stub implementation that returns `None`.
|
||||
pub fn get_system_profile_path() -> Option<&'static std::path::Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell rc script.
|
||||
///
|
||||
/// Stub implementation that returns `None`.
|
||||
pub fn get_system_rc_path() -> Option<&'static std::path::Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the platform default for case-insensitive pathname expansion.
|
||||
///
|
||||
/// In the stub implementation, this returns `false`.
|
||||
pub const fn default_case_insensitive_path_expansion() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the string contains a path separator character.
|
||||
///
|
||||
/// In the stub implementation, only `/` is considered a path separator.
|
||||
pub fn contains_path_separator(s: &str) -> bool {
|
||||
s.contains('/')
|
||||
}
|
||||
|
||||
/// Returns true if the string ends with a path separator character.
|
||||
///
|
||||
/// In the stub implementation, only `/` is considered a path separator.
|
||||
pub fn ends_with_path_separator(s: &str) -> bool {
|
||||
s.ends_with('/')
|
||||
}
|
||||
|
||||
/// Returns the string with a trailing path separator removed, if present.
|
||||
///
|
||||
/// In the stub implementation, only `/` is considered a path separator.
|
||||
pub fn strip_path_separator_suffix(s: &str) -> &str {
|
||||
s.strip_suffix('/').unwrap_or(s)
|
||||
}
|
||||
|
||||
/// Finds the byte index of the last path separator in the string.
|
||||
///
|
||||
/// In the stub implementation, only `/` is considered a path separator.
|
||||
pub fn rfind_path_separator(s: &str) -> Option<usize> {
|
||||
s.rfind('/')
|
||||
}
|
||||
|
||||
/// Splits a string on path separator characters, returning an iterator of components.
|
||||
///
|
||||
/// In the stub implementation, only `/` is used as a separator.
|
||||
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
|
||||
s.split('/')
|
||||
}
|
||||
|
||||
/// Returns the root path for an absolute pattern, if the first component indicates one.
|
||||
///
|
||||
/// In the stub implementation, an empty first component indicates an absolute path.
|
||||
pub fn pattern_path_root(first_component: &str) -> Option<std::path::PathBuf> {
|
||||
if first_component.is_empty() {
|
||||
Some(std::path::PathBuf::from("/"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a component onto a path for pattern expansion.
|
||||
///
|
||||
/// In the stub implementation, this delegates directly to `PathBuf::push`.
|
||||
pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
|
||||
path.push(component);
|
||||
}
|
||||
|
||||
/// Normalizes path separators for shell output.
|
||||
///
|
||||
/// In the stub implementation, this is a no-op.
|
||||
pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
|
||||
std::borrow::Cow::Borrowed(s)
|
||||
}
|
||||
|
||||
/// Resolves an owned path to the actual on-disk executable file, if any.
|
||||
///
|
||||
/// In the stub implementation, returns the path unchanged if it is
|
||||
/// executable (per the stub `PathExt`, which considers every path
|
||||
/// executable).
|
||||
pub fn resolve_executable(path: std::path::PathBuf) -> Option<std::path::PathBuf> {
|
||||
use crate::sys::fs::PathExt;
|
||||
if path.as_path().executable() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Terminal input utilities
|
||||
|
||||
use crate::{error, interfaces};
|
||||
|
||||
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
|
||||
/// if the key code is not recognized.
|
||||
///
|
||||
/// This is a stub implementation that recognizes single-byte non-control
|
||||
/// characters but does not support terminal-specific key sequences.
|
||||
pub fn try_get_key_from_key_code(key_code: &[u8]) -> Option<interfaces::Key> {
|
||||
if key_code.len() == 1 && !key_code[0].is_ascii_control() {
|
||||
Some(interfaces::Key::Character(key_code[0] as char))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
Ok("".into())
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/// Stub implementation of a pipe reader.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PipeReader {}
|
||||
|
||||
impl PipeReader {
|
||||
/// Tries to clone the reader.
|
||||
pub fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipeReader> for std::process::Stdio {
|
||||
fn from(_reader: PipeReader) -> Self {
|
||||
Self::null()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for PipeReader {
|
||||
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation o a pipe writer.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PipeWriter {}
|
||||
|
||||
impl PipeWriter {
|
||||
/// Tries to clone the writer.
|
||||
pub fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PipeWriter> for std::process::Stdio {
|
||||
fn from(_writer: PipeWriter) -> Self {
|
||||
Self::null()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for PipeWriter {
|
||||
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pipe() -> std::io::Result<(PipeReader, PipeWriter)> {
|
||||
Ok((PipeReader {}, PipeWriter {}))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Stub file descriptor polling utilities for platforms without poll support.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::openfiles::OpenFile;
|
||||
|
||||
/// Stub implementation that always returns an unsupported error.
|
||||
///
|
||||
/// Timeout-based reading is not supported on this platform.
|
||||
pub fn poll_for_input(_file: &OpenFile, _timeout: Duration) -> std::io::Result<bool> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"poll-based timeout is not supported on this platform",
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Process management utilities
|
||||
|
||||
pub(crate) type ProcessId = i32;
|
||||
|
||||
/// Provides access to a child process.
|
||||
pub struct Child {
|
||||
inner: std::process::Child,
|
||||
}
|
||||
|
||||
pub(crate) use std::process::ExitStatus;
|
||||
pub(crate) use std::process::Output;
|
||||
|
||||
impl Child {
|
||||
/// Returns the process ID of the child process, if available.
|
||||
pub fn id(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Asynchronously waits for the child process to exit.
|
||||
pub async fn wait(&mut self) -> std::io::Result<ExitStatus> {
|
||||
self.inner.wait()
|
||||
}
|
||||
|
||||
/// Asynchronously waits for the child process to exit and collects its
|
||||
/// output.
|
||||
pub async fn wait_with_output(self) -> std::io::Result<Output> {
|
||||
self.inner.wait_with_output()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spawn(mut command: std::process::Command) -> std::io::Result<Child> {
|
||||
let child = command.spawn()?;
|
||||
Ok(Child { inner: child })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Returns the user and system CPU time used by the current process.
|
||||
///
|
||||
/// This is a stub implementation that returns zero durations.
|
||||
pub fn get_self_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
|
||||
}
|
||||
|
||||
/// Returns the user and system CPU time used by child processes.
|
||||
///
|
||||
/// This is a stub implementation that returns zero durations.
|
||||
pub fn get_children_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::{error, sys, traps};
|
||||
|
||||
/// A stub enum representing system signals on unsupported platforms.
|
||||
#[allow(unnameable_types)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Signal {}
|
||||
|
||||
impl Signal {
|
||||
/// Returns an iterator over all possible signals.
|
||||
pub fn iterator() -> impl Iterator<Item = Self> {
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
/// Converts the signal into its corresponding name as a `&'static str`.
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
/// Creates a `Signal` from a string representation.
|
||||
pub fn from_str(s: &str) -> Result<Self, error::Error> {
|
||||
Err(error::ErrorKind::InvalidSignal(s.into()).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for Signal {
|
||||
type Error = error::Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
Err(error::ErrorKind::InvalidSignal(std::format!("{value}")).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("continuing process").into())
|
||||
}
|
||||
|
||||
/// Sends a signal to a specific process.
|
||||
///
|
||||
/// This is a stub implementation that returns an error.
|
||||
pub fn kill_process(
|
||||
_pid: sys::process::ProcessId,
|
||||
_signal: traps::TrapSignal,
|
||||
) -> Result<(), error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("killing process").into())
|
||||
}
|
||||
|
||||
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct FakeSignal {}
|
||||
|
||||
impl FakeSignal {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub async fn recv(&self) {
|
||||
futures::future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tstp_signal_listener() -> Result<FakeSignal, error::Error> {
|
||||
Ok(FakeSignal::new())
|
||||
}
|
||||
|
||||
pub(crate) fn chld_signal_listener() -> Result<FakeSignal, error::Error> {
|
||||
Ok(FakeSignal::new())
|
||||
}
|
||||
|
||||
pub(crate) async fn await_ctrl_c() -> std::io::Result<()> {
|
||||
FakeSignal::new().recv().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Terminal utilities.
|
||||
|
||||
use crate::{error, openfiles, sys, terminal};
|
||||
|
||||
/// Terminal configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config;
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
impl Config {
|
||||
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
|
||||
/// with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_file` - A reference to the open terminal.
|
||||
pub fn from_term(_file: &openfiles::OpenFile) -> Result<Self, error::Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
/// Applies the terminal settings to the terminal associated with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_file` - A reference to the open terminal.
|
||||
pub fn apply_to_term(&self, _file: &openfiles::OpenFile) -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies the given high-level terminal settings to this configuration. Does not modify any
|
||||
/// terminal itself.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `_settings` - The high-level terminal settings to apply to this configuration.
|
||||
pub fn update(&mut self, _settings: &terminal::Settings) {}
|
||||
}
|
||||
|
||||
/// Get the process ID of this process's parent.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the process group ID for this process's process group.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the foreground process ID of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Move the specified process to the foreground of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that takes no action.
|
||||
pub fn move_to_foreground(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the current process to the foreground of the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that returns `None`.
|
||||
pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to get the path of the terminal device associated with the attached terminal.
|
||||
///
|
||||
/// This is a stub implementation that always returns `None`.
|
||||
pub fn try_get_terminal_device_path() -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) fn get_user_home_dir(_username: &str) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
std::env::home_dir()
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective uid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective gid").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current username").into())
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Process management utilities
|
||||
|
||||
pub(crate) type ProcessId = i32;
|
||||
pub(crate) use tokio::process::Child;
|
||||
|
||||
pub(crate) fn spawn(command: std::process::Command) -> std::io::Result<Child> {
|
||||
let mut command = tokio::process::Command::from(command);
|
||||
command.spawn()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
pub mod async_pipe;
|
||||
pub mod commands;
|
||||
pub(crate) mod env;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod input;
|
||||
pub(crate) mod network;
|
||||
pub mod poll;
|
||||
use crate::error;
|
||||
pub use crate::sys::tokio_process as process;
|
||||
pub mod resource;
|
||||
pub mod signal;
|
||||
pub mod terminal;
|
||||
pub(crate) mod users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {
|
||||
/// A system error occurred.
|
||||
#[error("system error: {0}")]
|
||||
ErrnoError(#[from] nix::errno::Errno),
|
||||
}
|
||||
|
||||
impl From<nix::errno::Errno> for error::ErrorKind {
|
||||
fn from(err: nix::errno::Errno) -> Self {
|
||||
PlatformError::ErrnoError(err).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Async pipe reading utilities for Unix.
|
||||
|
||||
use std::io;
|
||||
use std::os::unix::io::OwnedFd;
|
||||
|
||||
use tokio::net::unix::pipe;
|
||||
|
||||
pub(crate) struct AsyncPipeReader(pipe::Receiver);
|
||||
|
||||
impl AsyncPipeReader {
|
||||
pub(crate) fn new(reader: std::io::PipeReader) -> io::Result<Self> {
|
||||
Ok(Self(pipe::Receiver::from_file(std::fs::File::from(
|
||||
OwnedFd::from(reader),
|
||||
))?))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_to_string(&mut self) -> io::Result<String> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
let mut s = String::new();
|
||||
self.0.read_to_string(&mut s).await?;
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//! Command execution utilities.
|
||||
|
||||
pub use std::os::unix::process::CommandExt;
|
||||
pub use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use command_fds::{CommandFdExt, FdMapping};
|
||||
|
||||
use crate::ShellFd;
|
||||
use crate::error;
|
||||
use crate::openfiles;
|
||||
|
||||
/// Extension trait for injecting file descriptors into commands.
|
||||
pub trait CommandFdInjectionExt {
|
||||
/// Injects the given open files as file descriptors into the command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `open_files` - A mapping of child file descriptors to open files.
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error>;
|
||||
}
|
||||
|
||||
impl CommandFdInjectionExt for std::process::Command {
|
||||
fn inject_fds(
|
||||
&mut self,
|
||||
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
|
||||
) -> Result<(), error::Error> {
|
||||
let fd_mappings: Vec<FdMapping> = open_files
|
||||
.map(|(child_fd, open_file)| -> Result<FdMapping, error::Error> {
|
||||
let parent_fd = open_file.try_clone_to_owned()?;
|
||||
Ok(FdMapping {
|
||||
child_fd,
|
||||
parent_fd,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.fd_mappings(fd_mappings)
|
||||
.map_err(|_e| error::ErrorKind::ChildCreationFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for arranging for commands to take the foreground.
|
||||
pub trait CommandFgControlExt {
|
||||
/// Arranges for the command to take the foreground when it is executed.
|
||||
fn take_foreground(&mut self);
|
||||
/// Arranges for the command to become a session leader when it is executed.
|
||||
fn lead_session(&mut self);
|
||||
}
|
||||
|
||||
impl CommandFgControlExt for std::process::Command {
|
||||
fn take_foreground(&mut self) {
|
||||
// SAFETY:
|
||||
// This arranges for a provided function to run in the context of
|
||||
// the forked process before it exec's the target command. In general,
|
||||
// rust can't guarantee safety of code running in such a context.
|
||||
unsafe {
|
||||
self.pre_exec(pre_exec_take_foreground);
|
||||
}
|
||||
}
|
||||
|
||||
fn lead_session(&mut self) {
|
||||
// SAFETY:
|
||||
// This arranges for a provided function to run in the context of
|
||||
// the forked process before it exec's the target command. In general,
|
||||
// rust can't guarantee safety of code running in such a context.
|
||||
unsafe {
|
||||
self.pre_exec(pre_exec_lead_session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_exec_take_foreground() -> Result<(), std::io::Error> {
|
||||
use crate::sys;
|
||||
|
||||
sys::terminal::move_self_to_foreground()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pre_exec_lead_session() -> Result<(), std::io::Error> {
|
||||
if let Err(e) = nix::unistd::setsid() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"failed to become session leader: {e}"
|
||||
)));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let control = libc::TIOCSCTTY;
|
||||
#[cfg(target_os = "macos")]
|
||||
let control: u64 = libc::TIOCSCTTY.into();
|
||||
|
||||
// SAFETY:
|
||||
// This is calling a libc function to set the controlling terminal.
|
||||
let result = unsafe { libc::ioctl(0, control, 0) };
|
||||
if result != 0 {
|
||||
return Err(std::io::Error::other("failed to set controlling terminal"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Environment variable retrieval for Unix platforms.
|
||||
|
||||
/// Retrieves environment variables from the host process.
|
||||
///
|
||||
/// On Unix, this is a direct passthrough to [`std::env::vars()`].
|
||||
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
|
||||
std::env::vars()
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! File descriptor utilities.
|
||||
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::{ShellFd, error, openfiles};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "android"))] {
|
||||
const FD_DIR_PATH: &str = "/proc/self/fd";
|
||||
} else if #[cfg(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "macos",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))] {
|
||||
const FD_DIR_PATH: &str = "/dev/fd";
|
||||
} else {
|
||||
/// Returns an iterator over all open file descriptors for the shell.
|
||||
pub fn iter_fds()
|
||||
-> Result<impl Iterator<Item = (ShellFd, openfiles::OpenFile)>, error::Error> {
|
||||
Ok(std::iter::empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a best-effort attempt to iterate over all open file descriptors
|
||||
/// for the current process.
|
||||
///
|
||||
/// If the platform does not support enumerating file descriptors, an empty iterator
|
||||
/// is returned. This function will skip any file descriptors that cannot be opened.
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "android",
|
||||
target_os = "freebsd",
|
||||
target_os = "macos",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
|
||||
std::fs::read_dir(FD_DIR_PATH)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| {
|
||||
let fd: RawFd = entry.file_name().to_str()?.parse().ok()?;
|
||||
|
||||
// SAFETY:
|
||||
// We are trying to open the file descriptor we found listed
|
||||
// in the filesystem, but there's a risk that it's not the same one
|
||||
// that we enumerated or that it's since been closed. For the purposes
|
||||
// of this function, either of those outcomes are acceptable. We
|
||||
// simply skip any fds that we can't open, and the function's purpose
|
||||
// is to make a best-effort attempt to open all available fds.
|
||||
let file = unsafe { open_file_by_fd(fd) }.ok()?;
|
||||
|
||||
Some((fd, file))
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempts to retrieve an `OpenFile` representation for the given already-open file descriptor.
|
||||
///
|
||||
/// If the file descriptor cannot be opened, `None` is returned. Note that there is no guarantee
|
||||
/// that the returned file matches the original file descriptor, as the fd may have been closed
|
||||
/// and potentially re-used in the meantime.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - The file descriptor to open.
|
||||
pub fn try_get_file_for_open_fd(fd: RawFd) -> Option<openfiles::OpenFile> {
|
||||
// SAFETY:
|
||||
// We are trying to open the file descriptor provided by the caller. There's a risk that the fd
|
||||
// is invalid or has been closed since it was enumerated. For the purposes of this function,
|
||||
// we simply return None if we can't open it. There's also a risk that the fd has been closed
|
||||
// and re-used for a different file; again, for the purposes of this function, we accept that
|
||||
// risk and document it as part of the function's contract.
|
||||
unsafe { open_file_by_fd(fd).ok() }
|
||||
}
|
||||
|
||||
unsafe fn open_file_by_fd(fd: RawFd) -> Result<openfiles::OpenFile, error::Error> {
|
||||
// SAFETY: We are creating a BorrowedFd from a file descriptor. Callers typically
|
||||
// enumerate available file descriptors from procfs, devfs, or similar, but there's
|
||||
// still a risk that the fd has become invalid or closed since then -- or that this
|
||||
// function gets used incorrectly.
|
||||
let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
|
||||
let owned_fd = borrowed_fd.try_clone_to_owned()?;
|
||||
Ok(std::fs::File::from(owned_fd).into())
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
//! Filesystem utilities.
|
||||
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
// _PATH_DEFPATH in https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/paths.h
|
||||
const ANDROID_DEFPATH: &str = "/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/apex/com.android.virt/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin";
|
||||
|
||||
impl crate::sys::fs::PathExt for Path {
|
||||
fn readable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::W_OK).is_ok()
|
||||
}
|
||||
|
||||
fn executable(&self) -> bool {
|
||||
nix::unistd::access(self, nix::unistd::AccessFlags::X_OK).is_ok()
|
||||
}
|
||||
|
||||
fn exists_and_is_block_device(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_block_device())
|
||||
}
|
||||
|
||||
fn exists_and_is_char_device(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_char_device())
|
||||
}
|
||||
|
||||
fn exists_and_is_fifo(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft: std::fs::FileType| ft.is_fifo())
|
||||
}
|
||||
|
||||
fn exists_and_is_socket(&self) -> bool {
|
||||
try_get_file_type(self).is_some_and(|ft| ft.is_socket())
|
||||
}
|
||||
|
||||
fn exists_and_is_setgid(&self) -> bool {
|
||||
const S_ISGID: u32 = 0o2000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISGID != 0)
|
||||
}
|
||||
|
||||
fn exists_and_is_setuid(&self) -> bool {
|
||||
const S_ISUID: u32 = 0o4000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISUID != 0)
|
||||
}
|
||||
|
||||
fn exists_and_is_sticky_bit(&self) -> bool {
|
||||
const S_ISVTX: u32 = 0o1000;
|
||||
let file_mode = try_get_file_mode(self);
|
||||
file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
|
||||
}
|
||||
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
|
||||
let metadata = self.metadata()?;
|
||||
Ok((metadata.dev(), metadata.ino()))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
|
||||
path.metadata().map(|metadata| metadata.file_type()).ok()
|
||||
}
|
||||
|
||||
fn try_get_file_mode(path: &Path) -> Option<u32> {
|
||||
path.metadata().map(|metadata| metadata.mode()).ok()
|
||||
}
|
||||
|
||||
/// Splits a platform-specific PATH-like value into individual paths.
|
||||
///
|
||||
/// On Unix, this delegates to [`std::env::split_paths`].
|
||||
pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
|
||||
std::env::split_paths(s)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
std::env::split_paths(ANDROID_DEFPATH).collect()
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
// standard hard-coded defaults for executable search path
|
||||
vec![
|
||||
"/usr/local/sbin".into(),
|
||||
"/usr/local/bin".into(),
|
||||
"/usr/sbin".into(),
|
||||
"/usr/bin".into(),
|
||||
"/sbin".into(),
|
||||
"/bin".into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the platform-specific set of paths that should contain standard system
|
||||
/// utilities. Used by `command -p`, for example.
|
||||
pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
|
||||
//
|
||||
// Try to call confstr(_CS_PATH). If that fails, can't find a string value, or
|
||||
// finds an empty string, then we'll fall back to hard-coded defaults.
|
||||
//
|
||||
|
||||
if let Ok(Some(cs_path)) = confstr_cs_path()
|
||||
&& !cs_path.as_os_str().is_empty()
|
||||
{
|
||||
return split_paths(&cs_path).collect();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
std::env::split_paths(ANDROID_DEFPATH).collect()
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
// standard hard-coded defaults
|
||||
vec![
|
||||
"/bin".into(),
|
||||
"/usr/bin".into(),
|
||||
"/sbin".into(),
|
||||
"/usr/sbin".into(),
|
||||
"/etc".into(),
|
||||
"/usr/etc".into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn confstr_cs_path() -> Result<Option<PathBuf>, std::io::Error> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Ok(Some(PathBuf::from(ANDROID_DEFPATH)))
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let value = confstr(nix::libc::_CS_PATH)?;
|
||||
|
||||
if let Some(value) = value {
|
||||
let value_str = PathBuf::from(value);
|
||||
Ok(Some(value_str))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
|
||||
/// indicates where all the POSIX.2 standard utilities can be found.
|
||||
///
|
||||
/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic way) by nix
|
||||
/// or similar. Until that exists, we accept the need to make the unsafe call directly.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
|
||||
// SAFETY:
|
||||
// Calling `confstr` with a null pointer and size 0 is a documented way to query
|
||||
// the required size of the buffer to hold the value associated with `name`. It
|
||||
// should not end up causing any undefined behavior.
|
||||
let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
|
||||
|
||||
// When confstr returns 0, it either means there's no value associated with _CS_PATH, or
|
||||
// _CS_PATH is considered invalid (and not present) on this platform. In both cases, we
|
||||
// treat it as a non-existent value and return None.
|
||||
if required_size == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut buffer = Vec::<u8>::with_capacity(required_size);
|
||||
|
||||
// SAFETY:
|
||||
// We are calling `confstr` with a valid pointer and size that we obtained from the
|
||||
// allocated buffer. Writing `c_char` (i8 or u8 depending on the platform) into
|
||||
// `Vec<u8>` is fine, as i8 and u8 have compatible representations, and Rust does
|
||||
// not support platforms where `c_char` is not 8-bit wide.
|
||||
let final_size =
|
||||
unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
|
||||
|
||||
if final_size == 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// Per the docs on `confstr`, it *may* return a size larger than the provided buffer.
|
||||
// In our usage we wouldn't expect to see this, as we've first queried the required size.
|
||||
// However, we defensively check for this case and return an error if it happens.
|
||||
if final_size > buffer.capacity() {
|
||||
return Err(std::io::Error::other(
|
||||
"confstr needed more space than advertised",
|
||||
));
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// We are trusting `confstr` to have written exactly `final_size` bytes into the buffer.
|
||||
// We have checked above that it didn't return a value *larger* than the capacity of
|
||||
// the buffer, and also checked for known error cases. Note that the returned length
|
||||
// should include the null terminator.
|
||||
unsafe { buffer.set_len(final_size) };
|
||||
|
||||
// The last byte is a null terminator. We assert that it is.
|
||||
if !matches!(buffer.pop(), Some(0)) {
|
||||
return Err(std::io::Error::other(
|
||||
"confstr did not null-terminate the returned string",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(std::ffi::OsString::from_vec(buffer)))
|
||||
}
|
||||
|
||||
/// Opens a null file that will discard all I/O.
|
||||
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
|
||||
let f = std::fs::File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/null")?;
|
||||
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
|
||||
pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell profile script.
|
||||
pub fn get_system_profile_path() -> Option<&'static Path> {
|
||||
Some(Path::new("/etc/profile"))
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell rc script.
|
||||
pub fn get_system_rc_path() -> Option<&'static Path> {
|
||||
Some(Path::new("/etc/bash.bashrc"))
|
||||
}
|
||||
|
||||
/// Returns true if the string contains a path separator character.
|
||||
///
|
||||
/// On Unix, only `/` is considered a path separator.
|
||||
pub fn contains_path_separator(s: &str) -> bool {
|
||||
s.contains('/')
|
||||
}
|
||||
|
||||
/// Returns true if the string ends with a path separator character.
|
||||
///
|
||||
/// On Unix, only `/` is considered a path separator.
|
||||
pub fn ends_with_path_separator(s: &str) -> bool {
|
||||
s.ends_with('/')
|
||||
}
|
||||
|
||||
/// Returns the string with a trailing path separator removed, if present.
|
||||
///
|
||||
/// On Unix, only `/` is considered a path separator.
|
||||
pub fn strip_path_separator_suffix(s: &str) -> &str {
|
||||
s.strip_suffix('/').unwrap_or(s)
|
||||
}
|
||||
|
||||
/// Returns the platform default for case-insensitive pathname expansion.
|
||||
///
|
||||
/// On Unix, filesystems are typically case-sensitive, so this returns `false`.
|
||||
pub const fn default_case_insensitive_path_expansion() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Finds the byte index of the last path separator in the string.
|
||||
///
|
||||
/// On Unix, only `/` is considered a path separator.
|
||||
pub fn rfind_path_separator(s: &str) -> Option<usize> {
|
||||
s.rfind('/')
|
||||
}
|
||||
|
||||
/// Splits a string on path separator characters, returning an iterator of components.
|
||||
///
|
||||
/// On Unix, only `/` is used as a separator.
|
||||
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
|
||||
s.split('/')
|
||||
}
|
||||
|
||||
/// Returns the root path for an absolute pattern, if the first component indicates one.
|
||||
///
|
||||
/// On Unix, an empty first component (from splitting a path like `/foo`) indicates
|
||||
/// an absolute path rooted at `/`.
|
||||
pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
|
||||
if first_component.is_empty() {
|
||||
Some(PathBuf::from("/"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a component onto a path for pattern expansion.
|
||||
///
|
||||
/// On Unix, this delegates directly to `PathBuf::push`.
|
||||
pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
|
||||
path.push(component);
|
||||
}
|
||||
|
||||
/// Normalizes path separators for shell output.
|
||||
///
|
||||
/// On Unix, this is a no-op since paths already use `/`.
|
||||
pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
|
||||
std::borrow::Cow::Borrowed(s)
|
||||
}
|
||||
|
||||
/// Resolves an owned path to the actual on-disk executable file, if any.
|
||||
///
|
||||
/// On Unix this is a straight passthrough: if the path is executable, the
|
||||
/// path is returned unchanged (no clone). This keeps `pathsearch::next`
|
||||
/// allocation-free on the happy path.
|
||||
///
|
||||
/// On Windows this function may append a `PATHEXT` extension and return a
|
||||
/// possibly-different `PathBuf`.
|
||||
pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
|
||||
use crate::sys::fs::PathExt;
|
||||
if path.as_path().executable() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn path_separator_helpers() {
|
||||
assert!(contains_path_separator("foo/bar"));
|
||||
assert!(!contains_path_separator("foobar"));
|
||||
// Backslashes are not separators on Unix.
|
||||
assert!(!contains_path_separator(r"foo\bar"));
|
||||
|
||||
assert!(ends_with_path_separator("foo/"));
|
||||
assert!(!ends_with_path_separator("foo"));
|
||||
assert!(!ends_with_path_separator(r"foo\"));
|
||||
|
||||
assert_eq!(strip_path_separator_suffix("foo/"), "foo");
|
||||
assert_eq!(strip_path_separator_suffix("foo"), "foo");
|
||||
assert_eq!(strip_path_separator_suffix(r"foo\"), r"foo\");
|
||||
|
||||
assert_eq!(rfind_path_separator("a/b/c"), Some(3));
|
||||
assert_eq!(rfind_path_separator("abc"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_path_for_pattern_basic() {
|
||||
let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
|
||||
assert_eq!(parts, vec!["a", "b", "c"]);
|
||||
|
||||
let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
|
||||
assert_eq!(parts, vec!["", "a", "b"]);
|
||||
|
||||
// Backslashes are not split on Unix.
|
||||
let parts: Vec<_> = split_path_for_pattern(r"a\b").collect();
|
||||
assert_eq!(parts, vec![r"a\b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_path_root_absolute() {
|
||||
assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_path_root_relative() {
|
||||
assert_eq!(pattern_path_root("foo"), None);
|
||||
// Drive-letter syntax is not recognized on Unix.
|
||||
assert_eq!(pattern_path_root("c:"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_path_for_pattern_appends_child() {
|
||||
let mut p = PathBuf::from("/home/reuben");
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
assert_eq!(p, PathBuf::from("/home/reuben/foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_separators_is_noop() {
|
||||
use std::borrow::Cow;
|
||||
assert!(matches!(
|
||||
normalize_path_separators("/foo/bar"),
|
||||
Cow::Borrowed("/foo/bar")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_case_insensitive_is_false() {
|
||||
assert!(!default_case_insensitive_path_expansion());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_executable_returns_input_unchanged() {
|
||||
// /bin/sh exists and is executable on every supported Unix host.
|
||||
let path = PathBuf::from("/bin/sh");
|
||||
let resolved = resolve_executable(path.clone());
|
||||
assert_eq!(resolved.as_deref(), Some(path.as_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_executable_returns_none_for_nonexistent() {
|
||||
let path = PathBuf::from("/this/path/should/not/exist/brush-test");
|
||||
assert!(resolve_executable(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_executable_returns_none_for_non_executable() {
|
||||
// /etc/hostname (or similar) is a regular file but not executable.
|
||||
// Use /etc/passwd which is universally present and not executable.
|
||||
let path = PathBuf::from("/etc/passwd");
|
||||
assert!(resolve_executable(path).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Terminal input utilities
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
use terminfo::capability as cap;
|
||||
|
||||
use crate::{error, interfaces};
|
||||
|
||||
macro_rules! key {
|
||||
( $terminfo:expr , $our_key:expr, $terminfo_key:ty ) => {{
|
||||
(
|
||||
$our_key,
|
||||
$terminfo
|
||||
.get::<$terminfo_key>()
|
||||
.map(|k| k.expand().to_vec()),
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
fn build_terminfo_key_map() -> HashMap<Vec<u8>, interfaces::Key> {
|
||||
let mut map: HashMap<Vec<u8>, interfaces::Key> = HashMap::new();
|
||||
|
||||
if let Ok(ti) = terminfo::Database::from_env() {
|
||||
// Iterate over key capabilities and populate the map
|
||||
let key_capabilities = [
|
||||
key!(ti, interfaces::Key::F(1), cap::KeyF1<'_>),
|
||||
key!(ti, interfaces::Key::F(2), cap::KeyF2<'_>),
|
||||
key!(ti, interfaces::Key::F(3), cap::KeyF3<'_>),
|
||||
key!(ti, interfaces::Key::F(4), cap::KeyF4<'_>),
|
||||
key!(ti, interfaces::Key::F(5), cap::KeyF5<'_>),
|
||||
key!(ti, interfaces::Key::F(6), cap::KeyF6<'_>),
|
||||
key!(ti, interfaces::Key::F(7), cap::KeyF7<'_>),
|
||||
key!(ti, interfaces::Key::F(8), cap::KeyF8<'_>),
|
||||
key!(ti, interfaces::Key::F(9), cap::KeyF9<'_>),
|
||||
key!(ti, interfaces::Key::F(10), cap::KeyF10<'_>),
|
||||
key!(ti, interfaces::Key::F(11), cap::KeyF11<'_>),
|
||||
key!(ti, interfaces::Key::F(12), cap::KeyF12<'_>),
|
||||
key!(ti, interfaces::Key::Backspace, cap::KeyBackspace<'_>),
|
||||
key!(ti, interfaces::Key::Enter, cap::KeyEnter<'_>),
|
||||
key!(ti, interfaces::Key::Left, cap::KeyLeft<'_>),
|
||||
key!(ti, interfaces::Key::Right, cap::KeyRight<'_>),
|
||||
key!(ti, interfaces::Key::Up, cap::KeyUp<'_>),
|
||||
key!(ti, interfaces::Key::Down, cap::KeyDown<'_>),
|
||||
key!(ti, interfaces::Key::Home, cap::KeyHome<'_>),
|
||||
key!(ti, interfaces::Key::End, cap::KeyEnd<'_>),
|
||||
key!(ti, interfaces::Key::PageUp, cap::KeyPPage<'_>),
|
||||
key!(ti, interfaces::Key::PageDown, cap::KeyNPage<'_>),
|
||||
key!(ti, interfaces::Key::BackTab, cap::BackTab<'_>),
|
||||
// It's not clear if these belong here, because they're not
|
||||
// strictly "key" capabilities.
|
||||
key!(ti, interfaces::Key::Up, cap::CursorUp<'_>),
|
||||
key!(ti, interfaces::Key::Down, cap::CursorDown<'_>),
|
||||
key!(ti, interfaces::Key::Left, cap::CursorLeft<'_>),
|
||||
key!(ti, interfaces::Key::Right, cap::CursorRight<'_>),
|
||||
];
|
||||
|
||||
for (key, v) in key_capabilities {
|
||||
if let Some(Ok(v)) = v {
|
||||
map.insert(v.clone(), key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub(crate) static TERMINFO_KEY_MAP: LazyLock<HashMap<Vec<u8>, interfaces::Key>> =
|
||||
LazyLock::new(build_terminfo_key_map);
|
||||
|
||||
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
|
||||
/// if the key code is not recognized.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_code`: The byte sequence representing the key code.
|
||||
pub fn try_get_key_from_key_code(key_code: &[u8]) -> Option<interfaces::Key> {
|
||||
if let Some(key) = TERMINFO_KEY_MAP.get(key_code) {
|
||||
Some(key.clone())
|
||||
} else if key_code.len() == 1 && !key_code[0].is_ascii_control() {
|
||||
Some(interfaces::Key::Character(key_code[0] as char))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
crate::sys::hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//! File descriptor polling utilities for timeout support.
|
||||
|
||||
use std::os::fd::BorrowedFd;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
||||
|
||||
use crate::openfiles::OpenFile;
|
||||
|
||||
/// Polls an open file for input readability with a timeout.
|
||||
///
|
||||
/// Returns `Ok(true)` if data is available for reading, `Ok(false)` if the timeout
|
||||
/// elapsed without data becoming available.
|
||||
///
|
||||
/// For regular files, always returns `Ok(true)` immediately since they're always
|
||||
/// "ready" (matching bash behavior where `-t` has no effect on regular files).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file` - The open file to poll.
|
||||
/// * `timeout` - Maximum time to wait. Use `Duration::ZERO` to check without blocking.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if polling fails or the file descriptor cannot be borrowed.
|
||||
pub fn poll_for_input(file: &OpenFile, timeout: Duration) -> std::io::Result<bool> {
|
||||
let fd = file
|
||||
.try_borrow_as_fd()
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
|
||||
// Regular files are always ready - timeout has no effect (bash behavior).
|
||||
if is_regular_file(fd) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Convert timeout to deadline for accurate time tracking across EINTR retries.
|
||||
let deadline = if timeout.is_zero() {
|
||||
// For zero timeout, use current instant so first check sees zero remaining.
|
||||
Some(Instant::now())
|
||||
} else {
|
||||
Some(Instant::now() + timeout)
|
||||
};
|
||||
|
||||
poll_fd_for_input(fd, deadline)
|
||||
}
|
||||
|
||||
/// Polls a file descriptor for input readability with a deadline.
|
||||
///
|
||||
/// Returns `Ok(true)` if data is available, `Ok(false)` if deadline passed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - File descriptor to poll
|
||||
/// * `deadline` - Optional deadline; `None` indicates no deadline.
|
||||
fn poll_fd_for_input(fd: BorrowedFd<'_>, deadline: Option<Instant>) -> std::io::Result<bool> {
|
||||
let mut poll_fds = [PollFd::new(fd, PollFlags::POLLIN)];
|
||||
let mut first_iteration = true;
|
||||
|
||||
loop {
|
||||
// Calculate remaining time on each iteration to handle EINTR correctly.
|
||||
let timeout_ms = match deadline {
|
||||
Some(d) => {
|
||||
let remaining = d.saturating_duration_since(Instant::now());
|
||||
// On first iteration, always do at least one poll even with zero timeout.
|
||||
// This allows `-t 0` to check if input is immediately available.
|
||||
if remaining.is_zero() && !first_iteration {
|
||||
return Ok(false); // Deadline passed after initial poll.
|
||||
}
|
||||
i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX)
|
||||
}
|
||||
None => -1, // Block indefinitely.
|
||||
};
|
||||
first_iteration = false;
|
||||
let poll_timeout = PollTimeout::try_from(timeout_ms).unwrap_or(PollTimeout::MAX);
|
||||
|
||||
match poll(&mut poll_fds, poll_timeout) {
|
||||
Ok(0) => return Ok(false), // Timeout
|
||||
Ok(_) => {
|
||||
let revents = poll_fds[0].revents().unwrap_or(PollFlags::empty());
|
||||
// POLLIN means data available. POLLHUP/POLLERR without POLLIN means
|
||||
// EOF/error - return true so caller reads and gets the proper result.
|
||||
return Ok(
|
||||
revents.intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR)
|
||||
);
|
||||
}
|
||||
Err(nix::errno::Errno::EINTR) => (), // Retry on signal with recalculated timeout.
|
||||
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a file descriptor refers to a regular file.
|
||||
///
|
||||
/// Regular files are always "ready" for reading (poll has no effect).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - File descriptor to check
|
||||
fn is_regular_file(fd: BorrowedFd<'_>) -> bool {
|
||||
match nix::sys::stat::fstat(fd) {
|
||||
Ok(stat) => {
|
||||
use nix::sys::stat::{SFlag, mode_t};
|
||||
mode_t::try_from(stat.st_mode)
|
||||
.is_ok_and(|mode| SFlag::from_bits_truncate(mode).contains(SFlag::S_IFREG))
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Resource utilities
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Returns the user and system CPU time used by the current process;
|
||||
/// expressed as a tuple containing user time and system time, in that order.
|
||||
pub fn get_self_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF)?;
|
||||
Ok((
|
||||
convert_rusage_time(usage.user_time()),
|
||||
convert_rusage_time(usage.system_time()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the user and system CPU time used by child processes; expressed
|
||||
/// as a tuple containing user time and system time, in that order.
|
||||
pub fn get_children_user_and_system_time()
|
||||
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
|
||||
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_CHILDREN)?;
|
||||
Ok((
|
||||
convert_rusage_time(usage.user_time()),
|
||||
convert_rusage_time(usage.system_time()),
|
||||
))
|
||||
}
|
||||
|
||||
const fn convert_rusage_time(time: nix::sys::time::TimeVal) -> std::time::Duration {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
std::time::Duration::new(time.tv_sec() as u64, time.tv_usec() as u32 * 1000)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Signal processing utilities
|
||||
|
||||
use crate::{error, sys, traps};
|
||||
|
||||
pub(crate) use nix::sys::signal::Signal;
|
||||
|
||||
pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGCONT)
|
||||
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a signal to a specific process.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pid` - The process ID to send the signal to
|
||||
/// * `signal` - The signal to send (must be a real signal, not a trap signal)
|
||||
pub fn kill_process(
|
||||
pid: sys::process::ProcessId,
|
||||
signal: traps::TrapSignal,
|
||||
) -> Result<(), error::Error> {
|
||||
let translated_signal = match signal {
|
||||
traps::TrapSignal::Signal(signal) => signal,
|
||||
traps::TrapSignal::Debug
|
||||
| traps::TrapSignal::Err
|
||||
| traps::TrapSignal::Exit
|
||||
| traps::TrapSignal::Return => {
|
||||
return Err(error::ErrorKind::InvalidSignal(signal.to_string()).into());
|
||||
}
|
||||
};
|
||||
|
||||
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), translated_signal)
|
||||
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
|
||||
nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tstp_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
|
||||
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::from_raw(
|
||||
nix::libc::SIGTSTP,
|
||||
))?;
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
pub(crate) fn chld_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
|
||||
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::child())?;
|
||||
Ok(signal)
|
||||
}
|
||||
|
||||
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
|
||||
|
||||
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
|
||||
let ignore = nix::sys::signal::SigAction::new(
|
||||
nix::sys::signal::SigHandler::SigIgn,
|
||||
nix::sys::signal::SaFlags::empty(),
|
||||
nix::sys::signal::SigSet::empty(),
|
||||
);
|
||||
|
||||
// SAFETY:
|
||||
// Setting the signal action should be safe here. The unsafe concerns
|
||||
// for calling `sigaction` are primarily around ensuring that any provided
|
||||
// signal handler functions are only performing operations that are
|
||||
// safe to do in a signal handler context. Here we are not providing
|
||||
// a custom handler, just asking the OS to ignore the signal.
|
||||
unsafe { nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGTTOU, &ignore) }?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
|
||||
let mut found_stopped = false;
|
||||
|
||||
loop {
|
||||
let wait_status = waitid_all(
|
||||
nix::sys::wait::WaitPidFlag::WUNTRACED | nix::sys::wait::WaitPidFlag::WNOHANG,
|
||||
);
|
||||
match wait_status {
|
||||
Ok(nix::sys::wait::WaitStatus::Stopped(_stopped_pid, _signal)) => {
|
||||
found_stopped = true;
|
||||
}
|
||||
Ok(_) => break,
|
||||
Err(nix::errno::Errno::ECHILD) => break,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found_stopped)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn waitid_all(
|
||||
flags: nix::sys::wait::WaitPidFlag,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
nix::sys::wait::waitid(nix::sys::wait::Id::All, flags)
|
||||
}
|
||||
|
||||
//
|
||||
// N.B. These functions were mostly copied from nix::sys::wait (https://github.com/nix-rust/nix, MIT license)
|
||||
// to enable use of the `waitid` call on macOS. Ideally nix would expose it on macOS and we would
|
||||
// remove this code.
|
||||
//
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn waitid_all(
|
||||
flags: nix::sys::wait::WaitPidFlag,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
// The siginfo structure is valid when filled with zeroes. Memory is zeroed
|
||||
// rather than uninitialized, as not all platforms initialize the memory in
|
||||
// the StillAlive case.
|
||||
let mut siginfo: nix::libc::siginfo_t = unsafe { std::mem::zeroed() };
|
||||
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
nix::errno::Errno::result(unsafe {
|
||||
nix::libc::waitid(nix::libc::P_ALL, 0, &raw mut siginfo, flags.bits())
|
||||
})?;
|
||||
|
||||
siginfo_to_wait_status(siginfo)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn siginfo_to_wait_status(
|
||||
siginfo: nix::libc::siginfo_t,
|
||||
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
let si_pid = unsafe { siginfo.si_pid() };
|
||||
if si_pid == 0 {
|
||||
return Ok(nix::sys::wait::WaitStatus::StillAlive);
|
||||
}
|
||||
|
||||
let pid = nix::unistd::Pid::from_raw(si_pid);
|
||||
|
||||
// SAFETY:
|
||||
// Code copied from nix::sys::wait implementation of waitid for other platforms.
|
||||
let si_status = unsafe { siginfo.si_status() };
|
||||
|
||||
let status = match siginfo.si_code {
|
||||
nix::libc::CLD_EXITED => nix::sys::wait::WaitStatus::Exited(pid, si_status),
|
||||
nix::libc::CLD_KILLED | nix::libc::CLD_DUMPED => nix::sys::wait::WaitStatus::Signaled(
|
||||
pid,
|
||||
nix::sys::signal::Signal::try_from(si_status)?,
|
||||
siginfo.si_code == nix::libc::CLD_DUMPED,
|
||||
),
|
||||
nix::libc::CLD_STOPPED => {
|
||||
nix::sys::wait::WaitStatus::Stopped(pid, nix::sys::signal::Signal::try_from(si_status)?)
|
||||
}
|
||||
nix::libc::CLD_CONTINUED => nix::sys::wait::WaitStatus::Continued(pid),
|
||||
_ => return Err(nix::errno::Errno::EINVAL),
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//! Terminal utilities.
|
||||
|
||||
use crate::{error, openfiles, sys, terminal};
|
||||
use std::{io::IsTerminal, os::fd::AsFd, path::PathBuf};
|
||||
|
||||
/// Terminal configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
termios: nix::sys::termios::Termios,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
|
||||
/// with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file` - A reference to the open terminal.
|
||||
pub fn from_term(file: &openfiles::OpenFile) -> Result<Self, error::Error> {
|
||||
let fd = file.try_borrow_as_fd()?;
|
||||
let termios = nix::sys::termios::tcgetattr(fd)?;
|
||||
Ok(Self { termios })
|
||||
}
|
||||
|
||||
/// Applies the terminal settings to the terminal associated with the given file descriptor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file` - A reference to the open terminal.
|
||||
pub fn apply_to_term(&self, file: &openfiles::OpenFile) -> Result<(), error::Error> {
|
||||
let fd = file.try_borrow_as_fd()?;
|
||||
nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies the given high-level terminal settings to this configuration. Does not modify any
|
||||
/// terminal itself.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `settings` - The high-level terminal settings to apply to this configuration.
|
||||
pub fn update(&mut self, settings: &terminal::Settings) {
|
||||
if let Some(echo_input) = &settings.echo_input {
|
||||
if *echo_input {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ECHO;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ECHO;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(line_input) = &settings.line_input {
|
||||
if *line_input {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ICANON;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ICANON;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(interrupt_signals) = &settings.interrupt_signals {
|
||||
if *interrupt_signals {
|
||||
self.termios.local_flags |= nix::sys::termios::LocalFlags::ISIG;
|
||||
} else {
|
||||
self.termios.local_flags -= nix::sys::termios::LocalFlags::ISIG;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output_nl_as_nlcr) = &settings.output_nl_as_nlcr {
|
||||
if *output_nl_as_nlcr {
|
||||
self.termios.output_flags |=
|
||||
nix::sys::termios::OutputFlags::OPOST | nix::sys::termios::OutputFlags::ONLCR;
|
||||
} else {
|
||||
self.termios.output_flags -= nix::sys::termios::OutputFlags::ONLCR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the process ID of this process's parent.
|
||||
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
|
||||
Some(nix::unistd::getppid().as_raw())
|
||||
}
|
||||
|
||||
/// Get the process group ID for this process's process group.
|
||||
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
|
||||
Some(nix::unistd::getpgrp().as_raw())
|
||||
}
|
||||
|
||||
/// Get the foreground process ID of the attached terminal.
|
||||
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
|
||||
nix::unistd::tcgetpgrp(std::io::stdin())
|
||||
.ok()
|
||||
.map(|pgid| pgid.as_raw())
|
||||
}
|
||||
|
||||
/// Move the specified process to the foreground of the attached terminal.
|
||||
pub fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
|
||||
nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the current process to the foreground of the attached terminal.
|
||||
// This function needs to return `std::io::Error` so that the OS error code can be recovered.
|
||||
pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
|
||||
if std::io::stdin().is_terminal() {
|
||||
let pgid = nix::unistd::getpgid(None)?;
|
||||
|
||||
// TODO(jobs): This sometimes fails with ENOTTY even though we checked that stdin is a
|
||||
// terminal. We should investigate why this is happening.
|
||||
let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to get the path of the terminal device associated with the attached terminal.
|
||||
/// Returns `None` if there is no terminal attached or the lookup failed.
|
||||
pub fn try_get_terminal_device_path() -> Option<PathBuf> {
|
||||
nix::unistd::ttyname(std::io::stdin()).ok()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use crate::{error, trace_categories};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use uzers::os::unix::UserExt;
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
uzers::get_current_uid() == 0
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_home_dir(username: &str) -> Option<PathBuf> {
|
||||
if let Some(user_info) = uzers::get_user_by_name(username) {
|
||||
return Some(user_info.home_dir().to_path_buf());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
if let Some(username) = uzers::get_current_username()
|
||||
&& let Some(user_info) = uzers::get_user_by_name(&username)
|
||||
{
|
||||
return Some(user_info.home_dir().to_path_buf());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
|
||||
if let Some(username) = uzers::get_current_username()
|
||||
&& let Some(user_info) = uzers::get_user_by_name(&username)
|
||||
{
|
||||
return Some(user_info.shell().to_path_buf());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_current_uid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_current_gid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_effective_uid())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Ok(uzers::get_effective_gid())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
|
||||
Ok(username.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
let groups = get_current_user_groups()?;
|
||||
Ok(groups.into_iter().map(|g| g.gid()).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(#475): uzers::all_users() is available but unsafe; for now we just return the current
|
||||
// user. That's better than nothing.
|
||||
let user = get_current_username()?;
|
||||
Ok(vec![user])
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(#475): uzers::all_groups() is available but unsafe; for now we just return the current
|
||||
// user's groups. That's better than nothing.
|
||||
let groups = get_current_user_groups()?;
|
||||
let group_names = groups
|
||||
.into_iter()
|
||||
.map(|g| g.name().to_string_lossy().to_string());
|
||||
Ok(group_names.collect())
|
||||
}
|
||||
|
||||
fn get_current_user_groups() -> Result<Vec<uzers::Group>, error::Error> {
|
||||
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
|
||||
let gid = uzers::get_current_gid();
|
||||
let groups = uzers::get_user_groups(&username, gid).unwrap_or_default();
|
||||
Ok(groups)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Filesystem utilities for WASM.
|
||||
|
||||
pub use crate::sys::stubs::fs::*;
|
||||
|
||||
impl crate::sys::fs::PathExt for std::path::Path {
|
||||
fn readable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn executable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn exists_and_is_block_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_char_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_fifo(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_socket(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setgid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setuid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_sticky_bit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
|
||||
Ok((0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits a PATH-like value into individual paths.
|
||||
///
|
||||
/// On WASM, `std::env::split_paths` is not available, so this
|
||||
/// implementation splits by the `:` separator.
|
||||
pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(
|
||||
s: &T,
|
||||
) -> impl Iterator<Item = std::path::PathBuf> {
|
||||
s.as_ref()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.split(':')
|
||||
.map(std::path::PathBuf::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
pub use crate::sys::stubs::async_pipe;
|
||||
pub use crate::sys::stubs::commands;
|
||||
pub(crate) use crate::sys::stubs::env;
|
||||
pub use crate::sys::stubs::fd;
|
||||
pub(crate) mod fs;
|
||||
pub use crate::sys::stubs::input;
|
||||
pub(crate) use crate::sys::stubs::network;
|
||||
pub(crate) use crate::sys::stubs::pipes;
|
||||
pub use crate::sys::stubs::poll;
|
||||
pub use crate::sys::stubs::process;
|
||||
pub use crate::sys::stubs::resource;
|
||||
pub use crate::sys::stubs::signal;
|
||||
pub use crate::sys::stubs::terminal;
|
||||
pub(crate) use crate::sys::stubs::users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {}
|
||||
@@ -0,0 +1,23 @@
|
||||
pub use crate::sys::stubs::async_pipe;
|
||||
pub use crate::sys::stubs::commands;
|
||||
pub(crate) mod env;
|
||||
pub use crate::sys::stubs::fd;
|
||||
pub(crate) mod fs;
|
||||
pub use crate::sys::stubs::input;
|
||||
pub(crate) mod network;
|
||||
pub use crate::sys::stubs::poll;
|
||||
pub use crate::sys::stubs::resource;
|
||||
|
||||
/// Signal processing utilities
|
||||
pub mod signal {
|
||||
pub(crate) use crate::sys::stubs::signal::*;
|
||||
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
|
||||
}
|
||||
|
||||
pub use crate::sys::stubs::terminal;
|
||||
pub use crate::sys::tokio_process as process;
|
||||
pub(crate) mod users;
|
||||
|
||||
/// Platform-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PlatformError {}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Environment variable retrieval for Windows.
|
||||
//!
|
||||
//! On Windows, well-known environment variable names are normalized to their
|
||||
//! canonical POSIX forms (e.g. `Path` → `PATH`), and `HOME` is synthesized
|
||||
//! from `USERPROFILE` or `HOMEDRIVE`+`HOMEPATH` if not already present.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Retrieves environment variables from the host process, applying
|
||||
/// Windows-specific fixups.
|
||||
///
|
||||
/// Normalizes well-known variable names to POSIX conventions, copies
|
||||
/// `TEMP`/`TMP` to `TMPDIR` (preserving originals for native Windows apps),
|
||||
/// and synthesizes `HOME` if it is not natively defined.
|
||||
///
|
||||
/// A [`BTreeMap`] is used (rather than `HashMap`) so iteration order is
|
||||
/// deterministic across runs. If two source variables collide under the same
|
||||
/// canonical name (e.g. both `Path` and `PATH` are set to different values),
|
||||
/// the conflict is logged and the last-seen value wins.
|
||||
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
|
||||
collect_host_env_vars(std::env::vars())
|
||||
}
|
||||
|
||||
/// Collects and normalizes a set of environment variables. Exposed as a
|
||||
/// pure function (taking the source iterator) so it can be unit-tested
|
||||
/// without touching the process environment.
|
||||
fn collect_host_env_vars<I>(source: I) -> std::collections::btree_map::IntoIter<String, String>
|
||||
where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
let mut vars = BTreeMap::new();
|
||||
|
||||
// Normalize host env vars and inject them into the map.
|
||||
for (k, v) in source {
|
||||
let normalized = normalize_env_name(&k);
|
||||
if let Some(existing) = vars.get(&normalized)
|
||||
&& existing != &v
|
||||
{
|
||||
tracing::warn!(
|
||||
"environment variable collision under canonical name {normalized}: \
|
||||
two different values were supplied (last-write wins)"
|
||||
);
|
||||
}
|
||||
vars.insert(normalized, v);
|
||||
}
|
||||
|
||||
// Synthesize HOME from Windows-native variables if not already present.
|
||||
if !vars.contains_key("HOME") {
|
||||
let home = vars.get("USERPROFILE").cloned().or_else(|| {
|
||||
let d = vars.get("HOMEDRIVE")?;
|
||||
let p = vars.get("HOMEPATH")?;
|
||||
Some(format!("{d}{p}"))
|
||||
});
|
||||
if let Some(home) = home {
|
||||
vars.insert("HOME".to_string(), home);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy TEMP/TMP to TMPDIR if TMPDIR doesn't already exist.
|
||||
if !vars.contains_key("TMPDIR")
|
||||
&& let Some(tmp) = vars.get("TEMP").or_else(|| vars.get("TMP")).cloned()
|
||||
{
|
||||
vars.insert("TMPDIR".to_string(), tmp);
|
||||
}
|
||||
|
||||
vars.into_iter()
|
||||
}
|
||||
|
||||
/// Normalizes the case of well-known environment variable names to their
|
||||
/// canonical POSIX forms (`Path` → `PATH`, `home` → `HOME`).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The environment variable name to normalize.
|
||||
fn normalize_env_name(name: &str) -> String {
|
||||
// Normalize well-known variable names so that later lookups by
|
||||
// canonical (uppercase) spelling always succeed regardless of the
|
||||
// host's original casing.
|
||||
const WELL_KNOWN: &[&str] = &[
|
||||
"PATH",
|
||||
"HOME",
|
||||
"USERPROFILE",
|
||||
"HOMEDRIVE",
|
||||
"HOMEPATH",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"TMPDIR",
|
||||
];
|
||||
|
||||
for &canonical in WELL_KNOWN {
|
||||
if name.eq_ignore_ascii_case(canonical) {
|
||||
return canonical.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(source: &[(&str, &str)]) -> BTreeMap<String, String> {
|
||||
collect_host_env_vars(
|
||||
source
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), (*v).to_string())),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_env_name_canonicalizes_well_known() {
|
||||
assert_eq!(normalize_env_name("Path"), "PATH");
|
||||
assert_eq!(normalize_env_name("path"), "PATH");
|
||||
assert_eq!(normalize_env_name("PATH"), "PATH");
|
||||
assert_eq!(normalize_env_name("Home"), "HOME");
|
||||
assert_eq!(normalize_env_name("UserProfile"), "USERPROFILE");
|
||||
assert_eq!(normalize_env_name("Temp"), "TEMP");
|
||||
assert_eq!(normalize_env_name("Tmp"), "TMP");
|
||||
assert_eq!(normalize_env_name("TmpDir"), "TMPDIR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_env_name_leaves_unknown_alone() {
|
||||
assert_eq!(normalize_env_name("FOO"), "FOO");
|
||||
assert_eq!(normalize_env_name("myVar"), "myVar");
|
||||
// Does not uppercase unknown names.
|
||||
assert_eq!(normalize_env_name("AppData"), "AppData");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthesizes_home_from_userprofile() {
|
||||
let vars = run(&[("UserProfile", r"C:\Users\reuben")]);
|
||||
assert_eq!(
|
||||
vars.get("HOME").map(String::as_str),
|
||||
Some(r"C:\Users\reuben")
|
||||
);
|
||||
assert_eq!(
|
||||
vars.get("USERPROFILE").map(String::as_str),
|
||||
Some(r"C:\Users\reuben")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthesizes_home_from_homedrive_homepath_when_no_userprofile() {
|
||||
let vars = run(&[("HomeDrive", "C:"), ("HomePath", r"\Users\reuben")]);
|
||||
assert_eq!(
|
||||
vars.get("HOME").map(String::as_str),
|
||||
Some(r"C:\Users\reuben")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_home() {
|
||||
let vars = run(&[("HOME", "/already/set"), ("UserProfile", r"C:\Users\other")]);
|
||||
assert_eq!(vars.get("HOME").map(String::as_str), Some("/already/set"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_temp_to_tmpdir() {
|
||||
let vars = run(&[("Temp", r"C:\Windows\Temp")]);
|
||||
assert_eq!(
|
||||
vars.get("TMPDIR").map(String::as_str),
|
||||
Some(r"C:\Windows\Temp")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_temp_over_tmp_for_tmpdir() {
|
||||
let vars = run(&[("TEMP", "one"), ("TMP", "two")]);
|
||||
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_tmp_when_no_temp() {
|
||||
let vars = run(&[("TMP", "two")]);
|
||||
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_existing_tmpdir() {
|
||||
let vars = run(&[("TMPDIR", "original"), ("TEMP", "other")]);
|
||||
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("original"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_iteration_order() {
|
||||
// BTreeMap iteration order is determined by the sorted key order,
|
||||
// so the outputs must be identical across invocations regardless of
|
||||
// input ordering.
|
||||
let a = run(&[("Path", "first"), ("ZETA", "zz"), ("Alpha", "aa")]);
|
||||
let b = run(&[("ZETA", "zz"), ("Alpha", "aa"), ("Path", "first")]);
|
||||
let keys_a: Vec<_> = a.keys().collect();
|
||||
let keys_b: Vec<_> = b.keys().collect();
|
||||
assert_eq!(keys_a, keys_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_last_write_wins() {
|
||||
// When two source names normalize to the same canonical key,
|
||||
// the later one should overwrite the earlier one (and it should not
|
||||
// panic).
|
||||
let vars = run(&[("Path", "first"), ("PATH", "second")]);
|
||||
assert_eq!(vars.get("PATH").map(String::as_str), Some("second"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
//! Filesystem utilities for Windows.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::error;
|
||||
|
||||
// Selectively re-export items from stubs that we don't override.
|
||||
pub(crate) use crate::sys::stubs::fs::MetadataExt;
|
||||
|
||||
/// Cached list of executable extensions from the `PATHEXT` environment
|
||||
/// variable. Each entry retains its leading dot (e.g. `".exe"`) and is stored
|
||||
/// lowercased so case-insensitive comparisons can be done without allocating.
|
||||
///
|
||||
/// NOTE: This is cached for the process lifetime. Changes to `PATHEXT` made
|
||||
/// inside the running shell are not reflected here. Bash itself has no
|
||||
/// `PATHEXT` semantics, so this is generally acceptable for now.
|
||||
static PATHEXT_EXTENSIONS: LazyLock<Vec<String>> = LazyLock::new(|| {
|
||||
std::env::var("PATHEXT")
|
||||
.unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string())
|
||||
.split(';')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
.collect()
|
||||
});
|
||||
|
||||
/// Returns the stem of a PATHEXT entry (with any leading `.` removed).
|
||||
///
|
||||
/// `PATHEXT` canonically stores entries like `.EXE`, but tolerant parsing
|
||||
/// accepts entries without the leading dot too.
|
||||
fn pathext_entry_stem(entry: &str) -> &str {
|
||||
entry.strip_prefix('.').unwrap_or(entry)
|
||||
}
|
||||
|
||||
/// Returns true if the path's extension is in the PATHEXT list.
|
||||
///
|
||||
/// Performs case-insensitive comparison against the cached PATHEXT entries
|
||||
/// without allocating.
|
||||
fn has_executable_extension(path: &Path) -> bool {
|
||||
path.extension().is_some_and(|ext| {
|
||||
PATHEXT_EXTENSIONS
|
||||
.iter()
|
||||
.any(|e| ext.eq_ignore_ascii_case(pathext_entry_stem(e)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if `path` is, by itself, an existing executable file.
|
||||
///
|
||||
/// Used both for the initial check in [`resolve_executable`] and for
|
||||
/// [`PathExt::executable`].
|
||||
fn is_executable_file(path: &Path) -> bool {
|
||||
has_executable_extension(path) && path.is_file()
|
||||
}
|
||||
|
||||
/// Resolves an owned path to the actual on-disk executable file, if any.
|
||||
///
|
||||
/// If the path is already a file with a `PATHEXT` extension, it is returned
|
||||
/// unchanged (no allocation). Otherwise, each `PATHEXT` extension is appended
|
||||
/// in turn and the first existing file is returned.
|
||||
pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
|
||||
if is_executable_file(&path) {
|
||||
return Some(path);
|
||||
}
|
||||
// Try appending each PATHEXT extension.
|
||||
for ext in PATHEXT_EXTENSIONS.iter() {
|
||||
let mut name = path.as_os_str().to_owned();
|
||||
name.push(ext);
|
||||
let candidate = PathBuf::from(name);
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl crate::sys::fs::PathExt for Path {
|
||||
fn readable(&self) -> bool {
|
||||
self.exists()
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
self.metadata().is_ok_and(|m| !m.permissions().readonly())
|
||||
}
|
||||
|
||||
fn executable(&self) -> bool {
|
||||
if is_executable_file(self) {
|
||||
return true;
|
||||
}
|
||||
// Try each PATHEXT extension without allocating a separate PathBuf
|
||||
// per candidate until one exists.
|
||||
PATHEXT_EXTENSIONS.iter().any(|ext| {
|
||||
let mut name = self.as_os_str().to_owned();
|
||||
name.push(ext);
|
||||
Self::new(&name).is_file()
|
||||
})
|
||||
}
|
||||
|
||||
fn exists_and_is_block_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_char_device(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_fifo(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_socket(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setgid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_setuid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn exists_and_is_sticky_bit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
|
||||
// TODO(windows): implement using file index / volume serial number.
|
||||
Err(error::ErrorKind::NotSupportedOnThisPlatform("get_device_and_inode").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits a platform-specific PATH-like value into individual paths.
|
||||
///
|
||||
/// On Windows, this delegates to [`std::env::split_paths`].
|
||||
pub fn split_paths<T: AsRef<OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
|
||||
std::env::split_paths(s)
|
||||
}
|
||||
|
||||
/// Opens a null file that will discard all I/O.
|
||||
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
|
||||
let f = std::fs::File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("NUL")?;
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
|
||||
pub fn try_open_special_file(path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
|
||||
if path.ends_with("dev/null") && path.is_absolute() {
|
||||
Some(open_null_file().map_err(std::io::Error::other))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default paths where executables are typically found on Windows.
|
||||
pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
|
||||
default_system_paths()
|
||||
}
|
||||
|
||||
/// Returns the default paths where standard system utilities are found on Windows.
|
||||
pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
|
||||
default_system_paths()
|
||||
}
|
||||
|
||||
fn default_system_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
if let Ok(sysroot) = std::env::var("SystemRoot") {
|
||||
paths.push(PathBuf::from(&sysroot).join("system32"));
|
||||
paths.push(PathBuf::from(&sysroot));
|
||||
paths.push(PathBuf::from(&sysroot).join("System32").join("Wbem"));
|
||||
paths.push(
|
||||
PathBuf::from(&sysroot)
|
||||
.join("System32")
|
||||
.join("WindowsPowerShell")
|
||||
.join("v1.0"),
|
||||
);
|
||||
}
|
||||
if let Ok(userprofile) = std::env::var("USERPROFILE") {
|
||||
paths.push(
|
||||
PathBuf::from(userprofile)
|
||||
.join("AppData")
|
||||
.join("Local")
|
||||
.join("Microsoft")
|
||||
.join("WindowsApps"),
|
||||
);
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell profile script.
|
||||
///
|
||||
/// On Windows, no system profile is loaded by default.
|
||||
pub const fn get_system_profile_path() -> Option<&'static Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the path to the system-wide shell rc script.
|
||||
///
|
||||
/// On Windows, no system rc file is loaded by default.
|
||||
pub const fn get_system_rc_path() -> Option<&'static Path> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the platform default for case-insensitive pathname expansion.
|
||||
///
|
||||
/// On Windows, filesystems are typically case-insensitive, so this returns `true`.
|
||||
pub const fn default_case_insensitive_path_expansion() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Path separator characters on Windows.
|
||||
const PATH_SEPARATORS: [char; 2] = ['/', '\\'];
|
||||
|
||||
/// Returns true if the string contains a path separator character.
|
||||
///
|
||||
/// On Windows, both `/` and `\` are considered path separators.
|
||||
pub fn contains_path_separator(s: &str) -> bool {
|
||||
s.contains(PATH_SEPARATORS)
|
||||
}
|
||||
|
||||
/// Returns true if the string ends with a path separator character.
|
||||
///
|
||||
/// On Windows, both `/` and `\` are considered path separators.
|
||||
pub fn ends_with_path_separator(s: &str) -> bool {
|
||||
s.ends_with(PATH_SEPARATORS)
|
||||
}
|
||||
|
||||
/// Returns the string with a trailing path separator removed, if present.
|
||||
///
|
||||
/// On Windows, both `/` and `\` are considered path separators.
|
||||
pub fn strip_path_separator_suffix(s: &str) -> &str {
|
||||
s.strip_suffix(PATH_SEPARATORS).unwrap_or(s)
|
||||
}
|
||||
|
||||
/// Finds the byte index of the last path separator in the string.
|
||||
///
|
||||
/// On Windows, both `/` and `\` are considered path separators.
|
||||
pub fn rfind_path_separator(s: &str) -> Option<usize> {
|
||||
s.rfind(PATH_SEPARATORS)
|
||||
}
|
||||
|
||||
/// Splits a string on path separator characters, returning an iterator of components.
|
||||
///
|
||||
/// On Windows, both `/` and `\` are used as separators.
|
||||
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
|
||||
s.split(PATH_SEPARATORS)
|
||||
}
|
||||
|
||||
/// Returns the root path for an absolute pattern, if the first component indicates one.
|
||||
///
|
||||
/// On Windows, recognizes both a leading separator (empty first component from splitting
|
||||
/// a path like `/foo`) and a drive-letter prefix like `C:` as absolute.
|
||||
///
|
||||
/// TODO(windows): UNC paths like `\\server\share\foo` are not yet handled
|
||||
/// specially; they split into `["", "", "server", "share", "foo"]`, and the
|
||||
/// leading empty component causes them to be treated as if they were rooted
|
||||
/// at `/`, which drops the server/share portion. Supporting UNC requires
|
||||
/// peeking further into the component list.
|
||||
pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
|
||||
if first_component.is_empty() {
|
||||
// Leading separator, e.g. `/foo` split into ["", "foo"].
|
||||
Some(PathBuf::from("/"))
|
||||
} else if first_component.len() == 2
|
||||
&& first_component.as_bytes()[0].is_ascii_alphabetic()
|
||||
&& first_component.as_bytes()[1] == b':'
|
||||
{
|
||||
// Drive letter prefix, e.g. `c:/foo` split into ["c:", "foo"].
|
||||
let mut root = String::with_capacity(3);
|
||||
root.push_str(first_component);
|
||||
root.push('/');
|
||||
Some(PathBuf::from(root))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a component onto a path for pattern expansion.
|
||||
///
|
||||
/// On Windows, `PathBuf::push` has special drive-letter and root-replacement
|
||||
/// semantics that conflict with shell path construction (e.g. pushing `C:foo`
|
||||
/// onto `D:\bar` replaces the whole path). This function always appends the
|
||||
/// component as a child, operating on the underlying `OsString` so non-UTF-8
|
||||
/// content in the path is preserved and no reallocation is needed.
|
||||
pub fn push_path_for_pattern(path: &mut PathBuf, component: &str) {
|
||||
// Separator characters are ASCII, and WTF-8-encoded OsStr bytes are a
|
||||
// superset of UTF-8, so checking the last byte directly is safe.
|
||||
let bytes = path.as_os_str().as_encoded_bytes();
|
||||
let needs_sep = !bytes.is_empty() && !matches!(bytes.last(), Some(b'/' | b'\\'));
|
||||
|
||||
let buf = path.as_mut_os_string();
|
||||
if needs_sep {
|
||||
buf.push("/");
|
||||
}
|
||||
buf.push(component);
|
||||
}
|
||||
|
||||
/// Normalizes path separators for shell output.
|
||||
///
|
||||
/// On Windows, replaces `\` with `/` since backslash is the shell escape character.
|
||||
pub fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
|
||||
if s.contains('\\') {
|
||||
std::borrow::Cow::Owned(s.replace('\\', "/"))
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn path_separator_helpers_both_slashes() {
|
||||
assert!(contains_path_separator("foo/bar"));
|
||||
assert!(contains_path_separator(r"foo\bar"));
|
||||
assert!(contains_path_separator(r"mixed/and\back"));
|
||||
assert!(!contains_path_separator("foobar"));
|
||||
|
||||
assert!(ends_with_path_separator("foo/"));
|
||||
assert!(ends_with_path_separator(r"foo\"));
|
||||
assert!(!ends_with_path_separator("foo"));
|
||||
|
||||
assert_eq!(strip_path_separator_suffix("foo/"), "foo");
|
||||
assert_eq!(strip_path_separator_suffix(r"foo\"), "foo");
|
||||
assert_eq!(strip_path_separator_suffix("foo"), "foo");
|
||||
|
||||
assert_eq!(rfind_path_separator("a/b/c"), Some(3));
|
||||
assert_eq!(rfind_path_separator(r"a\b\c"), Some(3));
|
||||
assert_eq!(rfind_path_separator(r"a/b\c"), Some(3));
|
||||
assert_eq!(rfind_path_separator("abc"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_path_for_pattern_both_slashes() {
|
||||
let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
|
||||
assert_eq!(parts, vec!["a", "b", "c"]);
|
||||
|
||||
let parts: Vec<_> = split_path_for_pattern(r"a\b\c").collect();
|
||||
assert_eq!(parts, vec!["a", "b", "c"]);
|
||||
|
||||
let parts: Vec<_> = split_path_for_pattern(r"a/b\c").collect();
|
||||
assert_eq!(parts, vec!["a", "b", "c"]);
|
||||
|
||||
let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
|
||||
assert_eq!(parts, vec!["", "a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_path_root_leading_separator() {
|
||||
assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_path_root_drive_letters() {
|
||||
assert_eq!(pattern_path_root("c:"), Some(PathBuf::from("c:/")));
|
||||
assert_eq!(pattern_path_root("C:"), Some(PathBuf::from("C:/")));
|
||||
assert_eq!(pattern_path_root("Z:"), Some(PathBuf::from("Z:/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_path_root_rejects_non_drive_two_char_prefix() {
|
||||
// "1:" is not a valid drive letter — must be alphabetic.
|
||||
assert_eq!(pattern_path_root("1:"), None);
|
||||
// Longer drive-like strings are not treated as roots.
|
||||
assert_eq!(pattern_path_root("cd"), None);
|
||||
assert_eq!(pattern_path_root("c:\\"), None);
|
||||
assert_eq!(pattern_path_root("foo"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_path_for_pattern_appends_with_forward_slash() {
|
||||
let mut p = PathBuf::from(r"C:\Users\reuben");
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
// Forward slash is used as the appended separator, yielding mixed
|
||||
// separators — acceptable because `normalize_path_separators` is
|
||||
// applied downstream before display.
|
||||
assert_eq!(p, PathBuf::from(r"C:\Users\reuben/foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_path_for_pattern_no_double_separator() {
|
||||
let mut p = PathBuf::from("C:/Users/reuben/");
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
assert_eq!(p, PathBuf::from("C:/Users/reuben/foo"));
|
||||
|
||||
let mut p = PathBuf::from(r"C:\Users\reuben\");
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
assert_eq!(p, PathBuf::from(r"C:\Users\reuben\foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_path_for_pattern_onto_drive_root() {
|
||||
let mut p = PathBuf::from("c:/");
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
assert_eq!(p, PathBuf::from("c:/foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_path_for_pattern_onto_empty() {
|
||||
let mut p = PathBuf::new();
|
||||
push_path_for_pattern(&mut p, "foo");
|
||||
// Empty path stays un-prefixed — we only add a separator between
|
||||
// existing content and the new component.
|
||||
assert_eq!(p, PathBuf::from("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_path_separators_converts_backslashes() {
|
||||
use std::borrow::Cow;
|
||||
// Already-forward-slashed input is borrowed (no allocation).
|
||||
assert!(matches!(
|
||||
normalize_path_separators("c:/foo/bar"),
|
||||
Cow::Borrowed("c:/foo/bar")
|
||||
));
|
||||
// Mixed or backslashed input becomes owned and fully forward-slashed.
|
||||
let normalized = normalize_path_separators(r"c:\foo\bar");
|
||||
assert_eq!(normalized.as_ref(), "c:/foo/bar");
|
||||
let normalized = normalize_path_separators(r"c:\foo/bar");
|
||||
assert_eq!(normalized.as_ref(), "c:/foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_case_insensitive_is_true() {
|
||||
assert!(default_case_insensitive_path_expansion());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_executable_extension_is_case_insensitive() {
|
||||
// Force the PATHEXT cache for this test's defaults.
|
||||
assert!(has_executable_extension(Path::new("foo.exe")));
|
||||
assert!(has_executable_extension(Path::new("foo.EXE")));
|
||||
assert!(has_executable_extension(Path::new("foo.Cmd")));
|
||||
assert!(!has_executable_extension(Path::new("foo.txt")));
|
||||
assert!(!has_executable_extension(Path::new("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pathext_entry_stem_strips_dot() {
|
||||
assert_eq!(pathext_entry_stem(".exe"), "exe");
|
||||
assert_eq!(pathext_entry_stem(".cmd"), "cmd");
|
||||
// Tolerant: entries without a leading dot are returned as-is.
|
||||
assert_eq!(pathext_entry_stem("exe"), "exe");
|
||||
assert_eq!(pathext_entry_stem(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_executable_for_nonexistent_returns_none() {
|
||||
// A path that cannot exist on any test host.
|
||||
let path = PathBuf::from(r"C:\__brush_test_definitely_missing__");
|
||||
assert!(resolve_executable(path).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
|
||||
crate::sys::hostname::get()
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#![allow(clippy::missing_const_for_fn)]
|
||||
#![allow(clippy::unnecessary_wraps)]
|
||||
|
||||
use crate::error;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Placeholder UID for non-elevated Windows processes.
|
||||
///
|
||||
/// Real Unix-style UIDs don't exist on Windows; this value is a
|
||||
/// conventional non-root sentinel (matching the typical first
|
||||
/// regular-user UID on Linux).
|
||||
const NON_ELEVATED_UID: u32 = 1000;
|
||||
|
||||
/// Placeholder GID for non-elevated Windows processes (see [`NON_ELEVATED_UID`]).
|
||||
const NON_ELEVATED_GID: u32 = 1000;
|
||||
|
||||
/// Cached elevation status. The underlying check queries the process token,
|
||||
/// which can't change after process start, so it's safe to memoize.
|
||||
static IS_ELEVATED: LazyLock<bool> = LazyLock::new(|| {
|
||||
check_elevation::is_elevated().unwrap_or_else(|err| {
|
||||
tracing::warn!("failed to determine process elevation: {err}");
|
||||
false
|
||||
})
|
||||
});
|
||||
|
||||
pub(crate) fn get_user_home_dir(_username: &str) -> Option<PathBuf> {
|
||||
// std::env::home_dir() doesn't support getting home dir for arbitrary users
|
||||
// For now, we only support getting the current user's home dir
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
|
||||
std::env::home_dir()
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_elevated() -> bool {
|
||||
*IS_ELEVATED
|
||||
}
|
||||
|
||||
pub(crate) fn is_root() -> bool {
|
||||
is_elevated()
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
|
||||
Ok(if is_elevated() { 0 } else { NON_ELEVATED_UID })
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
|
||||
Ok(if is_elevated() { 0 } else { NON_ELEVATED_GID })
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
|
||||
Ok(if is_elevated() { 0 } else { NON_ELEVATED_UID })
|
||||
}
|
||||
|
||||
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
|
||||
Ok(if is_elevated() { 0 } else { NON_ELEVATED_GID })
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_username() -> Result<String, error::Error> {
|
||||
let username = whoami::username().map_err(std::io::Error::from)?;
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
|
||||
// TODO(windows): implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(windows): implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
|
||||
// TODO(windows): implement some version of this for Windows
|
||||
Ok(vec![])
|
||||
}
|
||||
Reference in New Issue
Block a user