use std::fmt;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
pub const SUITE_DIR_NAME: &str = "Orthic Labs";
pub const SHARED_DIR_NAME: &str = "shared";
pub const ENV_SUITE_ROOT: &str = "RIGHTKIT_SUITE_ROOT";
#[derive(Debug)]
pub enum SuiteError {
NoRoot(String),
NotAbsolute(PathBuf),
Insecure(String),
Io(io::Error),
}
impl fmt::Display for SuiteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SuiteError::NoRoot(s) => write!(
f,
"no app-data root: {s} (refusing to fall back to the working directory)"
),
SuiteError::NotAbsolute(p) => write!(f, "suite root must be absolute: {}", p.display()),
SuiteError::Insecure(s) => write!(f, "insecure service directory: {s}"),
SuiteError::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SuiteError {}
impl From<io::Error> for SuiteError {
fn from(e: io::Error) -> Self {
SuiteError::Io(e)
}
}
struct TempGuard(PathBuf);
impl Drop for TempGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[derive(Clone)]
pub struct Suite {
root: PathBuf,
_temp: Option<Arc<TempGuard>>,
env_derived: bool,
}
impl fmt::Debug for Suite {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Suite({})", self.root.display())
}
}
fn absolute(p: PathBuf) -> Result<PathBuf, SuiteError> {
if !p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) {
return Err(SuiteError::NotAbsolute(p));
}
Ok(p)
}
#[cfg(feature = "test-mode")]
fn env_nonempty(k: &str) -> Option<String> {
std::env::var(k).ok().filter(|v| !v.trim().is_empty())
}
impl Suite {
pub fn resolve() -> Result<Suite, SuiteError> {
#[cfg(feature = "test-mode")]
{
let mut suite = Self::resolve_from_env()?;
suite.env_derived = true;
Ok(suite)
}
#[cfg(not(feature = "test-mode"))]
{
Self::per_user_default()
}
}
pub fn per_user_default() -> Result<Suite, SuiteError> {
let base = platform_default_base()?;
Suite::at(absolute(base)?.join(SUITE_DIR_NAME).join(SHARED_DIR_NAME))
}
#[deprecated(
since = "0.1.6",
note = "use rightkit_daemon::mode::{decide, roots}: release builds read no override selector, test builds fail closed on a validated TestRoot"
)]
pub fn resolve_ignoring_env_overrides(ignore_env_overrides: bool) -> Result<Suite, SuiteError> {
#[cfg(feature = "test-mode")]
{
if ignore_env_overrides {
Self::per_user_default()
} else {
Self::resolve()
}
}
#[cfg(not(feature = "test-mode"))]
{
let _ = ignore_env_overrides;
Self::per_user_default()
}
}
pub fn env_derived(&self) -> bool {
self.env_derived
}
#[cfg(feature = "test-mode")]
fn resolve_from_env() -> Result<Suite, SuiteError> {
for key in [ENV_SUITE_ROOT, "RIGHT_SUITE_DIR", "ORTHIC_SHARED_DIR"] {
if let Some(v) = env_nonempty(key) {
return Suite::at(v);
}
}
let base = if cfg!(windows) {
env_nonempty("LOCALAPPDATA")
.map(PathBuf::from)
.ok_or_else(|| SuiteError::NoRoot("LOCALAPPDATA is not set".into()))?
} else if cfg!(target_os = "macos") {
env_nonempty("HOME")
.map(|h| PathBuf::from(h).join("Library").join("Application Support"))
.ok_or_else(|| SuiteError::NoRoot("HOME is not set".into()))?
} else {
env_nonempty("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| {
env_nonempty("HOME").map(|h| PathBuf::from(h).join(".local").join("share"))
})
.ok_or_else(|| SuiteError::NoRoot("neither XDG_DATA_HOME nor HOME is set".into()))?
};
Suite::at(absolute(base)?.join(SUITE_DIR_NAME).join(SHARED_DIR_NAME))
}
pub fn at(root: impl Into<PathBuf>) -> Result<Suite, SuiteError> {
Ok(Suite {
root: absolute(root.into())?,
_temp: None,
env_derived: false,
})
}
pub fn per_run_temp(label: &str) -> Result<Suite, SuiteError> {
static N: AtomicU64 = AtomicU64::new(0);
let label: String = label
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.take(24)
.collect();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = absolute(std::env::temp_dir())?.join(format!(
"rk-{label}-{}-{}-{nanos:x}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
make_private_dir(&dir)?;
Ok(Suite {
root: dir.clone(),
_temp: Some(Arc::new(TempGuard(dir))),
env_derived: false,
})
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn service_dir(&self, service: &str) -> PathBuf {
self.root.join("runtime").join(service)
}
pub fn private_service_subdir(&self, service: &str, name: &str) -> Result<PathBuf, SuiteError> {
let d = self.service_dir(service).join(name);
make_private_dir(&d)?;
Ok(d)
}
pub(crate) fn app_parent(&self) -> PathBuf {
let shared = self.root.file_name().is_some_and(|n| n == SHARED_DIR_NAME);
let parent = self.root.parent();
match parent {
Some(p) if shared && p.file_name().is_some_and(|n| n == SUITE_DIR_NAME) => {
p.to_path_buf()
}
_ => self.root.join("apps"),
}
}
pub fn data_dir(&self, area: &str) -> Result<PathBuf, SuiteError> {
let d = self.root.join(area);
make_private_dir(&d)?;
Ok(d)
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn platform_default_base() -> Result<PathBuf, SuiteError> {
let home = passwd_home_dir()?;
Ok(if cfg!(target_os = "macos") {
home.join("Library").join("Application Support")
} else {
home.join(".local").join("share")
})
}
#[cfg(windows)]
fn platform_default_base() -> Result<PathBuf, SuiteError> {
use std::ffi::{c_void, OsString};
use std::os::windows::ffi::OsStringExt;
#[repr(C)]
#[allow(dead_code)]
struct Guid(u32, u16, u16, [u8; 8]);
const LOCAL_APP_DATA: Guid = Guid(
0xF1B3_2785,
0x6FBA,
0x4FCF,
[0x9D, 0x55, 0x7B, 0x8E, 0x7F, 0x15, 0x70, 0x91],
);
#[link(name = "shell32")]
unsafe extern "system" {
fn SHGetKnownFolderPath(
id: *const Guid,
flags: u32,
token: *mut c_void,
path: *mut *mut u16,
) -> i32;
}
#[link(name = "ole32")]
unsafe extern "system" {
fn CoTaskMemFree(memory: *mut c_void);
}
let mut text: *mut u16 = std::ptr::null_mut();
let status =
unsafe { SHGetKnownFolderPath(&LOCAL_APP_DATA, 0, std::ptr::null_mut(), &mut text) };
let folder = if status >= 0 && !text.is_null() {
let mut len = 0;
while unsafe { *text.add(len) } != 0 {
len += 1;
}
Some(PathBuf::from(OsString::from_wide(unsafe {
std::slice::from_raw_parts(text, len)
})))
} else {
None
};
unsafe { CoTaskMemFree(text.cast()) };
folder.filter(|path| path.is_absolute()).ok_or_else(|| {
SuiteError::NoRoot(format!(
"the profile's local application data folder is unavailable (SHGetKnownFolderPath: 0x{status:08X})"
))
})
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn platform_default_base() -> Result<PathBuf, SuiteError> {
Err(SuiteError::NoRoot(
"no environment-independent per-user root is implemented for this platform".into(),
))
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub(crate) fn passwd_home_dir() -> Result<PathBuf, SuiteError> {
use std::ffi::{c_char, c_int, CStr, OsStr};
use std::os::unix::ffi::OsStrExt;
#[repr(C)]
#[allow(dead_code)]
struct Passwd {
pw_name: *mut c_char,
pw_passwd: *mut c_char,
pw_uid: u32,
pw_gid: u32,
#[cfg(target_os = "macos")]
pw_change: i64,
#[cfg(target_os = "macos")]
pw_class: *mut c_char,
pw_gecos: *mut c_char,
pw_dir: *mut c_char,
pw_shell: *mut c_char,
#[cfg(target_os = "macos")]
pw_expire: i64,
}
extern "C" {
fn getpwuid_r(
uid: u32,
pwd: *mut Passwd,
buf: *mut c_char,
buflen: usize,
result: *mut *mut Passwd,
) -> c_int;
}
const ERANGE: c_int = 34;
let uid = crate::transport::current_uid();
let mut buf = vec![0u8; 1024];
loop {
let mut pwd = std::mem::MaybeUninit::<Passwd>::zeroed();
let mut result: *mut Passwd = std::ptr::null_mut();
let rc = unsafe {
getpwuid_r(
uid,
pwd.as_mut_ptr(),
buf.as_mut_ptr().cast::<c_char>(),
buf.len(),
&mut result,
)
};
if rc == ERANGE && buf.len() < (1 << 20) {
buf = vec![0u8; buf.len() * 4];
continue;
}
if rc != 0 {
return Err(SuiteError::NoRoot(format!(
"getpwuid_r failed: {}",
io::Error::from_raw_os_error(rc)
)));
}
if result.is_null() {
return Err(SuiteError::NoRoot(
"the account database has no entry for the current user".into(),
));
}
let dir = unsafe { (*result).pw_dir };
if dir.is_null() {
return Err(SuiteError::NoRoot(
"the account database has no home directory for the current user".into(),
));
}
let bytes = unsafe { CStr::from_ptr(dir) }.to_bytes();
return Ok(PathBuf::from(OsStr::from_bytes(bytes)));
}
}
pub const MAX_SERVICE_NAME_LEN: usize = 48;
pub fn valid_service_name(s: &str) -> bool {
!s.is_empty()
&& s.len() <= MAX_SERVICE_NAME_LEN
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
pub(crate) fn make_private_dir(dir: &Path) -> Result<(), SuiteError> {
std::fs::create_dir_all(dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let md = std::fs::metadata(dir)?;
if md.uid() != crate::transport::current_uid() {
return Err(SuiteError::Insecure(format!(
"{} is not owned by the current user",
dir.display()
)));
}
if md.permissions().mode() & 0o077 != 0 {
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
}
}
Ok(())
}
#[cfg(test)]
#[allow(deprecated)] mod tests {
use super::*;
#[test]
fn only_resolve_is_marked_environment_derived() {
#[cfg(not(feature = "test-mode"))]
{
let suite = Suite::resolve().unwrap();
assert!(!suite.env_derived());
assert_eq!(suite.root(), Suite::per_user_default().unwrap().root());
}
let dir = tempfile::tempdir().unwrap();
assert!(!Suite::at(dir.path()).unwrap().env_derived());
assert!(!Suite::per_run_temp("env-flag").unwrap().env_derived());
#[cfg(feature = "test-mode")]
if let Ok(suite) = Suite::resolve() {
assert!(suite.env_derived());
assert!(suite.clone().env_derived(), "the mark survives a clone");
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn the_per_user_default_comes_from_the_account_database_under_orthic_labs_shared() {
let suite = Suite::per_user_default().unwrap();
assert!(!suite.env_derived());
assert!(suite.root().is_absolute());
assert!(
suite
.root()
.ends_with(Path::new(SUITE_DIR_NAME).join(SHARED_DIR_NAME)),
"{:?}",
suite.root()
);
let home = passwd_home_dir().unwrap();
assert!(home.is_absolute());
assert!(suite.root().starts_with(&home));
assert_eq!(
Suite::resolve_ignoring_env_overrides(true).unwrap().root(),
suite.root()
);
}
#[cfg(windows)]
#[test]
fn the_per_user_default_comes_from_the_known_folder_table_not_the_environment() {
let suite = Suite::per_user_default().unwrap();
assert!(!suite.env_derived());
assert!(suite.root().is_absolute());
assert!(
suite
.root()
.ends_with(Path::new(SUITE_DIR_NAME).join(SHARED_DIR_NAME)),
"{:?}",
suite.root()
);
let base = platform_default_base().unwrap();
assert!(suite.root().starts_with(&base));
assert!(base.is_dir(), "{base:?}");
assert_eq!(
Suite::resolve_ignoring_env_overrides(true).unwrap().root(),
suite.root()
);
}
}