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;
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 map_refusal(refusal: rightkit_fs::test_mode::Refusal) -> Refusal {
use rightkit_fs::test_mode::Refusal as FsRefusal;
match refusal {
FsRefusal::ActivationWithoutRoot => Refusal::ActivationWithoutRoot,
FsRefusal::RootWithoutActivation => Refusal::RootWithoutActivation,
FsRefusal::RootNotAbsolute => Refusal::RootNotAbsolute,
FsRefusal::RootNotDirectory => Refusal::RootNotDirectory,
FsRefusal::RootHasSymlink => Refusal::RootHasSymlink,
FsRefusal::RootOverlapsUserData => Refusal::RootOverlapsUserData,
FsRefusal::RootOnRemovableOrCloud => Refusal::RootOnRemovableOrCloud,
FsRefusal::RootNotPrivate => Refusal::RootNotPrivate,
FsRefusal::RootNotCreatedByRunner => Refusal::RootNotCreatedByRunner,
FsRefusal::BindingAppMismatch => Refusal::BindingAppMismatch,
FsRefusal::UserRootUnknown => Refusal::UserRootUnknown,
FsRefusal::Io(e) => Refusal::Io(e),
other => Refusal::Io(other.rule().to_string()),
}
}
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 real = input
.per_user_default()
.map_err(|_| Refusal::UserRootUnknown)?;
let mut protected = input.protected_paths(&real);
protected.push(real.root().to_path_buf());
let validated =
rightkit_fs::test_mode::validate_root(app.id, Path::new(raw.trim()), &protected)
.map_err(map_refusal)?;
let suite_path = validated.child(Path::new("shared")).map_err(map_refusal)?;
let suite = Suite::at(suite_path).map_err(|e| Refusal::Io(e.to_string()))?;
Ok(TestRoot {
path: validated.path().to_path_buf(),
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(())
}
}
}