use crate::{ActionPlanOptions, ConfigRuntimeOptions, EnvironmentInput, Error, Result};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RuntimeOptionOverrides {
pub strict: Option<bool>,
pub dangerously_allow_sources_outside_root: Option<bool>,
pub dangerously_allow_targets_outside_worktree: Option<bool>,
}
impl RuntimeOptionOverrides {
pub fn from_environment(environment: &EnvironmentInput) -> Result<Self> {
Ok(Self {
strict: env_bool("TREEBOOT_STRICT", environment.treeboot_strict.as_deref())?,
dangerously_allow_sources_outside_root: env_bool(
"TREEBOOT_DANGEROUSLY_ALLOW_SOURCES_OUTSIDE_ROOT",
environment
.treeboot_dangerously_allow_sources_outside_root
.as_deref(),
)?,
dangerously_allow_targets_outside_worktree: env_bool(
"TREEBOOT_DANGEROUSLY_ALLOW_TARGETS_OUTSIDE_WORKTREE",
environment
.treeboot_dangerously_allow_targets_outside_worktree
.as_deref(),
)?,
})
}
pub fn from_process_env() -> Result<Self> {
Self::from_environment(&EnvironmentInput::from_process_env())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimePolicy {
overrides: RuntimeOptionOverrides,
cli_strict: bool,
}
impl RuntimePolicy {
pub fn from_environment(environment: &EnvironmentInput, cli_strict: bool) -> Result<Self> {
Ok(Self::from_overrides(
RuntimeOptionOverrides::from_environment(environment)?,
cli_strict,
))
}
pub fn from_process_env(cli_strict: bool) -> Result<Self> {
Ok(Self::from_overrides(
RuntimeOptionOverrides::from_process_env()?,
cli_strict,
))
}
#[must_use]
pub const fn from_overrides(overrides: RuntimeOptionOverrides, cli_strict: bool) -> Self {
Self {
overrides,
cli_strict,
}
}
#[must_use]
pub const fn pre_config_strict(&self) -> bool {
self.cli_strict || matches!(self.overrides.strict, Some(true))
}
#[must_use]
pub fn resolve(&self, config: &ConfigRuntimeOptions) -> ResolvedRuntimePolicy {
let mut options = config.clone();
if let Some(strict) = self.overrides.strict {
options.strict = strict;
}
if let Some(allow) = self.overrides.dangerously_allow_sources_outside_root {
options.dangerously_allow_sources_outside_root = allow;
}
if let Some(allow) = self.overrides.dangerously_allow_targets_outside_worktree {
options.dangerously_allow_targets_outside_worktree = allow;
}
if self.cli_strict {
options.strict = true;
}
ResolvedRuntimePolicy { options }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRuntimePolicy {
options: ConfigRuntimeOptions,
}
impl ResolvedRuntimePolicy {
#[must_use]
pub const fn options(&self) -> &ConfigRuntimeOptions {
&self.options
}
#[must_use]
pub fn into_options(self) -> ConfigRuntimeOptions {
self.options
}
#[must_use]
pub const fn strict(&self) -> bool {
self.options.strict
}
#[must_use]
pub fn default_ignore(&self) -> &[String] {
&self.options.default_ignore
}
#[must_use]
pub fn action_plan_options(&self) -> ActionPlanOptions {
ActionPlanOptions::from(self.options.clone())
}
#[must_use]
pub fn into_action_plan_options(self) -> ActionPlanOptions {
ActionPlanOptions::from(self.options)
}
}
fn env_bool(name: &'static str, value: Option<&std::ffi::OsStr>) -> Result<Option<bool>> {
let Some(value) = value else {
return Ok(None);
};
let Some(value) = value.to_str() else {
return Err(Error::InvalidBooleanEnv {
name,
value: value.to_string_lossy().into_owned(),
});
};
parse_bool(value)
.ok_or_else(|| Error::InvalidBooleanEnv {
name,
value: value.to_owned(),
})
.map(Some)
}
fn parse_bool(value: &str) -> Option<bool> {
match value.to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use super::*;
#[test]
fn runtime_option_overrides_should_parse_explicit_environment_input() {
let overrides = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
treeboot_strict: Some(OsString::from("yes")),
treeboot_dangerously_allow_sources_outside_root: Some(OsString::from("true")),
treeboot_dangerously_allow_targets_outside_worktree: Some(OsString::from("0")),
..EnvironmentInput::empty()
})
.expect("environment should parse");
assert_eq!(
overrides,
RuntimeOptionOverrides {
strict: Some(true),
dangerously_allow_sources_outside_root: Some(true),
dangerously_allow_targets_outside_worktree: Some(false),
}
);
}
#[test]
fn runtime_option_overrides_should_reject_invalid_explicit_environment_input() {
let error = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
treeboot_strict: Some(OsString::from("sometimes")),
..EnvironmentInput::empty()
})
.expect_err("environment should fail");
assert!(matches!(
error,
Error::InvalidBooleanEnv {
name: "TREEBOOT_STRICT",
..
}
));
}
#[cfg(unix)]
#[test]
fn runtime_option_overrides_should_reject_non_utf8_explicit_environment_input() {
use std::os::unix::ffi::OsStringExt;
let error = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
treeboot_strict: Some(OsString::from_vec(vec![0xFF])),
..EnvironmentInput::empty()
})
.expect_err("environment should fail");
assert!(matches!(
error,
Error::InvalidBooleanEnv {
name: "TREEBOOT_STRICT",
..
}
));
}
#[test]
fn runtime_policy_should_resolve_config_defaults() {
let policy = RuntimePolicy::from_overrides(RuntimeOptionOverrides::default(), false);
let config = ConfigRuntimeOptions {
strict: true,
default_ignore: vec![".DS_Store".to_owned()],
dangerously_allow_sources_outside_root: true,
dangerously_allow_targets_outside_worktree: false,
};
let resolved = policy.resolve(&config);
assert_eq!(resolved.options(), &config);
}
#[test]
fn runtime_policy_should_apply_environment_overrides_after_config() {
let policy = RuntimePolicy::from_overrides(
RuntimeOptionOverrides {
strict: Some(false),
dangerously_allow_sources_outside_root: Some(false),
dangerously_allow_targets_outside_worktree: Some(true),
},
false,
);
let config = ConfigRuntimeOptions {
strict: true,
default_ignore: vec!["build".to_owned()],
dangerously_allow_sources_outside_root: true,
dangerously_allow_targets_outside_worktree: false,
};
let resolved = policy.resolve(&config);
assert_eq!(
resolved.into_options(),
ConfigRuntimeOptions {
strict: false,
default_ignore: vec!["build".to_owned()],
dangerously_allow_sources_outside_root: false,
dangerously_allow_targets_outside_worktree: true,
}
);
}
#[test]
fn runtime_policy_should_apply_cli_strict_after_environment() {
let policy = RuntimePolicy::from_overrides(
RuntimeOptionOverrides {
strict: Some(false),
..RuntimeOptionOverrides::default()
},
true,
);
let resolved = policy.resolve(&ConfigRuntimeOptions::default());
assert!(resolved.strict());
}
#[test]
fn resolved_runtime_policy_should_convert_into_action_plan_options() {
let policy = RuntimePolicy::from_overrides(
RuntimeOptionOverrides {
strict: Some(true),
dangerously_allow_sources_outside_root: Some(true),
dangerously_allow_targets_outside_worktree: Some(true),
},
false,
);
let options = policy
.resolve(&ConfigRuntimeOptions {
default_ignore: vec!["target".to_owned()],
..ConfigRuntimeOptions::default()
})
.into_action_plan_options();
assert_eq!(
options,
ActionPlanOptions {
strict: true,
dangerously_allow_sources_outside_root: true,
dangerously_allow_targets_outside_worktree: true,
}
);
}
#[test]
fn runtime_policy_should_enable_pre_config_strict_from_cli_or_environment() {
let env_policy = RuntimePolicy::from_overrides(
RuntimeOptionOverrides {
strict: Some(true),
..RuntimeOptionOverrides::default()
},
false,
);
let cli_policy = RuntimePolicy::from_overrides(RuntimeOptionOverrides::default(), true);
assert!(env_policy.pre_config_strict());
assert!(cli_policy.pre_config_strict());
}
#[test]
fn parse_bool_should_accept_supported_true_values() {
for value in ["1", "true", "TRUE", "yes", "on"] {
assert_eq!(parse_bool(value), Some(true), "value {value:?}");
}
}
#[test]
fn parse_bool_should_accept_supported_false_values() {
for value in ["0", "false", "FALSE", "no", "off"] {
assert_eq!(parse_bool(value), Some(false), "value {value:?}");
}
}
#[test]
fn parse_bool_should_reject_unsupported_values() {
assert_eq!(parse_bool("sometimes"), None);
}
}