use crate::context::{Context, Environment};
use crate::error::Result;
use crate::service::Permit;
use crate::store::RawStore;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub(crate) mod administered;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(any(test, feature = "test-support"))]
pub mod memory;
pub(crate) mod program;
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
#[cfg(target_os = "linux")]
use linux as os;
#[cfg(target_os = "macos")]
use macos as os;
#[cfg(windows)]
use windows as os;
pub(crate) use administered::Administered;
pub(crate) use os::{fs, proc, user};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Os {
MacOs,
Linux,
Windows,
}
pub const OS: Os = os::OS;
pub const SECURITY: &str = "/usr/bin/security";
impl Os {
pub fn secrets_tool(self) -> Option<&'static str> {
match self {
Os::MacOs => Some(SECURITY),
Os::Linux | Os::Windows => None,
}
}
pub fn make_private_command(self, kind: Kind, paths: &[&str]) -> Option<String> {
match self {
Os::MacOs | Os::Linux => {
let mode = match kind {
Kind::Directory => "700",
Kind::File => "600",
Kind::Any => "go-rwx",
};
Some(format!("chmod {mode} {}", paths.join(" ")))
}
Os::Windows => None,
}
}
pub fn guarded_folders(self) -> &'static [&'static str] {
match self {
Os::MacOs => &[
"Desktop",
"Documents",
"Downloads",
"Library/Mobile Documents",
"Library/CloudStorage",
],
Os::Linux | Os::Windows => &[],
}
}
pub fn package_bins(self) -> &'static [&'static str] {
match self {
Os::MacOs => &["/opt/homebrew/bin", "/usr/local/bin"],
Os::Linux | Os::Windows => &[],
}
}
pub(crate) fn same_path(self, a: &Path, b: &Path) -> bool {
match self {
Os::MacOs | Os::Linux => a == b,
Os::Windows => same_path_in_any_case(a, b),
}
}
pub fn app_command_line(self, app: &Path) -> Option<PathBuf> {
match self {
Os::MacOs => (app.extension() == Some("app".as_ref()))
.then(|| app.join("Contents/Helpers/pitboard")),
Os::Linux | Os::Windows => None,
}
}
}
pub fn same_path_in_any_case(a: &Path, b: &Path) -> bool {
let (mut a, mut b) = (a.components(), b.components());
loop {
match (a.next(), b.next()) {
(None, None) => return true,
(Some(one), Some(other))
if same_name_in_any_case(one.as_os_str(), other.as_os_str()) => {}
_ => return false,
}
}
}
fn same_name_in_any_case(a: &std::ffi::OsStr, b: &std::ffi::OsStr) -> bool {
fn upper(c: char) -> char {
if u32::from(c) > 0xFFFF {
return c;
}
let mut upper = c.to_uppercase();
match (upper.next(), upper.next()) {
(Some(one), None) => one,
_ => c,
}
}
let (a, b) = (a.to_string_lossy(), b.to_string_lossy());
a.chars().map(upper).eq(b.chars().map(upper))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
windows,
allow(
dead_code,
reason = "only a login shell says or is late, and Windows has none; W17 says which \
`PATH` an app finds programs on there"
)
)]
pub(crate) enum LoginPath {
Said(String),
Unknown,
Late,
}
pub(crate) fn login_path(env: &Environment) -> LoginPath {
os::login_path(env)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Access {
pub shared: bool,
pub described: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Directory,
File,
Any,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Elevation {
Normal,
Elevated { why: &'static str },
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Process {
pub pid: u32,
pub path: PathBuf,
}
pub(crate) trait Host: Send + Sync + std::fmt::Debug {
fn foreign_secrets(&self, ctx: &Context, account: &str) -> Option<Box<dyn RawStore>>;
fn file(&self, path: PathBuf) -> Box<dyn RawStore>;
fn vault(&self, ctx: &Context) -> Box<dyn RawStore>;
fn vault_is_shared(&self) -> bool;
fn processes(&self, program: &str) -> Option<Vec<Process>>;
fn scheduler(&self) -> Option<&dyn Scheduler>;
fn elevation(&self, ctx: &Context) -> Elevation;
fn administered_file(&self, path: &Path) -> Administered {
if administered::READ_BY_REAL_HOSTS {
administered::read_text(path)
} else {
Administered::Unset
}
}
fn managed_preference(&self, domain: &str, key: &str) -> Administered;
}
pub(crate) trait Scheduler: Send + Sync + std::fmt::Debug {
fn location(&self, ctx: &Context) -> PathBuf;
fn installed(&self, ctx: &Context) -> bool;
fn program(&self, ctx: &Context) -> Option<PathBuf>;
fn environment(&self, ctx: &Context) -> Vec<(String, String)>;
fn put(
&self,
ctx: &Context,
permit: Permit,
program: &Path,
environment: &[(String, String)],
) -> Result<()>;
fn remove(&self, ctx: &Context, permit: Permit) -> Result<bool>;
fn started_this_process(&self, said: Option<&str>) -> bool;
}
pub(crate) fn default_pitboard_home(home: &Path) -> PathBuf {
match OS {
Os::MacOs | Os::Linux => home.join(".pitboard"),
Os::Windows => PathBuf::new(),
}
}
pub(crate) fn current() -> Arc<dyn Host> {
os::host()
}
pub(crate) fn current_program() -> std::io::Result<PathBuf> {
os::current_program()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_store_of_secrets_is_offered_only_where_there_is_one() {
let ctx = Context::for_unit_test();
let offered = ctx.host().foreign_secrets(&ctx, "someone");
assert_eq!(
offered.map(|k| k.kind()),
match OS {
Os::MacOs => Some(crate::store::Backend::Keychain),
Os::Linux => None,
Os::Windows => None,
}
);
assert_eq!(
ctx.host().file(PathBuf::from("/nowhere/at/all")).kind(),
crate::store::Backend::File
);
}
#[test]
fn paths_are_compared_exactly_on_macos_and_linux() {
for os in [Os::MacOs, Os::Linux] {
let same = |a: &str, b: &str| os.same_path(Path::new(a), Path::new(b));
assert!(same("ChatGPT.app", "ChatGPT.app"), "{os:?}");
assert!(
same("/Applications/ChatGPT.app/", "/Applications//ChatGPT.app"),
"{os:?}"
);
assert!(!same("chatgpt.app", "ChatGPT.app"), "{os:?}");
assert!(!same("ChatGPT.app", "ChatGPT"), "{os:?}");
}
}
#[test]
fn paths_are_compared_in_any_case_on_windows() {
let same = |a: &str, b: &str| Os::Windows.same_path(Path::new(a), Path::new(b));
assert!(same("Codex.exe", "codex.EXE"));
assert!(same("/Programs/OpenAI/Codex/", "/programs//openai/CODEX"));
assert!(same(r"C:\Users\Dana\Codex.exe", r"c:\users\dana\codex.exe"));
assert!(same("Éclair", "éCLAIR"));
assert!(!same("e\u{301}clair", "\u{e9}clair"), "not normalised");
assert!(!same("Straße", "STRASSE"), "ß is upper case as it is");
assert!(!same("Codex.exe", "Codex"));
assert!(!same("/a/b", "/a/b/c"));
}
#[test]
fn a_scheduler_is_offered_where_pitboard_writes_for_one() {
assert_eq!(
current().scheduler().is_some(),
match OS {
Os::MacOs | Os::Linux => true,
Os::Windows => false,
}
);
}
#[test]
fn the_host_reads_sudo_from_the_context_and_root_from_the_process() {
let under = |value: &str| {
let env: Environment = [("HOME", "/Users/x"), ("SUDO_UID", value)]
.into_iter()
.collect();
let ctx = Context::for_command_line(&env);
ctx.host().elevation(&ctx)
};
assert_eq!(
under("501"),
match OS {
Os::MacOs | Os::Linux => Elevation::Elevated { why: "with sudo" },
Os::Windows => Elevation::Unknown,
}
);
let ctx = Context::for_unit_test();
assert_eq!(
ctx.host().elevation(&ctx),
user::elevation(false),
"withheld from a unit test, so only root is read"
);
assert_eq!(under(""), user::elevation(false));
}
}