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)
}
impl Suite {
pub fn resolve() -> Result<Suite, SuiteError> {
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}")]
pub fn resolve_ignoring_env_overrides(ignore_env_overrides: bool) -> Result<Suite, SuiteError> {
let _ = ignore_env_overrides;
Self::per_user_default()
}
pub fn env_derived(&self) -> bool {
self.env_derived
}
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> {
Ok(account_profile_dir()?.join("AppData").join("Local"))
}
#[cfg(windows)]
pub(crate) fn account_profile_dir() -> Result<PathBuf, SuiteError> {
use std::ffi::{c_void, OsString};
use std::os::windows::ffi::OsStringExt;
const TOKEN_QUERY: u32 = 0x0008;
const ERROR_INSUFFICIENT_BUFFER: i32 = 122;
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetCurrentProcess() -> *mut c_void;
fn CloseHandle(handle: *mut c_void) -> i32;
}
#[link(name = "advapi32")]
unsafe extern "system" {
fn OpenProcessToken(process: *mut c_void, access: u32, token: *mut *mut c_void) -> i32;
}
#[link(name = "userenv")]
unsafe extern "system" {
fn GetUserProfileDirectoryW(token: *mut c_void, buf: *mut u16, size: *mut u32) -> i32;
}
struct Token(*mut c_void);
impl Drop for Token {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
}
let mut raw: *mut c_void = std::ptr::null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut raw) } == 0 {
return Err(SuiteError::NoRoot(format!(
"the process token is unavailable: {}",
io::Error::last_os_error()
)));
}
let token = Token(raw);
let mut buf = vec![0u16; 260];
for _ in 0..2 {
let mut size = buf.len() as u32;
let ok = unsafe { GetUserProfileDirectoryW(token.0, buf.as_mut_ptr(), &mut size) };
if ok != 0 {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
let dir = PathBuf::from(OsString::from_wide(&buf[..len]));
return if dir.is_absolute() {
Ok(dir)
} else {
Err(SuiteError::NoRoot(
"the account's profile directory is not absolute".into(),
))
};
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_INSUFFICIENT_BUFFER) && size as usize > buf.len() {
buf = vec![0u16; size as usize];
continue;
}
return Err(SuiteError::NoRoot(format!(
"the account's profile directory is unavailable: {error}"
)));
}
Err(SuiteError::NoRoot(
"the account's profile directory changed size during the lookup".into(),
))
}
#[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> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let Ok(md) = std::fs::symlink_metadata(dir) {
if md.uid() != crate::transport::current_uid() {
return Err(SuiteError::Insecure(format!(
"{} is not owned by the current user",
dir.display()
)));
}
}
}
rightkit_fs::private::ensure_private_dir(dir).map_err(suite_fs_error)?;
rightkit_fs::private::verify_owner_only(dir).map_err(suite_fs_error)
}
fn suite_fs_error(error: rightkit_fs::FsError) -> SuiteError {
match error {
rightkit_fs::FsError::Io { source, .. } => SuiteError::Io(source),
other => SuiteError::Insecure(other.to_string()),
}
}