1use crate::{ActionPlanOptions, ConfigRuntimeOptions, EnvironmentInput, Error, Result};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub struct RuntimeOptionOverrides {
6 pub strict: Option<bool>,
8 pub dangerously_allow_sources_outside_root: Option<bool>,
10 pub dangerously_allow_targets_outside_worktree: Option<bool>,
12}
13
14impl RuntimeOptionOverrides {
15 pub fn from_environment(environment: &EnvironmentInput) -> Result<Self> {
21 Ok(Self {
22 strict: env_bool("TREEBOOT_STRICT", environment.treeboot_strict.as_deref())?,
23 dangerously_allow_sources_outside_root: env_bool(
24 "TREEBOOT_DANGEROUSLY_ALLOW_SOURCES_OUTSIDE_ROOT",
25 environment
26 .treeboot_dangerously_allow_sources_outside_root
27 .as_deref(),
28 )?,
29 dangerously_allow_targets_outside_worktree: env_bool(
30 "TREEBOOT_DANGEROUSLY_ALLOW_TARGETS_OUTSIDE_WORKTREE",
31 environment
32 .treeboot_dangerously_allow_targets_outside_worktree
33 .as_deref(),
34 )?,
35 })
36 }
37
38 pub fn from_process_env() -> Result<Self> {
44 Self::from_environment(&EnvironmentInput::from_process_env())
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct RuntimePolicy {
51 overrides: RuntimeOptionOverrides,
52 cli_strict: bool,
53}
54
55impl RuntimePolicy {
56 pub fn from_environment(environment: &EnvironmentInput, cli_strict: bool) -> Result<Self> {
62 Ok(Self::from_overrides(
63 RuntimeOptionOverrides::from_environment(environment)?,
64 cli_strict,
65 ))
66 }
67
68 pub fn from_process_env(cli_strict: bool) -> Result<Self> {
74 Ok(Self::from_overrides(
75 RuntimeOptionOverrides::from_process_env()?,
76 cli_strict,
77 ))
78 }
79
80 #[must_use]
82 pub const fn from_overrides(overrides: RuntimeOptionOverrides, cli_strict: bool) -> Self {
83 Self {
84 overrides,
85 cli_strict,
86 }
87 }
88
89 #[must_use]
91 pub const fn pre_config_strict(&self) -> bool {
92 self.cli_strict || matches!(self.overrides.strict, Some(true))
93 }
94
95 #[must_use]
97 pub fn resolve(&self, config: &ConfigRuntimeOptions) -> ResolvedRuntimePolicy {
98 let mut options = config.clone();
99
100 if let Some(strict) = self.overrides.strict {
101 options.strict = strict;
102 }
103 if let Some(allow) = self.overrides.dangerously_allow_sources_outside_root {
104 options.dangerously_allow_sources_outside_root = allow;
105 }
106 if let Some(allow) = self.overrides.dangerously_allow_targets_outside_worktree {
107 options.dangerously_allow_targets_outside_worktree = allow;
108 }
109 if self.cli_strict {
110 options.strict = true;
111 }
112
113 ResolvedRuntimePolicy { options }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct ResolvedRuntimePolicy {
120 options: ConfigRuntimeOptions,
121}
122
123impl ResolvedRuntimePolicy {
124 #[must_use]
126 pub const fn options(&self) -> &ConfigRuntimeOptions {
127 &self.options
128 }
129
130 #[must_use]
132 pub fn into_options(self) -> ConfigRuntimeOptions {
133 self.options
134 }
135
136 #[must_use]
138 pub const fn strict(&self) -> bool {
139 self.options.strict
140 }
141
142 #[must_use]
144 pub fn default_ignore(&self) -> &[String] {
145 &self.options.default_ignore
146 }
147
148 #[must_use]
150 pub fn action_plan_options(&self) -> ActionPlanOptions {
151 ActionPlanOptions::from(self.options.clone())
152 }
153
154 #[must_use]
156 pub fn into_action_plan_options(self) -> ActionPlanOptions {
157 ActionPlanOptions::from(self.options)
158 }
159}
160
161fn env_bool(name: &'static str, value: Option<&std::ffi::OsStr>) -> Result<Option<bool>> {
162 let Some(value) = value else {
163 return Ok(None);
164 };
165
166 let Some(value) = value.to_str() else {
167 return Err(Error::InvalidBooleanEnv {
168 name,
169 value: value.to_string_lossy().into_owned(),
170 });
171 };
172
173 parse_bool(value)
174 .ok_or_else(|| Error::InvalidBooleanEnv {
175 name,
176 value: value.to_owned(),
177 })
178 .map(Some)
179}
180
181fn parse_bool(value: &str) -> Option<bool> {
182 match value.to_ascii_lowercase().as_str() {
183 "1" | "true" | "yes" | "on" => Some(true),
184 "0" | "false" | "no" | "off" => Some(false),
185 _ => None,
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use std::ffi::OsString;
192
193 use super::*;
194
195 #[test]
196 fn runtime_option_overrides_should_parse_explicit_environment_input() {
197 let overrides = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
198 treeboot_strict: Some(OsString::from("yes")),
199 treeboot_dangerously_allow_sources_outside_root: Some(OsString::from("true")),
200 treeboot_dangerously_allow_targets_outside_worktree: Some(OsString::from("0")),
201 ..EnvironmentInput::empty()
202 })
203 .expect("environment should parse");
204
205 assert_eq!(
206 overrides,
207 RuntimeOptionOverrides {
208 strict: Some(true),
209 dangerously_allow_sources_outside_root: Some(true),
210 dangerously_allow_targets_outside_worktree: Some(false),
211 }
212 );
213 }
214
215 #[test]
216 fn runtime_option_overrides_should_reject_invalid_explicit_environment_input() {
217 let error = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
218 treeboot_strict: Some(OsString::from("sometimes")),
219 ..EnvironmentInput::empty()
220 })
221 .expect_err("environment should fail");
222
223 assert!(matches!(
224 error,
225 Error::InvalidBooleanEnv {
226 name: "TREEBOOT_STRICT",
227 ..
228 }
229 ));
230 }
231
232 #[cfg(unix)]
233 #[test]
234 fn runtime_option_overrides_should_reject_non_utf8_explicit_environment_input() {
235 use std::os::unix::ffi::OsStringExt;
236
237 let error = RuntimeOptionOverrides::from_environment(&EnvironmentInput {
238 treeboot_strict: Some(OsString::from_vec(vec![0xFF])),
239 ..EnvironmentInput::empty()
240 })
241 .expect_err("environment should fail");
242
243 assert!(matches!(
244 error,
245 Error::InvalidBooleanEnv {
246 name: "TREEBOOT_STRICT",
247 ..
248 }
249 ));
250 }
251
252 #[test]
253 fn runtime_policy_should_resolve_config_defaults() {
254 let policy = RuntimePolicy::from_overrides(RuntimeOptionOverrides::default(), false);
255 let config = ConfigRuntimeOptions {
256 strict: true,
257 default_ignore: vec![".DS_Store".to_owned()],
258 dangerously_allow_sources_outside_root: true,
259 dangerously_allow_targets_outside_worktree: false,
260 };
261
262 let resolved = policy.resolve(&config);
263
264 assert_eq!(resolved.options(), &config);
265 }
266
267 #[test]
268 fn runtime_policy_should_apply_environment_overrides_after_config() {
269 let policy = RuntimePolicy::from_overrides(
270 RuntimeOptionOverrides {
271 strict: Some(false),
272 dangerously_allow_sources_outside_root: Some(false),
273 dangerously_allow_targets_outside_worktree: Some(true),
274 },
275 false,
276 );
277 let config = ConfigRuntimeOptions {
278 strict: true,
279 default_ignore: vec!["build".to_owned()],
280 dangerously_allow_sources_outside_root: true,
281 dangerously_allow_targets_outside_worktree: false,
282 };
283
284 let resolved = policy.resolve(&config);
285
286 assert_eq!(
287 resolved.into_options(),
288 ConfigRuntimeOptions {
289 strict: false,
290 default_ignore: vec!["build".to_owned()],
291 dangerously_allow_sources_outside_root: false,
292 dangerously_allow_targets_outside_worktree: true,
293 }
294 );
295 }
296
297 #[test]
298 fn runtime_policy_should_apply_cli_strict_after_environment() {
299 let policy = RuntimePolicy::from_overrides(
300 RuntimeOptionOverrides {
301 strict: Some(false),
302 ..RuntimeOptionOverrides::default()
303 },
304 true,
305 );
306
307 let resolved = policy.resolve(&ConfigRuntimeOptions::default());
308
309 assert!(resolved.strict());
310 }
311
312 #[test]
313 fn resolved_runtime_policy_should_convert_into_action_plan_options() {
314 let policy = RuntimePolicy::from_overrides(
315 RuntimeOptionOverrides {
316 strict: Some(true),
317 dangerously_allow_sources_outside_root: Some(true),
318 dangerously_allow_targets_outside_worktree: Some(true),
319 },
320 false,
321 );
322
323 let options = policy
324 .resolve(&ConfigRuntimeOptions {
325 default_ignore: vec!["target".to_owned()],
326 ..ConfigRuntimeOptions::default()
327 })
328 .into_action_plan_options();
329
330 assert_eq!(
331 options,
332 ActionPlanOptions {
333 strict: true,
334 dangerously_allow_sources_outside_root: true,
335 dangerously_allow_targets_outside_worktree: true,
336 }
337 );
338 }
339
340 #[test]
341 fn runtime_policy_should_enable_pre_config_strict_from_cli_or_environment() {
342 let env_policy = RuntimePolicy::from_overrides(
343 RuntimeOptionOverrides {
344 strict: Some(true),
345 ..RuntimeOptionOverrides::default()
346 },
347 false,
348 );
349 let cli_policy = RuntimePolicy::from_overrides(RuntimeOptionOverrides::default(), true);
350
351 assert!(env_policy.pre_config_strict());
352 assert!(cli_policy.pre_config_strict());
353 }
354
355 #[test]
356 fn parse_bool_should_accept_supported_true_values() {
357 for value in ["1", "true", "TRUE", "yes", "on"] {
358 assert_eq!(parse_bool(value), Some(true), "value {value:?}");
359 }
360 }
361
362 #[test]
363 fn parse_bool_should_accept_supported_false_values() {
364 for value in ["0", "false", "FALSE", "no", "off"] {
365 assert_eq!(parse_bool(value), Some(false), "value {value:?}");
366 }
367 }
368
369 #[test]
370 fn parse_bool_should_reject_unsupported_values() {
371 assert_eq!(parse_bool("sometimes"), None);
372 }
373}