pub(crate) mod ipc;
mod process;
use std::{
fs::{self, DirBuilder, File, OpenOptions, Permissions},
future::Future,
io,
os::unix::{
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt},
process::ExitStatusExt,
},
path::Path,
process::ExitStatus,
};
pub(crate) use tempfile::TempDir as PrivateTempDir;
use tokio::signal::unix::{SignalKind, signal};
pub(crate) use process::{
Child, kill_saved_group, kill_saved_session, process_start_time, running_process_start_time,
spawn_detached, spawn_gated, spawn_run, spawn_watched,
};
pub(crate) fn remove_abandoned_directory(path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path)
}
pub(crate) fn transient_file_access(_error: &io::Error) -> bool {
false
}
const PRIVATE_DIR_MODE: u32 = 0o700;
const PRIVATE_FILE_MODE: u32 = 0o600;
pub(crate) const PRIVATE_DIRECTORY: &str = "owner-only (mode 0700)";
pub(crate) const PRIVATE_FILE: &str = "readable by their owner only (mode 0600)";
pub(crate) const GROUP_OTHER_BITS: u32 = 0o077;
const GROUP_OTHER_WRITE_BITS: u32 = 0o022;
const PERMISSION_BITS: u32 = 0o777;
pub(crate) fn create_private_dir_all(path: &Path) -> io::Result<()> {
DirBuilder::new()
.recursive(true)
.mode(PRIVATE_DIR_MODE)
.create(path)
}
pub(crate) fn create_private_dir(path: &Path) -> io::Result<()> {
DirBuilder::new().mode(PRIVATE_DIR_MODE).create(path)?;
fs::set_permissions(path, Permissions::from_mode(PRIVATE_DIR_MODE))
}
pub(crate) fn private_tempdir_in(prefix: &str, parent: &Path) -> io::Result<PrivateTempDir> {
let directory = tempfile::Builder::new()
.prefix(prefix)
.permissions(Permissions::from_mode(PRIVATE_DIR_MODE))
.tempdir_in(parent)?;
fs::set_permissions(directory.path(), Permissions::from_mode(PRIVATE_DIR_MODE))?;
Ok(directory)
}
pub(crate) fn is_private_dir(path: &Path) -> io::Result<bool> {
Ok(fs::metadata(path)?.mode() & GROUP_OTHER_BITS == 0)
}
pub(crate) fn is_private_file(file: &File) -> io::Result<bool> {
let metadata = file.metadata()?;
Ok(metadata.is_file()
&& metadata.mode() & PERMISSION_BITS == PRIVATE_FILE_MODE
&& metadata.nlink() == 1)
}
pub(crate) fn private_options() -> OpenOptions {
let mut options = OpenOptions::new();
options.mode(PRIVATE_FILE_MODE);
options
}
pub(crate) fn restrict_file(file: &File) -> io::Result<()> {
file.set_permissions(Permissions::from_mode(PRIVATE_FILE_MODE))
}
pub(crate) fn sync_dir(path: &Path) -> io::Result<()> {
File::open(path).and_then(|directory| directory.sync_all())
}
pub(crate) fn file_identity(file: &File) -> io::Result<super::FileIdentity> {
let metadata = file.metadata()?;
Ok(super::FileIdentity {
volume: metadata.dev(),
index: metadata.ino(),
})
}
pub(crate) fn current_user() -> u32 {
unsafe { libc::geteuid() }
}
pub(crate) fn is_owner_only(file: &File) -> io::Result<bool> {
let metadata = file.metadata()?;
Ok(metadata.mode() & GROUP_OTHER_BITS == 0 && metadata.uid() == current_user())
}
pub(crate) const ENV_NAMES_IGNORE_CASE: bool = false;
pub(crate) const HOME_VARIABLE: &str = "HOME";
pub(crate) const TEMP_VARIABLES: &[&str] = &["TMPDIR", "TMP", "TEMP"];
pub(crate) const STATE_VARIABLES: &[&str] = &["XDG_STATE_HOME"];
pub(crate) const USER_VARIABLES: &[&str] = &["USER", "LOGNAME"];
pub(crate) fn home_directory() -> Option<std::path::PathBuf> {
std::env::var_os(HOME_VARIABLE)
.filter(|path| !path.is_empty())
.map(Into::into)
}
pub(crate) fn path_from_bytes(bytes: &[u8]) -> std::path::PathBuf {
use std::os::unix::ffi::OsStringExt;
std::ffi::OsString::from_vec(bytes.to_vec()).into()
}
pub(crate) const CRLF_LINE_ENDINGS: bool = false;
pub const VISIBLE_INPUT_FALLBACK: bool = false;
pub(crate) fn exit_signal(status: &ExitStatus) -> Option<i32> {
status.signal()
}
pub(crate) fn host_name() -> Option<String> {
hostname::get().ok()?.into_string().ok()
}
#[cfg(target_os = "macos")]
pub(crate) fn resolve_system_aliases(path: &Path) -> io::Result<std::path::PathBuf> {
let absolute = std::path::absolute(path)?;
for name in ["tmp", "var", "etc"] {
let alias = Path::new("/").join(name);
if let Ok(suffix) = absolute.strip_prefix(&alias) {
let metadata = alias.symlink_metadata()?;
let target = Path::new("/private").join(name);
let link = fs::read_link(&alias)?;
let link = if link.is_absolute() {
link
} else {
Path::new("/").join(link)
};
if !metadata.is_symlink() || metadata.uid() != 0 || link != target {
return Err(io::Error::other(format!(
"Refusing an untrusted system alias: {}",
alias.display()
)));
}
for parent in [Path::new("/"), Path::new("/private")] {
let metadata = parent.symlink_metadata()?;
if !metadata.is_dir()
|| metadata.uid() != 0
|| metadata.mode() & GROUP_OTHER_WRITE_BITS != 0
{
return Err(io::Error::other(
"System alias parents must not be replaceable.",
));
}
}
return Ok(target.join(suffix));
}
}
Ok(absolute)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn resolve_system_aliases(path: &Path) -> io::Result<std::path::PathBuf> {
std::path::absolute(path)
}
pub fn stop_requested() -> io::Result<impl Future<Output = ()> + Send + 'static> {
let mut interrupt = signal(SignalKind::interrupt())?;
let mut terminate = signal(SignalKind::terminate())?;
Ok(async move {
tokio::select! { _ = interrupt.recv() => {}, _ = terminate.recv() => {} }
})
}
pub struct HiddenInput(libc::termios);
impl HiddenInput {
pub fn new() -> io::Result<Self> {
let mut termios = std::mem::MaybeUninit::uninit();
let saved = unsafe {
if libc::tcgetattr(libc::STDIN_FILENO, termios.as_mut_ptr()) != 0 {
return Err(io::Error::last_os_error());
}
termios.assume_init()
};
let mut hidden = saved;
hidden.c_lflag &= !libc::ECHO;
if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &hidden) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(Self(saved))
}
}
impl Drop for HiddenInput {
fn drop(&mut self) {
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.0) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transient_file_access_is_always_false() {
assert!(!transient_file_access(&io::Error::from_raw_os_error(5)));
assert!(!transient_file_access(&io::Error::new(
io::ErrorKind::PermissionDenied,
"permission denied"
)));
}
}