use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::suite::{Suite, SuiteError, SUITE_DIR_NAME};
pub const TEST_CAPABILITY: bool = cfg!(feature = "test-mode");
#[cfg(feature = "test-mode")]
pub const TEST_MODE_MARKER_TEXT: &str = "RIGHTKIT-TEST-MODE-COMPILED";
#[cfg(feature = "test-mode")]
#[used]
#[no_mangle]
#[cfg_attr(target_os = "macos", link_section = "__DATA,__rkit_tm")]
#[cfg_attr(windows, link_section = ".rkittm")]
#[cfg_attr(not(any(target_os = "macos", windows)), link_section = ".rkit_tm")]
pub static TEST_MODE_MARKER: [u8; 27] = *b"RIGHTKIT-TEST-MODE-COMPILED";
#[cfg(feature = "test-mode")]
const SELECTOR_ACTIVATION: &str = "RIGHTKIT_UNDER_TEST";
#[cfg(feature = "test-mode")]
const SELECTOR_ROOT: &str = "RIGHTKIT_QA_DATA_DIR";
#[cfg(feature = "test-mode")]
const BINDING_FILE: &str = ".rightkit-app.json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AppId {
id: &'static str,
dir_name: &'static str,
}
impl AppId {
pub const fn new(id: &'static str) -> AppId {
AppId { id, dir_name: id }
}
pub const fn with_dir(id: &'static str, dir_name: &'static str) -> AppId {
AppId { id, dir_name }
}
pub const fn id(&self) -> &'static str {
self.id
}
pub const fn dir_name(&self) -> &'static str {
self.dir_name
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
ActivationWithoutRoot,
RootWithoutActivation,
RootNotAbsolute,
RootNotDirectory,
RootHasSymlink,
RootOverlapsUserData,
RootOnRemovableOrCloud,
RootNotPrivate,
RootNotCreatedByRunner,
BindingAppMismatch,
UserRootUnknown,
Io(String),
}
impl Refusal {
pub fn rule(&self) -> &'static str {
match self {
Refusal::ActivationWithoutRoot => "activation-without-root",
Refusal::RootWithoutActivation => "root-without-activation",
Refusal::RootNotAbsolute => "root-not-absolute",
Refusal::RootNotDirectory => "root-not-directory",
Refusal::RootHasSymlink => "root-has-symlink",
Refusal::RootOverlapsUserData => "root-overlaps-user-data",
Refusal::RootOnRemovableOrCloud => "root-on-removable-or-cloud",
Refusal::RootNotPrivate => "root-not-private",
Refusal::RootNotCreatedByRunner => "root-not-created-by-runner",
Refusal::BindingAppMismatch => "binding-app-mismatch",
Refusal::UserRootUnknown => "user-root-unknown",
Refusal::Io(_) => "io",
}
}
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Refusal::Io(e) => write!(f, "test mode refused ({}): {e}", self.rule()),
_ => write!(f, "test mode refused ({})", self.rule()),
}
}
}
impl std::error::Error for Refusal {}
impl From<io::Error> for Refusal {
fn from(e: io::Error) -> Self {
Refusal::Io(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct TestRoot {
path: PathBuf,
suite: Suite,
#[cfg_attr(not(feature = "test-mode"), allow(dead_code))]
app_id: &'static str,
}
impl TestRoot {
pub fn path(&self) -> &Path {
&self.path
}
pub fn suite(&self) -> Suite {
self.suite.clone()
}
pub fn app_dir(&self, app: &AppId) -> PathBuf {
self.path.join(app.dir_name)
}
}
#[derive(Debug, Clone)]
pub enum Mode {
Release,
Test(TestRoot),
}
impl Mode {
pub fn is_test(&self) -> bool {
matches!(self, Mode::Test(_))
}
pub fn test_root(&self) -> Option<&TestRoot> {
match self {
Mode::Test(root) => Some(root),
Mode::Release => None,
}
}
#[cfg(feature = "test-mode")]
#[doc(hidden)]
pub fn selector_env(&self, name: &str) -> Option<String> {
match self {
Mode::Test(_) => std::env::var(name).ok().filter(|v| !v.trim().is_empty()),
Mode::Release => None,
}
}
}
#[cfg(feature = "test-mode")]
#[macro_export]
macro_rules! test_selector {
($mode:expr, $name:literal) => {
$crate::mode::Mode::selector_env(&$mode, $name)
};
}
#[cfg(not(feature = "test-mode"))]
#[macro_export]
macro_rules! test_selector {
($mode:expr, $name:literal) => {{
let _ = &$mode;
::core::option::Option::<::std::string::String>::None
}};
}
pub trait ModeInput {
fn env(&self, name: &str) -> Option<String>;
fn per_user_default(&self) -> Result<Suite, SuiteError> {
Suite::per_user_default()
}
fn protected_paths(&self, real: &Suite) -> Vec<PathBuf> {
default_protected_paths(real)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ProcessInput;
impl ModeInput for ProcessInput {
fn env(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
}
pub fn default_protected_paths(real: &Suite) -> Vec<PathBuf> {
let mut out = Vec::new();
let Some(orthic) = real.root().parent() else {
return out;
};
out.push(orthic.to_path_buf());
if orthic.file_name().and_then(|n| n.to_str()) == Some(SUITE_DIR_NAME) {
if let Some(home) = orthic.ancestors().nth(3) {
for dir in user_content_dirs() {
out.push(home.join(dir));
}
}
}
out
}
fn user_content_dirs() -> [&'static str; 7] {
[
"Documents",
"Pictures",
"Desktop",
"Downloads",
"Movies",
"Music",
"Videos",
]
}
#[derive(Debug, Clone)]
pub struct AppRoots {
pub suite: Suite,
pub app_dir: PathBuf,
}
pub fn decide(app: &AppId, input: &dyn ModeInput) -> Result<Mode, Refusal> {
#[cfg(feature = "test-mode")]
{
capable::decide(app, input)
}
#[cfg(not(feature = "test-mode"))]
{
let _ = (app, input);
Ok(Mode::Release)
}
}
pub fn init_or_exit(app: &AppId) -> &'static Mode {
static MODE: OnceLock<Mode> = OnceLock::new();
if let Some(mode) = MODE.get() {
return mode;
}
match decide(app, &ProcessInput) {
Ok(mode) => MODE.get_or_init(|| mode),
Err(refusal) => {
eprintln!("{refusal}");
std::process::exit(2);
}
}
}
pub fn roots(app: &AppId, mode: &Mode) -> Result<AppRoots, SuiteError> {
roots_with(app, mode, &ProcessInput)
}
pub fn roots_with(app: &AppId, mode: &Mode, input: &dyn ModeInput) -> Result<AppRoots, SuiteError> {
match mode {
Mode::Release => {
let suite = input.per_user_default()?;
let orthic = suite
.root()
.parent()
.ok_or_else(|| SuiteError::NoRoot("the per-user suite root has no parent".into()))?
.to_path_buf();
Ok(AppRoots {
suite,
app_dir: orthic.join(app.dir_name),
})
}
Mode::Test(root) => Ok(AppRoots {
suite: root.suite(),
app_dir: root.app_dir(app),
}),
}
}
#[cfg(feature = "test-mode")]
mod capable {
use super::*;
use std::fs;
use std::path::Component;
fn truthy(v: &str) -> bool {
let t = v.trim();
!(t.is_empty() || t == "0" || t.eq_ignore_ascii_case("false"))
}
pub(super) fn decide(app: &AppId, input: &dyn ModeInput) -> Result<Mode, Refusal> {
let activated = input
.env(SELECTOR_ACTIVATION)
.filter(|v| truthy(v))
.is_some();
let root = input.env(SELECTOR_ROOT).filter(|v| !v.trim().is_empty());
match (activated, root) {
(false, None) => Ok(Mode::Release),
(false, Some(_)) => Err(Refusal::RootWithoutActivation),
(true, None) => Err(Refusal::ActivationWithoutRoot),
(true, Some(root)) => verify(app, input, &root).map(Mode::Test),
}
}
fn resolve(path: &Path) -> PathBuf {
let mut rest: Vec<std::ffi::OsString> = Vec::new();
let mut cur = path.to_path_buf();
loop {
if let Ok(canon) = fs::canonicalize(&cur) {
let mut out = canon;
for part in rest.iter().rev() {
out.push(part);
}
return out;
}
match (cur.file_name().map(|n| n.to_os_string()), cur.parent()) {
(Some(name), Some(parent)) => {
rest.push(name);
cur = parent.to_path_buf();
}
_ => return path.to_path_buf(),
}
}
}
fn overlaps(root: &Path, protected: &Path) -> bool {
root.starts_with(protected) || protected.starts_with(root)
}
fn on_cloud_folder(path: &Path) -> bool {
path.components().any(|c| match c {
Component::Normal(name) => {
let n = name.to_string_lossy().to_lowercase();
n.starts_with("onedrive")
|| n == "dropbox"
|| n == "icloud drive"
|| n == "mobile documents"
|| n == "google drive"
|| n == "googledrive"
}
_ => false,
})
}
#[cfg(unix)]
fn private_to_user(path: &Path) -> Result<bool, Refusal> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let md = fs::metadata(path)?;
Ok(md.uid() == crate::transport::current_uid() && md.permissions().mode() & 0o077 == 0)
}
#[cfg(not(unix))]
fn private_to_user(_path: &Path) -> Result<bool, Refusal> {
Ok(true)
}
fn check_binding(app: &AppId, canon: &Path) -> Result<(), Refusal> {
let bytes =
fs::read(canon.join(BINDING_FILE)).map_err(|_| Refusal::RootNotCreatedByRunner)?;
let value: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|_| Refusal::RootNotCreatedByRunner)?;
match value.get("app").and_then(|v| v.as_str()) {
None => Err(Refusal::RootNotCreatedByRunner),
Some(bound) if bound == app.id => Ok(()),
Some(_) => Err(Refusal::BindingAppMismatch),
}
}
fn verify(app: &AppId, input: &dyn ModeInput, raw: &str) -> Result<TestRoot, Refusal> {
let path = PathBuf::from(raw.trim());
let parent_dir =
|c: &Component<'_>| matches!(c, Component::ParentDir) || c.as_os_str() == "..";
if !path.is_absolute() || path.components().any(|c| parent_dir(&c)) {
return Err(Refusal::RootNotAbsolute);
}
let md = fs::symlink_metadata(&path).map_err(|_| Refusal::RootNotDirectory)?;
if md.file_type().is_symlink() {
return Err(Refusal::RootHasSymlink);
}
if !md.is_dir() {
return Err(Refusal::RootNotDirectory);
}
let canon = fs::canonicalize(&path)?;
let real = input
.per_user_default()
.map_err(|_| Refusal::UserRootUnknown)?;
let mut protected = input.protected_paths(&real);
protected.push(real.root().to_path_buf());
if protected.is_empty() {
return Err(Refusal::UserRootUnknown);
}
for p in &protected {
if overlaps(&canon, &resolve(p)) {
return Err(Refusal::RootOverlapsUserData);
}
}
if on_cloud_folder(&canon) {
return Err(Refusal::RootOnRemovableOrCloud);
}
if !private_to_user(&canon)? {
return Err(Refusal::RootNotPrivate);
}
check_binding(app, &canon)?;
let suite = Suite::at(canon.join("shared")).map_err(|e| Refusal::Io(e.to_string()))?;
Ok(TestRoot {
path: canon,
suite,
app_id: app.id,
})
}
impl TestRoot {
pub fn reset(&self) -> Result<(), Refusal> {
let app = AppId {
id: self.app_id,
dir_name: self.app_id,
};
check_binding(&app, &self.path)?;
for entry in fs::read_dir(&self.path)? {
let entry = entry?;
if entry.file_name().to_string_lossy().starts_with('.') {
continue;
}
let ty = entry.file_type()?;
if ty.is_dir() && !ty.is_symlink() {
fs::remove_dir_all(entry.path())?;
} else {
fs::remove_file(entry.path())?;
}
}
Ok(())
}
}
}