use std::cmp::Ordering;
use std::ffi::OsString;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use runner_manager_domain::path::{LocalAbsolutePath, LocalPathError, PathPlatform};
use crate::paths::AppPaths;
pub const WINDOWS_RUNNER_ROOT_NAME: &str = "rman";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RootOwner {
Host,
Repository(String),
}
impl RootOwner {
#[must_use]
pub fn remediation(&self) -> String {
match self {
RootOwner::Host => "runner-manager host set-runtime-root --path <PATH>".to_string(),
RootOwner::Repository(repository) => format!(
"runner-manager repo set-workspace {repository} --mode persistent --path <PATH>"
),
}
}
}
impl fmt::Display for RootOwner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RootOwner::Host => f.write_str("the host runner root"),
RootOwner::Repository(repository) => {
write!(f, "the persistent workspace root for {repository}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overlap {
Disjoint,
Same,
Inside,
Contains,
}
impl fmt::Display for Overlap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Overlap::Disjoint => "is unrelated to",
Overlap::Same => "is the same directory as",
Overlap::Inside => "is inside",
Overlap::Contains => "contains",
})
}
}
fn components_of(path: &str, platform: PathPlatform) -> Vec<&str> {
path.split(|c| platform.is_separator(c))
.filter(|component| !component.is_empty() && *component != ".")
.collect()
}
fn same_component(left: &str, right: &str, platform: PathPlatform) -> bool {
match platform {
PathPlatform::Windows => left
.chars()
.flat_map(char::to_lowercase)
.eq(right.chars().flat_map(char::to_lowercase)),
PathPlatform::Unix => left == right,
}
}
fn overlap_of(candidate: &str, other: &str, platform: PathPlatform) -> Overlap {
let left = components_of(candidate, platform);
let right = components_of(other, platform);
let shared = left
.iter()
.zip(right.iter())
.take_while(|(l, r)| same_component(l, r, platform))
.count();
if shared < left.len().min(right.len()) {
return Overlap::Disjoint;
}
match left.len().cmp(&right.len()) {
Ordering::Equal => Overlap::Same,
Ordering::Greater => Overlap::Inside,
Ordering::Less => Overlap::Contains,
}
}
#[derive(Debug, thiserror::Error)]
pub enum RunnerRootError {
#[error(
"the operating system did not report a system directory, so the default runner \
root <system-drive>\\{WINDOWS_RUNNER_ROOT_NAME} cannot be resolved: {source}. \
Configure one explicitly with `{}`.",
RootOwner::Host.remediation()
)]
SystemDirectoryUnavailable {
#[source]
source: io::Error,
},
#[error(
"the system directory {got:?} is not a usable volume for the default runner \
root: {source}. Configure one explicitly with `{}`.",
RootOwner::Host.remediation()
)]
SystemDirectoryUnusable {
got: String,
#[source]
source: LocalPathError,
},
#[error(
"the application runtime directory {} cannot be used as the default runner \
root: {source}",
got.display()
)]
ApplicationRuntimeDirectoryUnusable {
got: PathBuf,
#[source]
source: LocalPathError,
},
#[error(
"{} cannot be represented as text, and a runner root is stored, printed and \
compared as text",
got.display()
)]
NonUnicode { got: PathBuf },
#[error(
"{got:?} is written in {platform} path syntax, but this host uses {}; a row \
written on another operating system is corrupt state here rather than a \
usable root",
PathPlatform::NATIVE
)]
ForeignPlatform { got: String, platform: PathPlatform },
#[error("cannot inspect {}: {source}", path.display())]
Inspect {
path: PathBuf,
#[source]
source: io::Error,
},
#[error(
"{} already exists and is not a directory; a runner root is a directory that \
attempt directories are created inside",
path.display()
)]
ExistingFile { path: PathBuf },
#[error(
"{} is a symbolic link, junction or other reparse point. A runner root is the \
base of a recursive cleanup, so it must be the real directory rather than a \
name that can be repointed at one; configure the target directly.",
path.display()
)]
Symlinked { path: PathBuf },
#[error(
"{} cannot be created because more than its last component is missing; the \
deepest directory that does exist is {}. Create the intermediate directories \
first, or configure a path one level below an existing directory.",
path.display(),
deepest_existing.display()
)]
MissingParents {
path: PathBuf,
deepest_existing: PathBuf,
},
#[error(
"{} exists but is not a directory, so nothing can be created inside it",
parent.display()
)]
ParentIsNotADirectory { parent: PathBuf },
#[error(
"{} exists but this account may not create entries in it. Grant this account \
write access, or configure a directory it owns with `{remediation}`.",
path.display()
)]
NotWritable { path: PathBuf, remediation: String },
#[error(
"the runner root {} cannot be used: this process runs as the superuser, so file \
permissions are not what refused {}, and that directory is on a volume macOS \
withholds through its privacy controls. Grant Full Disk Access to the program \
that runs the service -- System Settings > Privacy & Security > Full Disk \
Access -- and start the service again, or configure a directory on the startup \
disk with `{remediation}`. Note that the grant follows the binary and not the \
path: an upgrade that replaces the service binary revokes it, and it has to be \
granted again to the new one.",
requested.display(),
refused.display()
)]
DeniedByPrivacyPolicy {
requested: PathBuf,
refused: PathBuf,
remediation: String,
},
#[error(
"{} does not exist yet and this account may not create it: its parent {} \
refuses. Grant this account write access to that directory, or configure a \
directory it owns with `{remediation}`.",
leaf.display(),
parent.display()
)]
ParentNotWritable {
parent: PathBuf,
leaf: PathBuf,
remediation: String,
},
#[error(
"{} is on {filesystem}. Runner correctness and restart recovery may not depend \
on a remote share that can disappear or change identity while a job runs \
(D10); configure a directory on a local volume.",
path.display()
)]
RemoteFilesystem { path: PathBuf, filesystem: String },
#[error(
"this host cannot prove that {} is on a local filesystem (it reported \
{filesystem}). A runner root is accepted only when locality is provable, so \
this fails closed; configure a directory on a local volume.",
path.display()
)]
UnprovableFilesystem { path: PathBuf, filesystem: String },
#[error(
"{} resolves to {}, which is a filesystem root. A runner root must be a \
directory below a root, because everything inside it is removed on cleanup.",
path.display(),
canonical.display()
)]
ResolvesToFilesystemRoot { path: PathBuf, canonical: PathBuf },
#[error(
"{} {relation} {other_owner} ({}). {detail}",
candidate.display(),
other.display()
)]
Overlaps {
candidate: PathBuf,
relation: Overlap,
other: PathBuf,
other_owner: String,
detail: &'static str,
},
#[error(
"{} is derived from the runner root {} but resolves to {}, which is outside it",
child.display(),
root.display(),
resolved.display()
)]
Escapes {
root: PathBuf,
child: PathBuf,
resolved: PathBuf,
},
#[error("{source}")]
DerivedName {
#[source]
source: LocalPathError,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Locality {
Local,
Remote,
Unprovable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilesystemIdentity {
pub locality: Locality,
pub name: String,
}
impl FilesystemIdentity {
#[must_use]
pub fn local(name: impl Into<String>) -> Self {
Self {
locality: Locality::Local,
name: name.into(),
}
}
#[must_use]
pub fn remote(name: impl Into<String>) -> Self {
Self {
locality: Locality::Remote,
name: name.into(),
}
}
#[must_use]
pub fn unprovable(name: impl Into<String>) -> Self {
Self {
locality: Locality::Unprovable,
name: name.into(),
}
}
}
pub trait FilesystemProbe {
fn identify(&self, directory: &Path) -> io::Result<FilesystemIdentity>;
fn is_writable(&self, directory: &Path) -> io::Result<bool>;
fn runs_as_superuser(&self) -> bool {
sys::runs_as_superuser()
}
fn is_on_privacy_gated_volume(&self, directory: &Path) -> bool {
sys::is_on_privacy_gated_volume(directory)
}
fn is_read_only(&self, directory: &Path) -> bool {
sys::is_read_only(directory)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HostFilesystem;
impl FilesystemProbe for HostFilesystem {
fn identify(&self, directory: &Path) -> io::Result<FilesystemIdentity> {
sys::identify(directory)
}
fn is_writable(&self, directory: &Path) -> io::Result<bool> {
sys::is_writable(directory)
}
}
static HOST_FILESYSTEM: HostFilesystem = HostFilesystem;
impl RunnerRootError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::SystemDirectoryUnavailable { .. } => "system_directory_unavailable",
Self::SystemDirectoryUnusable { .. } => "system_directory_unusable",
Self::ApplicationRuntimeDirectoryUnusable { .. } => "runtime_directory_unusable",
Self::NonUnicode { .. } => "non_unicode",
Self::ForeignPlatform { .. } => "foreign_platform",
Self::Inspect { .. } => "not_inspectable",
Self::ExistingFile { .. } => "existing_file",
Self::Symlinked { .. } => "symlinked",
Self::MissingParents { .. } => "missing_parents",
Self::ParentIsNotADirectory { .. } => "parent_not_a_directory",
Self::NotWritable { .. } => "not_writable",
Self::DeniedByPrivacyPolicy { .. } => "denied_by_privacy_policy",
Self::ParentNotWritable { .. } => "parent_not_writable",
Self::RemoteFilesystem { .. } => "remote_filesystem",
Self::UnprovableFilesystem { .. } => "unprovable_filesystem",
Self::ResolvesToFilesystemRoot { .. } => "resolves_to_filesystem_root",
Self::Overlaps { .. } => "overlaps_application_data",
Self::Escapes { .. } => "escapes_root",
Self::DerivedName { .. } => "underivable_name",
}
}
}
#[must_use]
pub fn is_on_privacy_gated_volume(path: &Path) -> bool {
sys::is_on_privacy_gated_volume(path)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlatformDefault<'a> {
WindowsSystemDirectory(&'a str),
ApplicationRuntimeDirectory(&'a Path),
}
pub fn default_runner_root_from(
source: PlatformDefault<'_>,
) -> Result<LocalAbsolutePath, RunnerRootError> {
match source {
PlatformDefault::WindowsSystemDirectory(raw) => {
let unusable = |source| RunnerRootError::SystemDirectoryUnusable {
got: raw.to_string(),
source,
};
let system =
LocalAbsolutePath::parse_for(raw, PathPlatform::Windows).map_err(unusable)?;
let volume: String = system.as_str().chars().take(3).collect();
LocalAbsolutePath::parse_for(
format!("{volume}{WINDOWS_RUNNER_ROOT_NAME}"),
PathPlatform::Windows,
)
.map_err(unusable)
}
PlatformDefault::ApplicationRuntimeDirectory(path) => {
let text = path.to_str().ok_or_else(|| RunnerRootError::NonUnicode {
got: path.to_path_buf(),
})?;
LocalAbsolutePath::new(text).map_err(|source| {
RunnerRootError::ApplicationRuntimeDirectoryUnusable {
got: path.to_path_buf(),
source,
}
})
}
}
}
#[cfg(windows)]
pub fn default_runner_root(app_paths: &AppPaths) -> Result<LocalAbsolutePath, RunnerRootError> {
let _ = app_paths;
let system = sys::system_directory()
.map_err(|source| RunnerRootError::SystemDirectoryUnavailable { source })?;
default_runner_root_from(PlatformDefault::WindowsSystemDirectory(&system))
}
#[cfg(not(windows))]
pub fn default_runner_root(app_paths: &AppPaths) -> Result<LocalAbsolutePath, RunnerRootError> {
default_runner_root_from(PlatformDefault::ApplicationRuntimeDirectory(
app_paths.runtime_dir(),
))
}
#[derive(Debug)]
struct Projection {
anchor: PathBuf,
anchor_as_written: PathBuf,
canonical: PathBuf,
missing: usize,
}
fn means_absent(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
)
}
fn project(path: &Path) -> Result<Projection, RunnerRootError> {
let mut missing: Vec<OsString> = Vec::new();
let mut cursor = path.to_path_buf();
loop {
match std::fs::symlink_metadata(&cursor) {
Ok(_) => break,
Err(error) if means_absent(&error) => {
let name = cursor.file_name().map(OsString::from);
let parent = cursor.parent().map(Path::to_path_buf);
let (Some(name), Some(parent)) = (name, parent) else {
return Err(RunnerRootError::Inspect {
path: cursor,
source: error,
});
};
missing.push(name);
cursor = parent;
}
Err(source) => {
return Err(RunnerRootError::Inspect {
path: cursor,
source,
});
}
}
}
let anchor = std::fs::canonicalize(&cursor)
.map(|canonical| plain(&canonical))
.map_err(|source| RunnerRootError::Inspect {
path: cursor.clone(),
source,
})?;
let mut canonical = anchor.clone();
for component in missing.iter().rev() {
canonical.push(component);
}
Ok(Projection {
anchor,
anchor_as_written: cursor,
canonical,
missing: missing.len(),
})
}
#[cfg(windows)]
fn plain(path: &Path) -> PathBuf {
let text = path.to_string_lossy();
if let Some(rest) = text.strip_prefix(r"\\?\UNC\") {
return PathBuf::from(format!(r"\\{rest}"));
}
if let Some(rest) = text.strip_prefix(r"\\?\") {
return PathBuf::from(rest);
}
path.to_path_buf()
}
#[cfg(not(windows))]
fn plain(path: &Path) -> PathBuf {
path.to_path_buf()
}
fn canonical_text(path: &Path) -> Option<String> {
project(path)
.ok()
.map(|projection| projection.canonical.to_string_lossy().into_owned())
}
fn is_filesystem_root(path: &Path) -> bool {
path.parent().is_none()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreflightedRoot {
root: LocalAbsolutePath,
canonical: PathBuf,
exists: bool,
filesystem: FilesystemIdentity,
}
impl PreflightedRoot {
#[must_use]
pub const fn root(&self) -> &LocalAbsolutePath {
&self.root
}
#[must_use]
pub fn canonical(&self) -> &Path {
&self.canonical
}
#[must_use]
pub const fn exists(&self) -> bool {
self.exists
}
#[must_use]
pub fn leaf_to_create(&self) -> Option<&Path> {
(!self.exists).then(|| self.root.as_path())
}
#[must_use]
pub const fn filesystem(&self) -> &FilesystemIdentity {
&self.filesystem
}
}
pub struct RootPreflight<'a> {
app_paths: &'a AppPaths,
others: Vec<(RootOwner, LocalAbsolutePath)>,
probe: &'a dyn FilesystemProbe,
}
impl fmt::Debug for RootPreflight<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RootPreflight")
.field("app_paths", &self.app_paths)
.field("others", &self.others)
.finish_non_exhaustive()
}
}
impl<'a> RootPreflight<'a> {
#[must_use]
pub fn new(app_paths: &'a AppPaths) -> Self {
Self::with_probe(app_paths, &HOST_FILESYSTEM)
}
#[must_use]
pub fn with_probe(app_paths: &'a AppPaths, probe: &'a dyn FilesystemProbe) -> Self {
Self {
app_paths,
others: Vec::new(),
probe,
}
}
#[must_use]
pub fn against(mut self, owner: RootOwner, root: LocalAbsolutePath) -> Self {
self.others.push((owner, root));
self
}
fn protected(&self) -> [(&'static str, &Path); 3] {
[
(
"the application configuration directory",
self.app_paths.config_dir(),
),
(
"the application state directory",
self.app_paths.state_dir(),
),
("the application log directory", self.app_paths.logs_dir()),
]
}
fn reject_overlap(
&self,
owner: &RootOwner,
candidate: &str,
canonical: bool,
) -> Result<(), RunnerRootError> {
let native = PathPlatform::NATIVE;
let text_of = |path: &Path| -> Option<String> {
if canonical {
canonical_text(path)
} else {
Some(path.to_string_lossy().into_owned())
}
};
let inside_runtime = || {
text_of(self.app_paths.runtime_dir()).is_some_and(|runtime| {
matches!(
overlap_of(candidate, &runtime, native),
Overlap::Same | Overlap::Inside
)
})
};
for (label, path) in self.protected() {
let Some(other) = text_of(path) else {
continue;
};
let relation = overlap_of(candidate, &other, native);
if relation == Overlap::Disjoint || (relation == Overlap::Inside && inside_runtime()) {
continue;
}
return Err(RunnerRootError::Overlaps {
candidate: PathBuf::from(candidate),
relation,
other: PathBuf::from(other),
other_owner: label.to_string(),
detail: "Runner workspaces are removed recursively and application data must \
survive that; configure a directory outside the application data tree.",
});
}
for (other_owner, root) in &self.others {
if other_owner == owner {
continue;
}
let Some(other) = text_of(root.as_path()) else {
continue;
};
let relation = overlap_of(candidate, &other, native);
if relation == Overlap::Disjoint {
continue;
}
return Err(RunnerRootError::Overlaps {
candidate: PathBuf::from(candidate),
relation,
other: PathBuf::from(other),
other_owner: other_owner.to_string(),
detail: "Two runner roots that contain one another can delete each other's \
workspaces; configure directories that do not overlap.",
});
}
Ok(())
}
pub fn check(
&self,
owner: &RootOwner,
root: &LocalAbsolutePath,
) -> Result<PreflightedRoot, RunnerRootError> {
if root.platform() != PathPlatform::NATIVE {
return Err(RunnerRootError::ForeignPlatform {
got: root.as_str().to_string(),
platform: root.platform(),
});
}
let candidate = root.as_path();
self.reject_overlap(owner, root.as_str(), false)?;
match std::fs::symlink_metadata(candidate) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(RunnerRootError::Symlinked {
path: candidate.to_path_buf(),
});
}
Ok(metadata) if !metadata.is_dir() => {
return Err(RunnerRootError::ExistingFile {
path: candidate.to_path_buf(),
});
}
Ok(_) => {}
Err(error) if means_absent(&error) => {}
Err(source) => {
return Err(RunnerRootError::Inspect {
path: candidate.to_path_buf(),
source,
});
}
}
let projection = project(candidate)?;
match projection.missing {
0 => {}
1 => {
if !projection.anchor.is_dir() {
return Err(RunnerRootError::ParentIsNotADirectory {
parent: projection.anchor_as_written.clone(),
});
}
}
_ => {
return Err(RunnerRootError::MissingParents {
path: candidate.to_path_buf(),
deepest_existing: projection.anchor_as_written.clone(),
});
}
}
if is_filesystem_root(&projection.canonical) {
return Err(RunnerRootError::ResolvesToFilesystemRoot {
path: candidate.to_path_buf(),
canonical: projection.canonical.clone(),
});
}
let filesystem =
self.probe
.identify(&projection.anchor)
.map_err(|source| RunnerRootError::Inspect {
path: projection.anchor.clone(),
source,
})?;
match filesystem.locality {
Locality::Local => {}
Locality::Remote => {
return Err(RunnerRootError::RemoteFilesystem {
path: projection.canonical.clone(),
filesystem: filesystem.name,
});
}
Locality::Unprovable => {
return Err(RunnerRootError::UnprovableFilesystem {
path: projection.canonical.clone(),
filesystem: filesystem.name,
});
}
}
let writable = self
.probe
.is_writable(&projection.anchor)
.map_err(|source| RunnerRootError::Inspect {
path: projection.anchor.clone(),
source,
})?;
if !writable {
if cfg!(target_os = "macos")
&& self.probe.runs_as_superuser()
&& self.probe.is_on_privacy_gated_volume(&projection.anchor)
&& !self.probe.is_read_only(&projection.anchor)
{
return Err(RunnerRootError::DeniedByPrivacyPolicy {
requested: candidate.to_path_buf(),
refused: projection.anchor_as_written.clone(),
remediation: owner.remediation(),
});
}
return Err(if projection.missing == 0 {
RunnerRootError::NotWritable {
path: projection.anchor_as_written.clone(),
remediation: owner.remediation(),
}
} else {
RunnerRootError::ParentNotWritable {
parent: projection.anchor_as_written.clone(),
leaf: candidate.to_path_buf(),
remediation: owner.remediation(),
}
});
}
self.reject_overlap(owner, &projection.canonical.to_string_lossy(), true)?;
Ok(PreflightedRoot {
root: root.clone(),
canonical: projection.canonical,
exists: projection.missing == 0,
filesystem,
})
}
}
pub fn derive_child(
root: &LocalAbsolutePath,
name: &str,
) -> Result<LocalAbsolutePath, RunnerRootError> {
root.join_child(name)
.map_err(|source| RunnerRootError::DerivedName { source })
}
pub fn verify_containment(
root: &LocalAbsolutePath,
child: &LocalAbsolutePath,
) -> Result<(), RunnerRootError> {
for value in [root, child] {
if value.platform() != PathPlatform::NATIVE {
return Err(RunnerRootError::ForeignPlatform {
got: value.as_str().to_string(),
platform: value.platform(),
});
}
}
let escapes = |resolved: PathBuf| RunnerRootError::Escapes {
root: root.as_path().to_path_buf(),
child: child.as_path().to_path_buf(),
resolved,
};
if overlap_of(child.as_str(), root.as_str(), root.platform()) != Overlap::Inside {
return Err(escapes(child.as_path().to_path_buf()));
}
let root_projection = project(root.as_path())?;
let child_projection = project(child.as_path())?;
let relation = overlap_of(
&child_projection.canonical.to_string_lossy(),
&root_projection.canonical.to_string_lossy(),
root.platform(),
);
if relation != Overlap::Inside {
return Err(escapes(child_projection.canonical));
}
Ok(())
}
#[cfg(windows)]
mod sys {
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_ADD_SUBDIRECTORY, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE,
FILE_SHARE_READ, FILE_SHARE_WRITE, GetDriveTypeW, GetVolumePathNameW, OPEN_EXISTING,
};
use windows::Win32::System::SystemInformation::GetSystemDirectoryW;
use windows::Win32::System::WindowsProgramming::{
DRIVE_CDROM, DRIVE_FIXED, DRIVE_NO_ROOT_DIR, DRIVE_RAMDISK, DRIVE_REMOTE, DRIVE_REMOVABLE,
DRIVE_UNKNOWN,
};
use windows::core::PCWSTR;
use super::FilesystemIdentity;
const fn hresult_from_win32(code: u32) -> i32 {
if code == 0 {
0
} else {
((code & 0x0000_ffff) | 0x8007_0000) as i32
}
}
fn io_error(error: &windows::core::Error) -> io::Error {
let code = error.code().0;
#[allow(clippy::cast_sign_loss)]
let unsigned = code as u32;
if unsigned & 0xffff_0000 == 0x8007_0000 {
#[allow(clippy::cast_possible_wrap)]
return io::Error::from_raw_os_error((unsigned & 0x0000_ffff) as i32);
}
io::Error::from_raw_os_error(code)
}
fn to_wide(path: &Path) -> Vec<u16> {
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
pub(super) fn system_directory() -> io::Result<String> {
let mut buffer = [0u16; 512];
let written = unsafe { GetSystemDirectoryW(Some(&mut buffer)) } as usize;
if written == 0 {
return Err(io::Error::last_os_error());
}
if written > buffer.len() {
return Err(io::Error::other(format!(
"the system directory needs {written} UTF-16 code units, which is more \
than a system path is expected to occupy"
)));
}
String::from_utf16(&buffer[..written]).map_err(io::Error::other)
}
fn volume_path(directory: &Path) -> io::Result<Vec<u16>> {
let file = to_wide(directory);
let mut buffer = [0u16; 512];
unsafe { GetVolumePathNameW(PCWSTR(file.as_ptr()), &mut buffer) }
.map_err(|error| io_error(&error))?;
let length = buffer
.iter()
.position(|unit| *unit == 0)
.unwrap_or(buffer.len());
let mut mount = buffer[..length].to_vec();
mount.push(0);
Ok(mount)
}
pub(super) fn identify(directory: &Path) -> io::Result<FilesystemIdentity> {
let mount = volume_path(directory)?;
let kind = unsafe { GetDriveTypeW(PCWSTR(mount.as_ptr())) };
Ok(match kind {
DRIVE_FIXED => FilesystemIdentity::local("a fixed local volume"),
DRIVE_REMOVABLE => FilesystemIdentity::local("a removable local volume"),
DRIVE_RAMDISK => FilesystemIdentity::local("a RAM disk"),
DRIVE_CDROM => FilesystemIdentity::local("an optical drive"),
DRIVE_REMOTE => FilesystemIdentity::remote("a network drive"),
DRIVE_NO_ROOT_DIR => FilesystemIdentity::unprovable("no mounted volume"),
DRIVE_UNKNOWN => FilesystemIdentity::unprovable("an unknown drive type"),
other => FilesystemIdentity::unprovable(format!("drive type {other}")),
})
}
pub(super) const fn is_on_privacy_gated_volume(_path: &Path) -> bool {
false
}
pub(super) const fn runs_as_superuser() -> bool {
false
}
pub(super) const fn is_read_only(_path: &Path) -> bool {
false
}
pub(super) fn is_writable(directory: &Path) -> io::Result<bool> {
let wide = to_wide(directory);
let access = FILE_ADD_SUBDIRECTORY.0;
let opened = unsafe {
CreateFileW(
PCWSTR(wide.as_ptr()),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
None,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
None,
)
};
match opened {
Ok(handle) => {
unsafe {
let _ = CloseHandle(handle);
}
Ok(true)
}
Err(error) if error.code().0 == hresult_from_win32(ERROR_ACCESS_DENIED.0) => Ok(false),
Err(error) => Err(io_error(&error)),
}
}
}
#[cfg(unix)]
mod sys {
use std::ffi::CString;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use super::FilesystemIdentity;
fn c_path(path: &Path) -> io::Result<CString> {
CString::new(path.as_os_str().as_bytes()).map_err(|_| {
io::Error::other("a path containing a NUL cannot be given to the operating system")
})
}
#[cfg(target_os = "macos")]
pub(super) fn is_on_privacy_gated_volume(path: &Path) -> bool {
let Ok(path) = c_path(path) else {
return false;
};
let mut buffer: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(path.as_ptr(), &raw mut buffer) } != 0 {
return false;
}
let mount = unsafe { std::ffi::CStr::from_ptr(buffer.f_mntonname.as_ptr()) };
mount.to_bytes().starts_with(b"/Volumes/")
}
#[cfg(not(target_os = "macos"))]
pub(super) const fn is_on_privacy_gated_volume(_path: &Path) -> bool {
false
}
#[cfg(not(target_os = "macos"))]
pub(super) const fn is_read_only(_path: &Path) -> bool {
false
}
#[cfg(target_os = "macos")]
pub(super) fn is_read_only(path: &Path) -> bool {
let Ok(path) = c_path(path) else {
return false;
};
let mut buffer: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(path.as_ptr(), &raw mut buffer) } != 0 {
return false;
}
buffer.f_flags & u32::try_from(libc::MNT_RDONLY).unwrap_or(0) != 0
}
pub(super) fn runs_as_superuser() -> bool {
unsafe { libc::geteuid() == 0 }
}
pub(super) fn is_writable(directory: &Path) -> io::Result<bool> {
let path = c_path(directory)?;
let result = unsafe { libc::access(path.as_ptr(), libc::W_OK | libc::X_OK) };
if result == 0 {
return Ok(true);
}
let error = io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::EACCES | libc::EPERM | libc::EROFS) => Ok(false),
_ => Err(error),
}
}
#[cfg(target_os = "linux")]
const LOCAL_MAGICS: &[(u32, &str)] = &[
(0x0000_ef53, "ext2/ext3/ext4"),
(0x9123_683e, "btrfs"),
(0x5846_5342, "xfs"),
(0x0102_1994, "tmpfs"),
(0x794c_7630, "overlayfs"),
(0x2fc1_2fc1, "zfs"),
(0xf2f5_2010, "f2fs"),
(0x0000_4d44, "vfat"),
(0x2011_bab0, "exfat"),
(0x5346_544e, "ntfs"),
(0x8584_58f6, "ramfs"),
(0x0000_9660, "iso9660"),
(0x7371_7368, "squashfs"),
(0x3153_464a, "jfs"),
(0x5265_4973, "reiserfs"),
(0xca45_1a4e, "bcachefs"),
(0x0000_4244, "hfs"),
(0x0000_482b, "hfsplus"),
];
#[cfg(target_os = "linux")]
const REMOTE_MAGICS: &[(u32, &str)] = &[
(0x0000_6969, "nfs"),
(0xff53_4d42, "cifs"),
(0xfe53_4d42, "smb2"),
(0x0000_517b, "smb"),
(0x7375_7245, "coda"),
(0x0000_564c, "ncpfs"),
(0x5346_414f, "afs"),
(0x6b41_4653, "afs"),
(0x0bd0_0bd0, "lustre"),
(0x00c3_6400, "ceph"),
(0x0102_1997, "9p"),
(0x0116_1970, "gfs2"),
(0x7461_636f, "ocfs2"),
];
#[cfg(target_os = "linux")]
pub(super) fn identify(directory: &Path) -> io::Result<FilesystemIdentity> {
let path = c_path(directory)?;
let mut buffer: libc::statfs = unsafe { std::mem::zeroed() };
let result = unsafe { libc::statfs(path.as_ptr(), &raw mut buffer) };
if result != 0 {
return Err(io::Error::last_os_error());
}
let magic = buffer.f_type as u32;
if let Some((_, name)) = LOCAL_MAGICS.iter().find(|(value, _)| *value == magic) {
return Ok(FilesystemIdentity::local(*name));
}
if let Some((_, name)) = REMOTE_MAGICS.iter().find(|(value, _)| *value == magic) {
return Ok(FilesystemIdentity::remote(*name));
}
Ok(FilesystemIdentity::unprovable(format!(
"filesystem type 0x{magic:08x}"
)))
}
#[cfg(target_os = "macos")]
pub(super) fn identify(directory: &Path) -> io::Result<FilesystemIdentity> {
let path = c_path(directory)?;
let mut buffer: libc::statfs = unsafe { std::mem::zeroed() };
let result = unsafe { libc::statfs(path.as_ptr(), &raw mut buffer) };
if result != 0 {
return Err(io::Error::last_os_error());
}
let name = unsafe { std::ffi::CStr::from_ptr(buffer.f_fstypename.as_ptr()) }
.to_string_lossy()
.into_owned();
#[allow(clippy::cast_sign_loss)]
let local = buffer.f_flags & (libc::MNT_LOCAL as u32) != 0;
Ok(if local {
FilesystemIdentity::local(name)
} else {
FilesystemIdentity::remote(name)
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub(super) fn identify(_directory: &Path) -> io::Result<FilesystemIdentity> {
Ok(FilesystemIdentity::unprovable(
"an operating system this build cannot interrogate",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use PathPlatform::{Unix, Windows};
#[derive(Debug)]
struct StubFilesystem {
identity: FilesystemIdentity,
writable: bool,
superuser: bool,
gated_volume: bool,
read_only: bool,
}
impl StubFilesystem {
fn saying(identity: FilesystemIdentity) -> Self {
Self {
identity,
writable: true,
superuser: false,
gated_volume: false,
read_only: false,
}
}
fn unwritable() -> Self {
Self {
identity: FilesystemIdentity::local("a test volume"),
writable: false,
superuser: false,
gated_volume: false,
read_only: false,
}
}
#[cfg(target_os = "macos")]
fn unwritable_to_the_superuser() -> Self {
Self {
superuser: true,
gated_volume: true,
..Self::unwritable()
}
}
#[cfg(target_os = "macos")]
fn unwritable_to_the_superuser_on_the_startup_disk() -> Self {
Self {
superuser: true,
..Self::unwritable()
}
}
#[cfg(target_os = "macos")]
fn read_only_gated_volume() -> Self {
Self {
superuser: true,
gated_volume: true,
read_only: true,
..Self::unwritable()
}
}
}
impl FilesystemProbe for StubFilesystem {
fn identify(&self, _directory: &Path) -> io::Result<FilesystemIdentity> {
Ok(self.identity.clone())
}
fn is_writable(&self, _directory: &Path) -> io::Result<bool> {
Ok(self.writable)
}
fn runs_as_superuser(&self) -> bool {
self.superuser
}
fn is_on_privacy_gated_volume(&self, _directory: &Path) -> bool {
self.gated_volume
}
fn is_read_only(&self, _directory: &Path) -> bool {
self.read_only
}
}
fn native(path: &Path) -> LocalAbsolutePath {
LocalAbsolutePath::new(path.to_str().expect("the fixture path is unicode"))
.expect("the fixture path is a storable local path")
}
fn foreign() -> LocalAbsolutePath {
if cfg!(windows) {
LocalAbsolutePath::parse_for("/srv/rman", Unix)
} else {
LocalAbsolutePath::parse_for("C:\\rman", Windows)
}
.expect("the fixture is valid for the other platform")
}
#[cfg(windows)]
fn link_dir(target: &Path, link: &Path) -> bool {
std::process::Command::new("cmd")
.arg("/C")
.arg("mklink")
.arg("/J")
.arg(link)
.arg(target)
.output()
.is_ok_and(|output| output.status.success())
}
#[cfg(unix)]
fn link_dir(target: &Path, link: &Path) -> bool {
std::os::unix::fs::symlink(target, link).is_ok()
}
struct Fixture {
root: tempfile::TempDir,
paths: AppPaths,
workspaces: PathBuf,
}
impl Fixture {
fn check(&self, path: &Path) -> Result<PreflightedRoot, RunnerRootError> {
RootPreflight::new(&self.paths).check(&RootOwner::Host, &native(path))
}
}
fn fixture() -> Fixture {
let root = tempfile::tempdir().expect("a temporary directory");
let paths = AppPaths::rooted_at(root.path());
paths.create_all().expect("the layout is created");
let workspaces = root.path().join("workspaces");
std::fs::create_dir(&workspaces).expect("the workspace parent is created");
Fixture {
root,
paths,
workspaces,
}
}
fn snapshot(root: &Path) -> Vec<String> {
fn walk(path: &Path, into: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(path) else {
return;
};
for entry in entries.flatten() {
let metadata = entry
.metadata()
.expect("an entry that was just listed can be inspected");
#[cfg(unix)]
let permissions = {
use std::os::unix::fs::PermissionsExt;
format!("{:04o}", metadata.permissions().mode() & 0o7777)
};
#[cfg(not(unix))]
let permissions = format!("readonly={}", metadata.permissions().readonly());
into.push(format!(
"{} dir={} len={} {permissions}",
entry.path().display(),
metadata.is_dir(),
metadata.len()
));
if metadata.is_dir() {
walk(&entry.path(), into);
}
}
}
let mut entries = Vec::new();
walk(root, &mut entries);
entries.sort();
entries
}
#[test]
fn the_windows_default_is_the_system_drive_plus_rman() {
let cases = [
("C:\\Windows\\system32", "C:\\rman"),
("E:\\Windows\\system32", "E:\\rman"),
("c:/windows/system32", "C:\\rman"),
("Z:\\WINDOWS\\SYSTEM32", "Z:\\rman"),
("D:\\Windows", "D:\\rman"),
];
for (system_directory, expected) in cases {
let resolved =
default_runner_root_from(PlatformDefault::WindowsSystemDirectory(system_directory))
.expect("a drive path resolves");
assert_eq!(
resolved.as_str(),
expected,
"system directory {system_directory:?}"
);
assert_eq!(resolved.platform(), Windows);
}
}
#[test]
fn a_system_directory_that_is_not_a_local_drive_fails_with_the_remediation() {
for system_directory in [
"\\\\nas\\share\\system32",
"\\\\?\\C:\\Windows\\system32",
"C:\\",
"windows\\system32",
"",
] {
let error =
default_runner_root_from(PlatformDefault::WindowsSystemDirectory(system_directory))
.expect_err("an unusable system directory must not resolve");
let message = error.to_string();
assert!(
message.contains("host set-runtime-root"),
"the message must name the command that fixes it: {message}"
);
}
}
#[test]
fn the_application_runtime_directory_arm_changes_nothing() {
let root = tempfile::tempdir().expect("a temporary directory");
let paths = AppPaths::rooted_at(root.path());
let resolved = default_runner_root_from(PlatformDefault::ApplicationRuntimeDirectory(
paths.runtime_dir(),
))
.expect("a resolved runtime directory is storable");
assert_eq!(resolved.as_path(), paths.runtime_dir());
}
#[cfg(not(windows))]
#[test]
fn the_macos_and_linux_defaults_are_the_existing_runtime_directory() {
let discovered = AppPaths::discover().expect("a home directory exists on every CI leg");
assert_eq!(
default_runner_root(&discovered)
.expect("the discovered layout resolves")
.as_path(),
discovered.runtime_dir(),
"moving the Unix defaults would relocate live workspaces for no reason"
);
let root = tempfile::tempdir().expect("a temporary directory");
let rooted = AppPaths::rooted_at(root.path());
assert_eq!(
default_runner_root(&rooted)
.expect("an explicit root resolves")
.as_path(),
rooted.runtime_dir()
);
}
#[cfg(windows)]
#[test]
fn the_windows_default_is_this_machines_system_drive() {
let paths = AppPaths::rooted_at(Path::new("C:\\does-not-matter"));
let resolved = default_runner_root(&paths).expect("this host has a system directory");
let text = resolved.as_str();
assert_eq!(
&text[1..],
format!(":\\{WINDOWS_RUNNER_ROOT_NAME}"),
"the default is <system-drive> plus {WINDOWS_RUNNER_ROOT_NAME}, got {text}"
);
assert!(text.starts_with(|c: char| c.is_ascii_uppercase()));
}
#[cfg(windows)]
#[test]
#[serial_test::serial(environment)]
fn the_windows_default_ignores_a_rewritten_system_drive_variable() {
let paths = AppPaths::rooted_at(Path::new("C:\\does-not-matter"));
let before = default_runner_root(&paths).expect("this host has a system directory");
let restore_drive = std::env::var_os("SystemDrive");
let restore_root = std::env::var_os("SystemRoot");
unsafe {
std::env::set_var("SystemDrive", "Q:");
std::env::set_var("SystemRoot", "Q:\\Windows");
}
let after = default_runner_root(&paths);
unsafe {
match restore_drive {
Some(value) => std::env::set_var("SystemDrive", value),
None => std::env::remove_var("SystemDrive"),
}
match restore_root {
Some(value) => std::env::set_var("SystemRoot", value),
None => std::env::remove_var("SystemRoot"),
}
}
let after = after.expect("the kernel still answers");
assert_eq!(after, before);
assert_ne!(after.as_str(), "Q:\\rman");
}
#[test]
fn overlap_is_decided_component_by_component_on_both_platforms() {
let cases = [
(Unix, "/srv/rman", "/srv/rman", Overlap::Same),
(Unix, "/srv/rman/s1", "/srv/rman", Overlap::Inside),
(Unix, "/srv", "/srv/rman", Overlap::Contains),
(Unix, "/srv/rman", "/srv/other", Overlap::Disjoint),
(Unix, "/srv/rman-old", "/srv/rman", Overlap::Disjoint),
(Unix, "/", "/srv/rman", Overlap::Contains),
(Unix, "/srv/Rman", "/srv/rman", Overlap::Disjoint),
(Windows, "C:\\rman", "C:\\rman", Overlap::Same),
(Windows, "C:\\RMAN", "c:\\rman", Overlap::Same),
(Windows, "C:\\rman\\s1", "C:\\rman", Overlap::Inside),
(Windows, "C:\\", "C:\\rman", Overlap::Contains),
(Windows, "D:\\rman", "C:\\rman", Overlap::Disjoint),
(Windows, "C:\\rman-old", "C:\\rman", Overlap::Disjoint),
];
for (platform, candidate, other, expected) in cases {
assert_eq!(
overlap_of(candidate, other, platform),
expected,
"{platform}: {candidate:?} vs {other:?}"
);
}
}
#[test]
fn a_filesystem_root_never_reaches_the_preflight() {
for (raw, platform) in [("/", Unix), ("C:\\", Windows), ("c:/", Windows)] {
assert!(
LocalAbsolutePath::parse_for(raw, platform).is_err(),
"{raw:?} must not be storable"
);
}
let (root, below) = if cfg!(windows) {
("C:\\", "C:\\rman")
} else {
("/", "/srv")
};
assert!(is_filesystem_root(Path::new(root)));
assert!(!is_filesystem_root(Path::new(below)));
}
#[test]
fn an_existing_writable_local_directory_is_accepted() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::create_dir(&root).expect("the root is created");
let checked = fixture
.check(&root)
.expect("a plain writable directory on this machine is usable");
assert!(checked.exists());
assert_eq!(checked.leaf_to_create(), None);
assert_eq!(checked.filesystem().locality, Locality::Local);
assert_eq!(
checked.canonical(),
plain(&std::fs::canonicalize(&root).expect("it exists"))
);
}
#[test]
fn a_missing_leaf_below_a_writable_parent_is_accepted_and_not_created() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
let checked = fixture.check(&root).expect("a creatable leaf is usable");
assert!(!checked.exists());
assert_eq!(checked.leaf_to_create(), Some(root.as_path()));
assert!(
!root.exists(),
"creation is the caller's explicit step, never the preflight's"
);
}
#[test]
fn more_than_one_missing_level_is_refused_with_the_deepest_directory() {
let fixture = fixture();
let root = fixture.workspaces.join("a").join("b");
let error = fixture
.check(&root)
.expect_err("only the leaf may be missing");
let RunnerRootError::MissingParents {
deepest_existing, ..
} = &error
else {
panic!("expected MissingParents, got {error}");
};
assert_eq!(deepest_existing, &fixture.workspaces);
}
#[test]
fn an_existing_file_is_refused() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::write(&root, b"not a directory").expect("the file is created");
let error = fixture
.check(&root)
.expect_err("a file is not a runner root");
assert!(
matches!(error, RunnerRootError::ExistingFile { .. }),
"got {error}"
);
}
#[test]
fn a_file_where_the_parent_should_be_is_refused() {
let fixture = fixture();
let file = fixture.workspaces.join("notes.txt");
std::fs::write(&file, b"notes").expect("the file is created");
let root = file.join("rman");
let error = fixture
.check(&root)
.expect_err("nothing can be created inside a file");
assert!(
matches!(error, RunnerRootError::ParentIsNotADirectory { .. }),
"got {error}"
);
}
#[test]
fn a_linked_root_is_refused_rather_than_followed() {
let fixture = fixture();
let target = fixture.workspaces.join("real");
std::fs::create_dir(&target).expect("the target is created");
let root = fixture.workspaces.join("rman");
if !link_dir(&target, &root) {
return;
}
let error = fixture
.check(&root)
.expect_err("a runner root is the base of a recursive cleanup");
assert!(
matches!(error, RunnerRootError::Symlinked { .. }),
"got {error}"
);
}
#[test]
fn a_link_whose_target_is_gone_is_still_reported_as_a_link() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
if !link_dir(&fixture.workspaces.join("gone"), &root) {
return;
}
let error = fixture
.check(&root)
.expect_err("a dangling link is not a runner root");
assert!(
matches!(error, RunnerRootError::Symlinked { .. }),
"got {error}"
);
}
#[test]
fn a_link_in_the_path_cannot_smuggle_a_root_into_application_data() {
let fixture = fixture();
let bridge = fixture.workspaces.join("bridge");
if !link_dir(fixture.paths.state_dir(), &bridge) {
return;
}
let root = bridge.join("rman");
let error = fixture
.check(&root)
.expect_err("the canonical check must see through the link");
let RunnerRootError::Overlaps { relation, .. } = &error else {
panic!("expected Overlaps, got {error}");
};
assert_eq!(*relation, Overlap::Inside);
}
#[test]
fn a_root_that_collides_with_application_data_is_refused() {
let fixture = fixture();
let preflight = RootPreflight::new(&fixture.paths);
let cases = [
(fixture.paths.state_dir().to_path_buf(), Overlap::Same),
(fixture.paths.logs_dir().join("rman"), Overlap::Inside),
(fixture.root.path().to_path_buf(), Overlap::Contains),
];
for (candidate, expected) in cases {
let error = preflight
.check(&RootOwner::Host, &native(&candidate))
.expect_err("application data may not share a tree with runner workspaces");
let RunnerRootError::Overlaps { relation, .. } = &error else {
panic!("{} gave {error}", candidate.display());
};
assert_eq!(*relation, expected, "{}", candidate.display());
}
}
#[test]
fn the_macos_shaped_layout_still_accepts_its_own_runtime_directory() {
let root = tempfile::tempdir().expect("a temporary directory");
let base = root.path();
let paths = AppPaths::from_directories(
base,
base.join("state"),
base.join("runtime"),
base.join("logs"),
);
paths.create_all().expect("the layout is created");
let preflight = RootPreflight::new(&paths);
preflight
.check(&RootOwner::Host, &native(paths.runtime_dir()))
.expect("the platform default must pass its own preflight");
preflight
.check(
&RootOwner::Host,
&native(&paths.runtime_dir().join("nested")),
)
.expect("a directory below the runtime directory is still the runner area");
for refused in [base.to_path_buf(), base.join("state"), base.join("beside")] {
let error = preflight
.check(&RootOwner::Host, &native(&refused))
.expect_err("only the runtime subtree is exempt");
assert!(
matches!(error, RunnerRootError::Overlaps { .. }),
"{} gave {error}",
refused.display()
);
}
}
#[test]
fn two_roots_may_not_contain_one_another() {
let fixture = fixture();
let host = fixture.workspaces.join("host");
let repository = host.join("acme");
let other = fixture.workspaces.join("other");
std::fs::create_dir_all(&repository).expect("both roots are created");
std::fs::create_dir(&other).expect("the third root is created");
let owner = RootOwner::Repository("acme/widgets".to_string());
let preflight = RootPreflight::new(&fixture.paths)
.against(RootOwner::Host, native(&host))
.against(
RootOwner::Repository("acme/gadgets".to_string()),
native(&other),
);
let error = preflight
.check(&owner, &native(&repository))
.expect_err("a repository root inside the host root is refused");
let RunnerRootError::Overlaps {
relation,
other_owner,
..
} = &error
else {
panic!("expected Overlaps, got {error}");
};
assert_eq!(*relation, Overlap::Inside);
assert_eq!(other_owner, &RootOwner::Host.to_string());
let error = preflight
.check(&owner, &native(&other))
.expect_err("two repositories may not share a root");
assert!(
matches!(error, RunnerRootError::Overlaps { .. }),
"got {error}"
);
}
#[test]
fn a_root_does_not_overlap_itself_when_it_is_revalidated() {
let fixture = fixture();
let root = fixture.workspaces.join("acme");
std::fs::create_dir(&root).expect("the root is created");
let owner = RootOwner::Repository("acme/widgets".to_string());
RootPreflight::new(&fixture.paths)
.against(owner.clone(), native(&root))
.check(&owner, &native(&root))
.expect("re-checking a stored setting must not report it against itself");
}
#[test]
fn a_row_written_on_another_operating_system_fails_closed() {
let fixture = fixture();
let error = RootPreflight::new(&fixture.paths)
.check(&RootOwner::Host, &foreign())
.expect_err("a foreign path is corrupt state on this host");
assert!(
matches!(error, RunnerRootError::ForeignPlatform { .. }),
"got {error}"
);
}
#[test]
fn a_remote_filesystem_is_refused() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::create_dir(&root).expect("the root is created");
let probe = StubFilesystem::saying(FilesystemIdentity::remote("nfs"));
let error = RootPreflight::with_probe(&fixture.paths, &probe)
.check(&RootOwner::Host, &native(&root))
.expect_err("a network share may not hold runner workspaces");
assert!(
matches!(error, RunnerRootError::RemoteFilesystem { .. }),
"got {error}"
);
}
#[test]
fn a_filesystem_this_host_cannot_classify_fails_closed() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::create_dir(&root).expect("the root is created");
let probe =
StubFilesystem::saying(FilesystemIdentity::unprovable("filesystem type 0x00001234"));
let error = RootPreflight::with_probe(&fixture.paths, &probe)
.check(&RootOwner::Host, &native(&root))
.expect_err("unprovable locality is a refusal, not a shrug");
assert!(
matches!(error, RunnerRootError::UnprovableFilesystem { .. }),
"got {error}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_refusal_the_superuser_received_names_the_privacy_control_not_the_permissions() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let probe = StubFilesystem::unwritable_to_the_superuser();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&existing))
.expect_err("a root the service cannot write is unusable");
assert!(
matches!(error, RunnerRootError::DeniedByPrivacyPolicy { .. }),
"a superuser cannot be refused by file permissions, so this is the \
privacy layer: {error}"
);
let rendered = error.to_string();
assert!(
rendered.contains("Full Disk Access"),
"the refusal must name the control that grants it: {rendered}"
);
assert!(
rendered.contains(&RootOwner::Host.remediation()),
"the refusal must still show the command that moves the root: {rendered}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_superuser_refused_on_the_startup_disk_is_not_blamed_on_the_privacy_layer() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let probe = StubFilesystem::unwritable_to_the_superuser_on_the_startup_disk();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&existing))
.expect_err("an unwritable root is unusable");
assert!(
matches!(error, RunnerRootError::NotWritable { .. }),
"the privacy layer gates no directory on the startup disk: {error}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_read_only_volume_is_not_blamed_on_the_privacy_layer() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let probe = StubFilesystem::read_only_gated_volume();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&existing))
.expect_err("a read-only root is unusable");
assert!(
matches!(error, RunnerRootError::NotWritable { .. }),
"consent cannot make a read-only mount writable: {error}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_privacy_refusal_names_the_root_that_was_asked_for_and_the_one_that_refused() {
let fixture = fixture();
let leaf = fixture.workspaces.join("not-created-yet");
let probe = StubFilesystem::unwritable_to_the_superuser();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&leaf))
.expect_err("a root the service cannot create is unusable");
let rendered = error.to_string();
assert!(
rendered.contains("not-created-yet"),
"the root the operator asked for is missing from the refusal: {rendered}"
);
assert!(
rendered.contains(&fixture.workspaces.display().to_string()),
"the directory that actually refused is missing from the refusal: {rendered}"
);
}
#[test]
fn a_refusal_an_ordinary_account_received_still_names_file_permissions() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let probe = StubFilesystem::unwritable();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&existing))
.expect_err("an unwritable root is unusable");
assert!(
matches!(error, RunnerRootError::NotWritable { .. }),
"got {error}"
);
}
#[test]
fn an_unwritable_directory_and_an_unwritable_parent_are_reported_apart() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let missing = fixture.workspaces.join("other");
let probe = StubFilesystem::unwritable();
let preflight = RootPreflight::with_probe(&fixture.paths, &probe);
let error = preflight
.check(&RootOwner::Host, &native(&existing))
.expect_err("an unwritable root is unusable");
assert!(
matches!(error, RunnerRootError::NotWritable { .. }),
"got {error}"
);
assert!(
error.to_string().contains(&RootOwner::Host.remediation()),
"the refusal must show the command that fixes it: {error}"
);
let error = preflight
.check(&RootOwner::Host, &native(&missing))
.expect_err("an unwritable parent cannot hold a new leaf");
let RunnerRootError::ParentNotWritable { parent, leaf, .. } = &error else {
panic!("expected ParentNotWritable, got {error}");
};
assert_eq!(parent, &fixture.workspaces);
assert_eq!(leaf, &missing);
assert!(
error.to_string().contains(&RootOwner::Host.remediation()),
"the refusal must show the command that fixes it: {error}"
);
let repository = RootOwner::Repository("acme/widgets".to_string());
let error = preflight
.check(&repository, &native(&missing))
.expect_err("an unwritable parent cannot hold a new leaf");
assert!(
error.to_string().contains(&repository.remediation()),
"a repository root must name its own command: {error}"
);
}
#[test]
fn nothing_is_created_removed_or_repermissioned_by_any_verdict() {
let fixture = fixture();
let existing = fixture.workspaces.join("rman");
std::fs::create_dir(&existing).expect("the root is created");
let file = fixture.workspaces.join("notes.txt");
std::fs::write(&file, b"operator data").expect("the file is created");
let before = snapshot(fixture.root.path());
let stub = StubFilesystem::unwritable();
let host_preflight = RootPreflight::new(&fixture.paths);
let stub_preflight = RootPreflight::with_probe(&fixture.paths, &stub);
for preflight in [&host_preflight, &stub_preflight] {
for candidate in [
existing.clone(),
fixture.workspaces.join("missing"),
fixture.workspaces.join("a").join("b"),
file.clone(),
file.join("leaf"),
fixture.paths.state_dir().to_path_buf(),
] {
let _ = preflight.check(&RootOwner::Host, &native(&candidate));
}
}
assert_eq!(
before,
snapshot(fixture.root.path()),
"the preflight changed the filesystem"
);
}
#[test]
fn this_machines_temporary_directory_is_local_and_writable() {
let root = tempfile::tempdir().expect("a temporary directory");
let canonical = plain(&std::fs::canonicalize(root.path()).expect("it exists"));
let identity = HostFilesystem
.identify(&canonical)
.expect("the platform answers");
assert_eq!(
identity.locality,
Locality::Local,
"the suite's own temporary directory reported {identity:?}; a CI leg whose \
temporary filesystem is unknown to this table would refuse every runner root"
);
assert!(
HostFilesystem
.is_writable(&canonical)
.expect("the platform answers"),
"a directory this process just created must be writable"
);
}
#[cfg(windows)]
#[test]
fn the_system_drive_root_is_writable_when_a_directory_can_be_created_in_it() {
let paths = AppPaths::rooted_at(Path::new("C:\\does-not-matter"));
let default = default_runner_root(&paths).expect("this host has a system directory");
let parent = default
.as_path()
.parent()
.expect("the default root is one level below the system drive")
.to_path_buf();
let probe = parent.join(format!("rman-preflight-probe-{}", std::process::id()));
if std::fs::create_dir(&probe).is_err() {
return;
}
let writable = HostFilesystem.is_writable(&parent);
std::fs::remove_dir(&probe).expect("the probe is removed");
assert!(
writable.expect("the platform answers"),
"{} accepts a new directory, so the preflight must not refuse the default \
runner root as unwritable",
parent.display()
);
}
#[cfg(unix)]
#[test]
fn a_directory_this_account_cannot_write_is_reported_as_such() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("a temporary directory");
let locked = root.path().join("locked");
std::fs::create_dir(&locked).expect("the directory is created");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555))
.expect("the directory is made unwritable");
let probe = locked.join("probe");
let is_root = std::fs::create_dir(&probe).is_ok();
if is_root {
std::fs::remove_dir(&probe).expect("the probe is removed");
}
let writable = HostFilesystem.is_writable(&locked);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755))
.expect("the directory is restored");
if is_root {
return;
}
assert!(
!writable.expect("the platform answers"),
"a 0555 directory must not be reported writable"
);
}
#[test]
fn a_derived_child_is_one_component_below_the_root() {
let root = LocalAbsolutePath::parse_for("/srv/rman", Unix).expect("a valid root");
assert_eq!(
derive_child(&root, "s1").expect("a valid slot").as_str(),
"/srv/rman/s1"
);
let root = LocalAbsolutePath::parse_for("C:\\rman", Windows).expect("a valid root");
assert_eq!(
derive_child(&root, "0123456789ab")
.expect("a valid attempt")
.as_str(),
"C:\\rman\\0123456789ab"
);
for name in ["..", "a/b", "", "."] {
assert!(
derive_child(&root, name).is_err(),
"{name:?} must not be a derived child"
);
}
}
#[test]
fn containment_is_proven_lexically_and_after_resolution() {
let fixture = fixture();
let directory = fixture.workspaces.join("rman");
std::fs::create_dir(&directory).expect("the root is created");
let root = native(&directory);
let slot = derive_child(&root, "s1").expect("a valid slot");
verify_containment(&root, &slot)
.expect("a slot that does not exist yet is still contained");
std::fs::create_dir(slot.as_path()).expect("the slot is created");
verify_containment(&root, &slot).expect("an existing slot is contained");
let sibling = native(&fixture.workspaces.join("elsewhere"));
assert!(
matches!(
verify_containment(&root, &sibling),
Err(RunnerRootError::Escapes { .. })
),
"a sibling is not contained"
);
assert!(
matches!(
verify_containment(&root, &root),
Err(RunnerRootError::Escapes { .. })
),
"the root is not strictly inside itself"
);
}
#[test]
fn a_link_inside_the_root_that_points_outside_it_is_not_contained() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::create_dir(&root).expect("the root is created");
let outside = fixture.workspaces.join("outside");
std::fs::create_dir(&outside).expect("the escape target is created");
let escape = root.join("s1");
if !link_dir(&outside, &escape) {
return;
}
let error = verify_containment(&native(&root), &native(&escape))
.expect_err("cleanup may not follow a link out of the root it was given");
assert!(
matches!(error, RunnerRootError::Escapes { .. }),
"got {error}"
);
}
#[test]
fn each_owner_names_the_command_that_changes_it() {
assert_eq!(
RootOwner::Host.remediation(),
"runner-manager host set-runtime-root --path <PATH>"
);
assert_eq!(
RootOwner::Repository("acme/widgets".to_string()).remediation(),
"runner-manager repo set-workspace acme/widgets --mode persistent --path <PATH>"
);
assert!(RootOwner::Host.to_string().contains("host runner root"));
assert!(
RootOwner::Repository("acme/widgets".to_string())
.to_string()
.contains("acme/widgets")
);
}
#[test]
fn a_refusal_names_the_paths_and_says_what_to_do_about_it() {
let fixture = fixture();
let root = fixture.workspaces.join("rman");
std::fs::create_dir(&root).expect("the root is created");
let probe = StubFilesystem::saying(FilesystemIdentity::remote("nfs"));
let message = RootPreflight::with_probe(&fixture.paths, &probe)
.check(&RootOwner::Host, &native(&root))
.expect_err("a network share is refused")
.to_string();
assert!(message.contains("nfs"), "{message}");
assert!(message.contains("local volume"), "{message}");
let message = fixture
.check(fixture.paths.state_dir())
.expect_err("application data is protected")
.to_string();
assert!(
message.contains(&fixture.paths.state_dir().display().to_string()),
"the directory that refused must be named: {message}"
);
assert!(
message.contains("the application state directory"),
"the operator must be told what it collided with: {message}"
);
assert!(
message.contains("outside the application data tree"),
"the message must say what to do instead: {message}"
);
}
}