rootasrole_core/database/
options.rs

1use std::collections::HashMap;
2#[cfg(feature = "finder")]
3use std::path::PathBuf;
4use std::{borrow::Borrow, cell::RefCell, rc::Rc};
5
6use bon::{bon, builder, Builder};
7use chrono::Duration;
8
9#[cfg(feature = "finder")]
10use libc::PATH_MAX;
11use linked_hash_set::LinkedHashSet;
12
13#[cfg(feature = "pcre2")]
14use pcre2::bytes::Regex;
15use serde::{Deserialize, Deserializer, Serialize};
16use serde_json::{Map, Value};
17use strum::{Display, EnumIs, EnumIter, FromRepr};
18
19use log::debug;
20#[cfg(feature = "finder")]
21use log::warn;
22
23use crate::rc_refcell;
24
25#[cfg(feature = "finder")]
26use super::finder::Cred;
27use super::{deserialize_duration, is_default, serialize_duration, FilterMatcher};
28
29use super::{
30    lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey,
31    structs::{SConfig, SRole, STask},
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum Level {
36    #[default]
37    None,
38    Default,
39    Global,
40    Role,
41    Task,
42}
43
44#[derive(Debug, Clone, Copy, FromRepr, EnumIter, Display)]
45pub enum OptType {
46    Path,
47    Env,
48    Root,
49    Bounding,
50    Wildcard,
51    Timeout,
52}
53
54#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
55#[serde(rename_all = "lowercase")]
56#[derive(Default)]
57pub enum PathBehavior {
58    Delete,
59    KeepSafe,
60    KeepUnsafe,
61    #[default]
62    Inherit,
63}
64
65#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display)]
66#[serde(rename_all = "lowercase")]
67#[derive(Default)]
68pub enum TimestampType {
69    #[default]
70    PPID,
71    TTY,
72    UID,
73}
74
75#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)]
76pub struct STimeout {
77    #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
78    pub type_field: Option<TimestampType>,
79    #[serde(
80        serialize_with = "serialize_duration",
81        deserialize_with = "deserialize_duration",
82        skip_serializing_if = "Option::is_none"
83    )]
84    pub duration: Option<Duration>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub max_usage: Option<u64>,
87    #[serde(default)]
88    #[serde(flatten, skip_serializing_if = "Map::is_empty")]
89    #[builder(default)]
90    pub _extra_fields: Map<String, Value>,
91}
92
93#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder)]
94pub struct SPathOptions {
95    #[serde(rename = "default", default, skip_serializing_if = "is_default")]
96    #[builder(start_fn)]
97    pub default_behavior: PathBehavior,
98    #[serde(
99        default,
100        skip_serializing_if = "Option::is_none",
101        deserialize_with = "lhs_deserialize",
102        serialize_with = "lhs_serialize"
103    )]
104    #[builder(with = |v : impl IntoIterator<Item = impl ToString>| { v.into_iter().map(|s| s.to_string()).collect() })]
105    pub add: Option<LinkedHashSet<String>>,
106    #[serde(
107        default,
108        skip_serializing_if = "Option::is_none",
109        deserialize_with = "lhs_deserialize",
110        serialize_with = "lhs_serialize",
111        alias = "del"
112    )]
113    #[builder(with = |v : impl IntoIterator<Item = impl ToString>| { v.into_iter().map(|s| s.to_string()).collect() })]
114    pub sub: Option<LinkedHashSet<String>>,
115    #[serde(default)]
116    #[serde(flatten)]
117    #[builder(default)]
118    pub _extra_fields: Map<String, Value>,
119}
120
121#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
122#[serde(rename_all = "lowercase")]
123#[derive(Default)]
124pub enum EnvBehavior {
125    Delete,
126    Keep,
127    #[default]
128    Inherit,
129}
130
131#[derive(Serialize, Hash, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)]
132enum EnvKeyType {
133    Wildcarded,
134    Normal,
135}
136
137#[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone, Builder)]
138#[serde(transparent)]
139pub struct EnvKey {
140    #[serde(skip)]
141    env_type: EnvKeyType,
142    value: String,
143}
144
145impl std::fmt::Display for EnvKey {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "{}", self.value)
148    }
149}
150
151#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)]
152pub struct SEnvOptions {
153    #[serde(rename = "default", default, skip_serializing_if = "is_default")]
154    #[builder(start_fn)]
155    pub default_behavior: EnvBehavior,
156    #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")]
157    pub override_behavior: Option<bool>,
158    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
159    #[builder(default, with = |iter: impl IntoIterator<Item = (impl ToString, impl ToString)>| {
160        let mut map = HashMap::with_hasher(Default::default());
161        map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string())));
162        map
163    })]
164    pub set: HashMap<String, String>,
165    #[serde(
166        default,
167        skip_serializing_if = "Option::is_none",
168        deserialize_with = "lhs_deserialize_envkey",
169        serialize_with = "lhs_serialize_envkey"
170    )]
171    #[builder(with = |v : impl IntoIterator<Item = impl ToString>| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})]
172    pub keep: Option<LinkedHashSet<EnvKey>>,
173    #[serde(
174        default,
175        skip_serializing_if = "Option::is_none",
176        deserialize_with = "lhs_deserialize_envkey",
177        serialize_with = "lhs_serialize_envkey"
178    )]
179    #[builder(with = |v : impl IntoIterator<Item = impl ToString>| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})]
180    pub check: Option<LinkedHashSet<EnvKey>>,
181    #[serde(
182        default,
183        skip_serializing_if = "Option::is_none",
184        deserialize_with = "lhs_deserialize_envkey",
185        serialize_with = "lhs_serialize_envkey"
186    )]
187    #[builder(with = |v : impl IntoIterator<Item = impl ToString>| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})]
188    pub delete: Option<LinkedHashSet<EnvKey>>,
189    #[serde(default, flatten)]
190    #[builder(default)]
191    pub _extra_fields: Map<String, Value>,
192}
193
194#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
195#[serde(rename_all = "lowercase")]
196#[derive(Default)]
197pub enum SBounding {
198    Strict,
199    Ignore,
200    #[default]
201    Inherit,
202}
203
204#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
205#[serde(rename_all = "kebab-case")]
206#[derive(Default)]
207pub enum SPrivileged {
208    Privileged,
209    #[default]
210    User,
211    Inherit,
212}
213
214#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)]
215#[serde(rename_all = "kebab-case")]
216#[derive(Default)]
217pub enum SAuthentication {
218    Skip,
219    #[default]
220    Perform,
221    Inherit,
222}
223
224#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
225#[serde(rename_all = "kebab-case")]
226pub struct Opt {
227    #[serde(skip)]
228    pub level: Level,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub path: Option<SPathOptions>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub env: Option<SEnvOptions>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub root: Option<SPrivileged>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub bounding: Option<SBounding>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub authentication: Option<SAuthentication>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub wildcard_denied: Option<String>,
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub timeout: Option<STimeout>,
243    #[serde(default, flatten)]
244    pub _extra_fields: Map<String, Value>,
245}
246
247#[bon]
248impl Opt {
249    #[builder]
250    pub fn new(
251        #[builder(start_fn)] level: Level,
252        path: Option<SPathOptions>,
253        env: Option<SEnvOptions>,
254        root: Option<SPrivileged>,
255        bounding: Option<SBounding>,
256        authentication: Option<SAuthentication>,
257        #[builder(into)] wildcard_denied: Option<String>,
258        timeout: Option<STimeout>,
259        #[builder(default)] _extra_fields: Map<String, Value>,
260    ) -> Rc<RefCell<Self>> {
261        rc_refcell!(Opt {
262            level,
263            path,
264            env,
265            root,
266            bounding,
267            authentication,
268            wildcard_denied,
269            timeout,
270            _extra_fields,
271        })
272    }
273
274    pub fn raw_new(level: Level) -> Self {
275        Opt {
276            level,
277            ..Default::default()
278        }
279    }
280
281    pub fn level_default() -> Rc<RefCell<Self>> {
282        Self::builder(Level::Default)
283            .root(SPrivileged::User)
284            .bounding(SBounding::Strict)
285            .path(
286                SPathOptions::builder(PathBehavior::Delete)
287                    .add([
288                        "/usr/local/sbin",
289                        "/usr/local/bin",
290                        "/usr/sbin",
291                        "/usr/bin",
292                        "/sbin",
293                        "/snap/bin",
294                    ])
295                    .build(),
296            )
297            .authentication(SAuthentication::Perform)
298            .env(
299                SEnvOptions::builder(EnvBehavior::Delete)
300                    .keep([
301                        "HOME",
302                        "USER",
303                        "LOGNAME",
304                        "COLORS",
305                        "DISPLAY",
306                        "HOSTNAME",
307                        "KRB5CCNAME",
308                        "LS_COLORS",
309                        "PS1",
310                        "PS2",
311                        "XAUTHORY",
312                        "XAUTHORIZATION",
313                        "XDG_CURRENT_DESKTOP",
314                    ])
315                    .unwrap()
316                    .check([
317                        "COLORTERM",
318                        "LANG",
319                        "LANGUAGE",
320                        "LC_*",
321                        "LINGUAS",
322                        "TERM",
323                        "TZ",
324                    ])
325                    .unwrap()
326                    .delete([
327                        "PS4",
328                        "SHELLOPTS",
329                        "PERLLIB",
330                        "PERL5LIB",
331                        "PERL5OPT",
332                        "PYTHONINSPECT",
333                    ])
334                    .unwrap()
335                    .build(),
336            )
337            .timeout(
338                STimeout::builder()
339                    .type_field(TimestampType::PPID)
340                    .duration(Duration::minutes(5))
341                    .build(),
342            )
343            .wildcard_denied(";&|")
344            .build()
345    }
346}
347
348impl Default for Opt {
349    fn default() -> Self {
350        Opt {
351            path: Some(SPathOptions::default()),
352            env: Some(SEnvOptions::default()),
353            root: Some(SPrivileged::default()),
354            bounding: Some(SBounding::default()),
355            authentication: None,
356            wildcard_denied: None,
357            timeout: None,
358            _extra_fields: Map::default(),
359            level: Level::Default,
360        }
361    }
362}
363
364impl Default for OptStack {
365    fn default() -> Self {
366        OptStack {
367            stack: [None, Some(Opt::level_default()), None, None, None],
368            roles: None,
369            role: None,
370            task: None,
371        }
372    }
373}
374
375impl Default for SPathOptions {
376    fn default() -> Self {
377        SPathOptions {
378            default_behavior: PathBehavior::Inherit,
379            add: None,
380            sub: None,
381            _extra_fields: Map::default(),
382        }
383    }
384}
385
386fn is_valid_env_name(s: &str) -> bool {
387    let mut chars = s.chars();
388
389    // Check if the first character is a letter or underscore
390    if let Some(first_char) = chars.next() {
391        if !(first_char.is_ascii_alphabetic() || first_char == '_') {
392            return false;
393        }
394    } else {
395        return false; // Empty string
396    }
397
398    // Check if the remaining characters are alphanumeric or underscores
399    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
400}
401
402#[cfg(feature = "pcre2")]
403fn is_regex(s: &str) -> bool {
404    Regex::new(s).is_ok()
405}
406
407#[cfg(not(feature = "pcre2"))]
408fn is_regex(_s: &str) -> bool {
409    true // Always return true if regex feature is disabled
410}
411
412impl EnvKey {
413    pub fn new(s: String) -> Result<Self, String> {
414        //debug!("Creating env key: {}", s);
415        if is_valid_env_name(&s) {
416            Ok(EnvKey {
417                env_type: EnvKeyType::Normal,
418                value: s,
419            })
420        } else if is_regex(&s) {
421            Ok(EnvKey {
422                env_type: EnvKeyType::Wildcarded,
423                value: s,
424            })
425        } else {
426            Err(format!(
427                "env key {}, must be a valid env, or a valid regex",
428                s
429            ))
430        }
431    }
432}
433
434impl PartialEq<str> for EnvKey {
435    fn eq(&self, other: &str) -> bool {
436        self.value == *other
437    }
438}
439
440impl From<EnvKey> for String {
441    fn from(val: EnvKey) -> Self {
442        val.value
443    }
444}
445
446impl From<String> for EnvKey {
447    fn from(s: String) -> Self {
448        EnvKey::new(s).expect("Invalid env key")
449    }
450}
451
452impl From<&str> for EnvKey {
453    fn from(s: &str) -> Self {
454        EnvKey::new(s.into()).expect("Invalid env key")
455    }
456}
457
458impl<'de> Deserialize<'de> for EnvKey {
459    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
460    where
461        D: Deserializer<'de>,
462    {
463        let s = String::deserialize(deserializer)?;
464        EnvKey::new(s).map_err(serde::de::Error::custom)
465    }
466}
467
468impl SEnvOptions {
469    pub fn new(behavior: EnvBehavior) -> Self {
470        SEnvOptions {
471            default_behavior: behavior,
472            ..Default::default()
473        }
474    }
475}
476
477trait EnvSet {
478    fn env_matches(&self, wildcarded: &EnvKey) -> bool;
479}
480
481impl<T> EnvSet for HashMap<String, T> {
482    fn env_matches(&self, wildcarded: &EnvKey) -> bool {
483        match wildcarded.env_type {
484            EnvKeyType::Normal => self.contains_key(&wildcarded.value),
485            EnvKeyType::Wildcarded => self.keys().any(|s| check_wildcarded(wildcarded, s)),
486        }
487    }
488}
489
490impl EnvSet for LinkedHashSet<EnvKey> {
491    fn env_matches(&self, needle: &EnvKey) -> bool {
492        self.iter().any(|s| match s.env_type {
493            EnvKeyType::Normal => s == needle,
494            EnvKeyType::Wildcarded => check_wildcarded(s, &needle.value),
495        })
496    }
497}
498
499impl EnvSet for Option<LinkedHashSet<EnvKey>> {
500    fn env_matches(&self, needle: &EnvKey) -> bool {
501        self.as_ref().map_or(false, |set| set.env_matches(needle))
502    }
503}
504
505#[cfg(feature = "pcre2")]
506fn check_wildcarded(wildcarded: &EnvKey, s: &String) -> bool {
507    Regex::new(&format!("^{}$", wildcarded.value)) // convert to regex
508        .unwrap()
509        .is_match(s.as_bytes())
510        .is_ok_and(|m| m)
511}
512
513#[cfg(not(feature = "pcre2"))]
514fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool {
515    true
516}
517
518#[cfg(feature = "finder")]
519fn tz_is_safe(tzval: &str) -> bool {
520    // tzcode treats a value beginning with a ':' as a path.
521    let tzval = if let Some(val) = tzval.strip_prefix(':') {
522        val
523    } else {
524        tzval
525    };
526
527    // Reject fully-qualified TZ that doesn't begin with the zoneinfo dir.
528    if tzval.starts_with('/') {
529        return false;
530    }
531
532    // Make sure TZ only contains printable non-space characters
533    // and does not contain a '..' path element.
534    let mut lastch = '/';
535    for cp in tzval.chars() {
536        if cp.is_ascii_whitespace() || !cp.is_ascii_graphic() {
537            return false;
538        }
539        if lastch == '/'
540            && cp == '.'
541            && tzval
542                .chars()
543                .nth(tzval.chars().position(|c| c == '.').unwrap() + 1)
544                == Some('.')
545            && (tzval
546                .chars()
547                .nth(tzval.chars().position(|c| c == '.').unwrap() + 2)
548                == Some('/')
549                || tzval
550                    .chars()
551                    .nth(tzval.chars().position(|c| c == '.').unwrap() + 2)
552                    .is_none())
553        {
554            return false;
555        }
556        lastch = cp;
557    }
558
559    // Reject extra long TZ values (even if not a path).
560    if tzval.len() >= PATH_MAX.try_into().unwrap() {
561        return false;
562    }
563
564    true
565}
566
567#[cfg(feature = "finder")]
568fn check_env(key: &str, value: &str) -> bool {
569    debug!("Checking env: {}={}", key, value);
570    match key {
571        "TZ" => tz_is_safe(value),
572        _ => !value.chars().any(|c| c == '/' || c == '%'),
573    }
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct OptStack {
578    pub(crate) stack: [Option<Rc<RefCell<Opt>>>; 5],
579    roles: Option<Rc<RefCell<SConfig>>>,
580    role: Option<Rc<RefCell<SRole>>>,
581    task: Option<Rc<RefCell<STask>>>,
582}
583
584#[cfg(not(tarpaulin_include))]
585impl<S: opt_stack_builder::State> OptStackBuilder<S> {
586    fn opt(mut self, opt: Option<Rc<RefCell<Opt>>>) -> Self {
587        if let Some(opt) = opt {
588            self.stack[opt.as_ref().borrow().level as usize] = Some(opt.clone());
589        }
590        self
591    }
592    fn with_task(
593        self,
594        task: Rc<RefCell<STask>>,
595    ) -> OptStackBuilder<
596        opt_stack_builder::SetTask<opt_stack_builder::SetRole<opt_stack_builder::SetRoles<S>>>,
597    >
598    where
599        <S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
600        <S as opt_stack_builder::State>::Role: opt_stack_builder::IsUnset,
601        <S as opt_stack_builder::State>::Task: opt_stack_builder::IsUnset,
602    {
603        self.with_role(
604            task.as_ref()
605                .borrow()
606                ._role
607                .as_ref()
608                .unwrap()
609                .upgrade()
610                .unwrap(),
611        )
612        .task(task.to_owned())
613        .opt(task.as_ref().borrow().options.to_owned())
614    }
615    fn with_role(
616        self,
617        role: Rc<RefCell<SRole>>,
618    ) -> OptStackBuilder<opt_stack_builder::SetRole<opt_stack_builder::SetRoles<S>>>
619    where
620        <S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
621        <S as opt_stack_builder::State>::Role: opt_stack_builder::IsUnset,
622    {
623        self.with_roles(
624            role.as_ref()
625                .borrow()
626                ._config
627                .as_ref()
628                .unwrap()
629                .upgrade()
630                .unwrap(),
631        )
632        .role(role.to_owned())
633        .opt(role.as_ref().borrow().options.to_owned())
634    }
635
636    fn with_roles(
637        self,
638        roles: Rc<RefCell<SConfig>>,
639    ) -> OptStackBuilder<opt_stack_builder::SetRoles<S>>
640    where
641        <S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
642    {
643        self.with_default()
644            .roles(roles.to_owned())
645            .opt(roles.as_ref().borrow().options.to_owned())
646    }
647
648    fn with_default(self) -> Self {
649        self.opt(Some(
650            Opt::builder(Level::Default)
651                .root(SPrivileged::User)
652                .bounding(SBounding::Strict)
653                .path(
654                    SPathOptions::builder(PathBehavior::Delete)
655                        .add([
656                            "/usr/local/sbin",
657                            "/usr/local/bin",
658                            "/usr/sbin",
659                            "/usr/bin",
660                            "/sbin",
661                            "/bin",
662                            "/snap/bin",
663                        ])
664                        .build(),
665                )
666                .authentication(SAuthentication::Perform)
667                .env(
668                    SEnvOptions::builder(EnvBehavior::Delete)
669                        .keep([
670                            "HOME",
671                            "USER",
672                            "LOGNAME",
673                            "COLORS",
674                            "DISPLAY",
675                            "HOSTNAME",
676                            "KRB5CCNAME",
677                            "LS_COLORS",
678                            "PS1",
679                            "PS2",
680                            "XAUTHORY",
681                            "XAUTHORIZATION",
682                            "XDG_CURRENT_DESKTOP",
683                        ])
684                        .unwrap()
685                        .check([
686                            "COLORTERM",
687                            "LANG",
688                            "LANGUAGE",
689                            "LC_*",
690                            "LINGUAS",
691                            "TERM",
692                            "TZ",
693                        ])
694                        .unwrap()
695                        .delete([
696                            "PS4",
697                            "SHELLOPTS",
698                            "PERLLIB",
699                            "PERL5LIB",
700                            "PERL5OPT",
701                            "PYTHONINSPECT",
702                        ])
703                        .unwrap()
704                        .build(),
705                )
706                .timeout(
707                    STimeout::builder()
708                        .type_field(TimestampType::TTY)
709                        .duration(Duration::minutes(5))
710                        .build(),
711                )
712                .wildcard_denied(";&|")
713                .build(),
714        ))
715    }
716}
717
718#[bon]
719impl OptStack {
720    #[builder]
721    pub fn new(
722        #[builder(field)] stack: [Option<Rc<RefCell<Opt>>>; 5],
723        roles: Option<Rc<RefCell<SConfig>>>,
724        role: Option<Rc<RefCell<SRole>>>,
725        task: Option<Rc<RefCell<STask>>>,
726    ) -> Self {
727        OptStack {
728            stack,
729            roles,
730            role,
731            task,
732        }
733    }
734    pub fn from_task(task: Rc<RefCell<STask>>) -> Self {
735        OptStack::builder().with_task(task).build()
736    }
737    pub fn from_role(role: Rc<RefCell<SRole>>) -> Self {
738        OptStack::builder().with_role(role).build()
739    }
740    pub fn from_roles(roles: Rc<RefCell<SConfig>>) -> Self {
741        OptStack::builder().with_roles(roles).build()
742    }
743
744    fn find_in_options<F: Fn(&Opt) -> Option<(Level, V)>, V>(&self, f: F) -> Option<(Level, V)> {
745        for opt in self.stack.iter().rev() {
746            if let Some(opt) = opt.to_owned() {
747                let res = f(&opt.as_ref().borrow());
748                if res.is_some() {
749                    debug!("res: {:?}", res.as_ref().unwrap().0);
750                    return res;
751                }
752            }
753        }
754        None
755    }
756
757    fn iter_in_options<F: FnMut(&Opt)>(&self, mut f: F) {
758        for opt in self.stack.iter() {
759            if let Some(opt) = opt.to_owned() {
760                f(&opt.as_ref().borrow());
761            }
762        }
763    }
764
765    #[cfg(feature = "finder")]
766    fn calculate_path(&self) -> String {
767        let path = self.get_final_path();
768        let default = LinkedHashSet::new();
769        println!("path: {:?}", path);
770        if let Some(add) = path.add {
771            let final_add = add.difference(path.sub.as_ref().unwrap_or(&default)).fold(
772                "".to_string(),
773                |mut acc, s| {
774                    if !acc.is_empty() {
775                        acc.insert(0, ':');
776                    }
777                    acc.insert_str(0, s);
778                    acc
779                },
780            );
781            match path.default_behavior {
782                PathBehavior::Inherit | PathBehavior::Delete => final_add,
783                is_safe => std::env::vars()
784                    .find_map(|(key, value)| if key == "PATH" { Some(value) } else { None })
785                    .unwrap_or(String::new())
786                    .split(':')
787                    .filter(|s| {
788                        !path.sub.as_ref().unwrap_or(&default).contains(*s)
789                            && (!is_safe.is_keep_safe() || PathBuf::from(s).exists())
790                    })
791                    .fold(final_add, |mut acc, s| {
792                        if !acc.is_empty() {
793                            acc.push(':');
794                        }
795                        acc.push_str(s);
796                        acc
797                    }),
798            }
799        } else {
800            "".to_string()
801        }
802    }
803
804    fn get_final_path(&self) -> SPathOptions {
805        let mut final_behavior = PathBehavior::Delete;
806        let default = LinkedHashSet::new();
807        let final_add = rc_refcell!(LinkedHashSet::new());
808        // Cannot use HashSet as we need to keep order
809        let final_sub = rc_refcell!(LinkedHashSet::new());
810        self.iter_in_options(|opt| {
811            let final_add_clone = Rc::clone(&final_add);
812            let final_sub_clone = Rc::clone(&final_sub);
813            if let Some(p) = opt.path.borrow().as_ref() {
814                match p.default_behavior {
815                    PathBehavior::KeepSafe | PathBehavior::KeepUnsafe | PathBehavior::Delete => {
816                        if let Some(add) = p.add.as_ref() {
817                            final_add_clone.as_ref().replace(add.clone());
818                        }
819                        if let Some(sub) = p.sub.as_ref() {
820                            final_sub_clone.as_ref().replace(sub.clone());
821                        }
822                    }
823                    PathBehavior::Inherit => {
824                        if final_behavior.is_delete() {
825                            let union: LinkedHashSet<String> = final_add_clone
826                                .as_ref()
827                                .borrow()
828                                .union(p.add.as_ref().unwrap_or(&default))
829                                .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e))
830                                .cloned()
831                                .collect();
832                            final_add_clone.as_ref().borrow_mut().extend(union);
833                            debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow());
834                        } else {
835                            let union: LinkedHashSet<String> = final_sub_clone
836                                .as_ref()
837                                .borrow()
838                                .union(p.sub.as_ref().unwrap_or(&default))
839                                .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e))
840                                .cloned()
841                                .collect();
842                            final_sub_clone.as_ref().borrow_mut().extend(union);
843                        }
844                    }
845                }
846                if !p.default_behavior.is_inherit() {
847                    final_behavior = p.default_behavior;
848                }
849            }
850        });
851        SPathOptions::builder(final_behavior)
852            .add(
853                final_add
854                    .clone()
855                    .as_ref()
856                    .borrow()
857                    .iter()
858                    .collect::<Vec<_>>()
859                    .as_slice(),
860            )
861            .sub(
862                final_sub
863                    .clone()
864                    .as_ref()
865                    .borrow()
866                    .iter()
867                    .collect::<Vec<_>>()
868                    .as_slice(),
869            )
870            .build()
871    }
872
873    #[allow(dead_code)]
874    #[cfg(not(tarpaulin_include))]
875    fn union_all_path(&self) -> SPathOptions {
876        let mut final_behavior = PathBehavior::Delete;
877        let default = LinkedHashSet::new();
878        let final_add = rc_refcell!(LinkedHashSet::new());
879        // Cannot use HashSet as we need to keep order
880        let final_sub = rc_refcell!(LinkedHashSet::new());
881        self.iter_in_options(|opt| {
882            let final_add_clone = Rc::clone(&final_add);
883            let final_sub_clone = Rc::clone(&final_sub);
884            if let Some(p) = opt.path.borrow().as_ref() {
885                match p.default_behavior {
886                    PathBehavior::Delete => {
887                        let union = final_add_clone
888                            .as_ref()
889                            .borrow()
890                            .union(p.add.as_ref().unwrap_or(&default))
891                            .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e))
892                            .cloned()
893                            .collect();
894                        // policy is to delete, so we add whitelist and remove blacklist
895                        final_add_clone.as_ref().replace(union);
896                        debug!("delete final_add: {:?}", final_add_clone.as_ref().borrow());
897                    }
898                    PathBehavior::KeepSafe | PathBehavior::KeepUnsafe => {
899                        let union = final_sub_clone
900                            .as_ref()
901                            .borrow()
902                            .union(p.sub.as_ref().unwrap_or(&default))
903                            .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e))
904                            .cloned()
905                            .collect();
906                        //policy is to keep, so we remove blacklist and add whitelist
907                        final_sub_clone.as_ref().replace(union);
908                    }
909                    PathBehavior::Inherit => {
910                        if final_behavior.is_delete() {
911                            let union: LinkedHashSet<String> = final_add_clone
912                                .as_ref()
913                                .borrow()
914                                .union(p.add.as_ref().unwrap_or(&default))
915                                .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e))
916                                .cloned()
917                                .collect();
918                            final_add_clone.as_ref().borrow_mut().extend(union);
919                            debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow());
920                        } else {
921                            let union: LinkedHashSet<String> = final_sub_clone
922                                .as_ref()
923                                .borrow()
924                                .union(p.sub.as_ref().unwrap_or(&default))
925                                .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e))
926                                .cloned()
927                                .collect();
928                            final_sub_clone.as_ref().borrow_mut().extend(union);
929                        }
930                    }
931                }
932                if !p.default_behavior.is_inherit() {
933                    final_behavior = p.default_behavior;
934                }
935            }
936        });
937        SPathOptions::builder(final_behavior)
938            .add(
939                final_add
940                    .clone()
941                    .as_ref()
942                    .borrow()
943                    .iter()
944                    .collect::<Vec<_>>()
945                    .as_slice(),
946            )
947            .sub(
948                final_sub
949                    .clone()
950                    .as_ref()
951                    .borrow()
952                    .iter()
953                    .collect::<Vec<_>>()
954                    .as_slice(),
955            )
956            .build()
957    }
958
959    #[cfg(feature = "finder")]
960    pub fn calculate_filtered_env<I>(
961        &self,
962        opt_filter: Option<FilterMatcher>,
963        target: Cred,
964        final_env: I,
965    ) -> Result<HashMap<String, String>, String>
966    where
967        I: Iterator<Item = (String, String)>,
968    {
969        let env = self.get_final_env(opt_filter);
970        println!("env: {:?}", env);
971        if env.default_behavior.is_keep() {
972            warn!("Keeping environment variables is dangerous operation, it can lead to security vulnerabilities. 
973            Please consider using delete instead. 
974            See https://www.sudo.ws/security/advisories/bash_env/, 
975            https://www.sudo.ws/security/advisories/perl_env/ or 
976            https://nvd.nist.gov/vuln/detail/CVE-2006-0151");
977        }
978        let mut final_env: HashMap<String, String> = match env.default_behavior {
979            EnvBehavior::Inherit => Err("Internal Error with environment behavior".to_string()),
980            EnvBehavior::Delete => Ok(final_env
981                .filter_map(|(key, value)| {
982                    let key = EnvKey::new(key).expect("Unexpected environment variable");
983                    if env.keep.env_matches(&key)
984                        || (env.check.env_matches(&key) && check_env(&key.value, &value))
985                    {
986                        debug!("Keeping env: {}={}", key.value, value);
987                        Some((key.value, value))
988                    } else {
989                        debug!("Dropping env: {}", key.value);
990                        None
991                    }
992                })
993                .collect()),
994            EnvBehavior::Keep => Ok(final_env
995                .filter_map(|(key, value)| {
996                    let key = EnvKey::new(key).expect("Unexpected environment variable");
997                    if !env.delete.env_matches(&key)
998                        || (env.check.env_matches(&key) && check_env(&key.value, &value))
999                    {
1000                        debug!("Keeping env: {}={}", key.value, value);
1001                        Some((key.value, value))
1002                    } else {
1003                        debug!("Dropping env: {}", key.value);
1004                        None
1005                    }
1006                })
1007                .collect()),
1008        }?;
1009        final_env.insert("PATH".into(), self.calculate_path());
1010        final_env.insert("LOGNAME".into(), target.user.name.clone());
1011        final_env.insert("USER".into(), target.user.name);
1012        final_env.insert("HOME".into(), target.user.dir.to_string_lossy().to_string());
1013        final_env
1014            .entry("TERM".into())
1015            .or_insert_with(|| "unknown".into());
1016        final_env.insert(
1017            "SHELL".into(),
1018            target.user.shell.to_string_lossy().to_string(),
1019        );
1020        final_env.extend(env.set);
1021        Ok(final_env)
1022    }
1023
1024    fn get_final_env(&self, cmd_filter: Option<FilterMatcher>) -> SEnvOptions {
1025        let mut final_behavior = EnvBehavior::default();
1026        let mut final_set = HashMap::new();
1027        let mut final_keep = LinkedHashSet::new();
1028        let mut final_check = LinkedHashSet::new();
1029        let mut final_delete = LinkedHashSet::new();
1030        let overriden_behavior = cmd_filter.as_ref().and_then(|f| f.env_behavior);
1031        self.iter_in_options(|opt| {
1032            if let Some(p) = opt.env.borrow().as_ref() {
1033                final_behavior = match p.default_behavior {
1034                    EnvBehavior::Delete | EnvBehavior::Keep => {
1035                        // policy is to delete, so we add whitelist and remove blacklist
1036                        final_keep = p
1037                            .keep
1038                            .as_ref()
1039                            .unwrap_or(&LinkedHashSet::new())
1040                            .iter()
1041                            .filter(|e| {
1042                                !p.set.env_matches(e)
1043                                    || !p.check.env_matches(e)
1044                                    || !p.delete.env_matches(e)
1045                            })
1046                            .cloned()
1047                            .collect();
1048                        final_check = p
1049                            .check
1050                            .as_ref()
1051                            .unwrap_or(&LinkedHashSet::new())
1052                            .iter()
1053                            .filter(|e| !p.set.env_matches(e) || !p.delete.env_matches(e))
1054                            .cloned()
1055                            .collect();
1056                        final_delete = p
1057                            .delete
1058                            .as_ref()
1059                            .unwrap_or(&LinkedHashSet::new())
1060                            .iter()
1061                            .filter(|e| !p.set.env_matches(e) || !p.check.env_matches(e))
1062                            .cloned()
1063                            .collect();
1064                        final_set = p.set.clone();
1065                        debug!("check: {:?}", final_check);
1066                        p.default_behavior
1067                    }
1068                    EnvBehavior::Inherit => {
1069                        final_keep = final_keep
1070                            .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new()))
1071                            .cloned()
1072                            .collect();
1073                        final_check = final_check
1074                            .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
1075                            .cloned()
1076                            .collect();
1077                        final_delete = final_delete
1078                            .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new()))
1079                            .cloned()
1080                            .collect();
1081                        final_set.extend(p.set.clone());
1082                        debug!("check: {:?}", final_check);
1083                        final_behavior
1084                    }
1085                };
1086            }
1087        });
1088        SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior))
1089            .set(final_set)
1090            .keep(final_keep)
1091            .unwrap()
1092            .check(final_check)
1093            .unwrap()
1094            .delete(final_delete)
1095            .unwrap()
1096            .build()
1097    }
1098
1099    #[allow(dead_code)]
1100    #[cfg(not(tarpaulin_include))]
1101    fn union_all_env(
1102        &self,
1103    ) -> (
1104        EnvBehavior,
1105        LinkedHashSet<EnvKey>,
1106        LinkedHashSet<EnvKey>,
1107        LinkedHashSet<EnvKey>,
1108    ) {
1109        let mut final_behavior = EnvBehavior::default();
1110        let mut final_keep = LinkedHashSet::new();
1111        let mut final_check = LinkedHashSet::new();
1112        let mut final_delete = LinkedHashSet::new();
1113        self.iter_in_options(|opt| {
1114            if let Some(p) = opt.env.borrow().as_ref() {
1115                final_behavior = match p.default_behavior {
1116                    EnvBehavior::Delete => {
1117                        // policy is to delete, so we add whitelist and remove blacklist
1118                        final_keep = final_keep
1119                            .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new()))
1120                            .filter(|e| !p.check.env_matches(e) || !p.delete.env_matches(e))
1121                            .cloned()
1122                            .collect();
1123                        final_check = final_check
1124                            .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
1125                            .filter(|e| !p.delete.env_matches(e))
1126                            .cloned()
1127                            .collect();
1128                        p.default_behavior
1129                    }
1130                    EnvBehavior::Keep => {
1131                        //policy is to keep, so we remove blacklist and add whitelist
1132                        final_delete = final_delete
1133                            .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new()))
1134                            .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e))
1135                            .cloned()
1136                            .collect();
1137                        final_check = final_check
1138                            .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
1139                            .filter(|e| !p.keep.env_matches(e))
1140                            .cloned()
1141                            .collect();
1142                        p.default_behavior
1143                    }
1144                    EnvBehavior::Inherit => {
1145                        if final_behavior.is_delete() {
1146                            final_keep = final_keep
1147                                .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new()))
1148                                .filter(|e| !p.delete.env_matches(e) || !p.check.env_matches(e))
1149                                .cloned()
1150                                .collect();
1151                            final_check = final_check
1152                                .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
1153                                .filter(|e| !p.delete.env_matches(e))
1154                                .cloned()
1155                                .collect();
1156                        } else {
1157                            final_delete = final_delete
1158                                .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new()))
1159                                .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e))
1160                                .cloned()
1161                                .collect();
1162                            final_check = final_check
1163                                .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
1164                                .filter(|e| !p.keep.env_matches(e))
1165                                .cloned()
1166                                .collect();
1167                        }
1168                        final_behavior
1169                    }
1170                };
1171            }
1172        });
1173        (final_behavior, final_keep, final_check, final_delete)
1174    }
1175    pub fn get_root_behavior(&self) -> (Level, SPrivileged) {
1176        self.find_in_options(|opt| {
1177            if let Some(p) = &opt.borrow().root {
1178                return Some((opt.level, *p));
1179            }
1180            None
1181        })
1182        .unwrap_or((Level::None, SPrivileged::default()))
1183    }
1184    pub fn get_bounding(&self) -> (Level, SBounding) {
1185        self.find_in_options(|opt| {
1186            if let Some(p) = &opt.borrow().bounding {
1187                return Some((opt.level, *p));
1188            }
1189            None
1190        })
1191        .unwrap_or((Level::None, SBounding::default()))
1192    }
1193    pub fn get_authentication(&self) -> (Level, SAuthentication) {
1194        self.find_in_options(|opt| {
1195            if let Some(p) = &opt.borrow().authentication {
1196                return Some((opt.level, *p));
1197            }
1198            None
1199        })
1200        .unwrap_or((Level::None, SAuthentication::default()))
1201    }
1202
1203    pub fn get_wildcard(&self) -> (Level, String) {
1204        self.find_in_options(|opt| {
1205            if let Some(p) = opt.borrow().wildcard_denied.borrow().as_ref() {
1206                return Some((opt.level, p.clone()));
1207            }
1208            None
1209        })
1210        .unwrap_or((Level::None, "".to_owned()))
1211    }
1212
1213    pub fn get_timeout(&self) -> (Level, STimeout) {
1214        self.find_in_options(|opt| {
1215            if let Some(p) = &opt.borrow().timeout {
1216                return Some((opt.level, p.clone()));
1217            }
1218            None
1219        })
1220        .unwrap_or((Level::None, STimeout::default()))
1221    }
1222
1223    fn get_level(&self) -> Level {
1224        let (level, _) = self
1225            .find_in_options(|opt| Some((opt.level, ())))
1226            .unwrap_or((Level::None, ()));
1227        level
1228    }
1229
1230    pub fn to_opt(&self) -> Rc<RefCell<Opt>> {
1231        Opt::builder(self.get_level())
1232            .path(self.get_final_path())
1233            .env(self.get_final_env(None))
1234            .maybe_root(
1235                self.find_in_options(|opt| opt.root.map(|root| (opt.level, root)))
1236                    .map(|(_, root)| root),
1237            )
1238            .maybe_bounding(
1239                self.find_in_options(|opt| opt.bounding.map(|bounding| (opt.level, bounding)))
1240                    .map(|(_, bounding)| bounding),
1241            )
1242            .maybe_authentication(
1243                self.find_in_options(|opt| {
1244                    opt.authentication
1245                        .map(|authentication| (opt.level, authentication))
1246                })
1247                .map(|(_, authentication)| authentication),
1248            )
1249            .maybe_wildcard_denied(
1250                self.find_in_options(|opt| {
1251                    opt.wildcard_denied
1252                        .borrow()
1253                        .as_ref()
1254                        .map(|wildcard| (opt.level, wildcard.clone()))
1255                })
1256                .map(|(_, wildcard)| wildcard),
1257            )
1258            .maybe_timeout(
1259                self.find_in_options(|opt| opt.timeout.clone().map(|timeout| (opt.level, timeout)))
1260                    .map(|(_, timeout)| timeout),
1261            )
1262            .build()
1263    }
1264}
1265
1266impl PartialEq for OptStack {
1267    fn eq(&self, other: &Self) -> bool {
1268        // we must assess that every option result in the same final result
1269        let path = self.get_final_path();
1270        let default = LinkedHashSet::new();
1271        let other_path = other.get_final_path();
1272        let res = path.default_behavior == other_path.default_behavior
1273            && path
1274                .add
1275                .as_ref()
1276                .unwrap_or(&default)
1277                .symmetric_difference(other_path.add.as_ref().unwrap_or(&default))
1278                .count()
1279                == 0
1280            && path
1281                .sub
1282                .as_ref()
1283                .unwrap_or(&default)
1284                .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default))
1285                .count()
1286                == 0
1287            && self.get_root_behavior().1 == other.get_root_behavior().1
1288            && self.get_bounding().1 == other.get_bounding().1
1289            && self.get_wildcard().1 == other.get_wildcard().1
1290            && self.get_authentication().1 == other.get_authentication().1
1291            && self.get_timeout().1 == other.get_timeout().1;
1292        debug!(
1293            "final_behavior == other_path.behavior : {}
1294        && add {:?} - other_add {:?} == 0 : {}
1295        && sub - other_sub == 0 : {}
1296        && self.get_root_behavior().1 == other.get_root_behavior().1 : {}
1297        && self.get_bounding().1 == other.get_bounding().1 : {}
1298        && self.get_wildcard().1 == other.get_wildcard().1 : {}
1299        && self.get_authentication().1 == other.get_authentication().1 : {}
1300        && self.get_timeout().1 == other.get_timeout().1 : {}",
1301            path.default_behavior == other_path.default_behavior,
1302            path.add,
1303            other_path.add,
1304            path.add
1305                .as_ref()
1306                .unwrap_or(&default)
1307                .symmetric_difference(other_path.add.as_ref().unwrap_or(&default))
1308                .count()
1309                == 0,
1310            path.sub
1311                .as_ref()
1312                .unwrap_or(&default)
1313                .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default))
1314                .count()
1315                == 0,
1316            self.get_root_behavior().1 == other.get_root_behavior().1,
1317            self.get_bounding().1 == other.get_bounding().1,
1318            self.get_wildcard().1 == other.get_wildcard().1,
1319            self.get_authentication().1 == other.get_authentication().1,
1320            self.get_timeout().1 == other.get_timeout().1
1321        );
1322        debug!("OPT check: {}", res);
1323        res
1324    }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329
1330    use nix::unistd::Pid;
1331
1332    use super::super::options::*;
1333    use super::super::structs::*;
1334
1335    fn env_key_set_equal<I, J>(a: I, b: J) -> bool
1336    where
1337        I: IntoIterator<Item = EnvKey>,
1338        J: IntoIterator<Item = EnvKey>,
1339    {
1340        let mut a_vec: Vec<_> = a.into_iter().collect();
1341        let mut b_vec: Vec<_> = b.into_iter().collect();
1342        a_vec.sort_by(|a, b| a.value.cmp(&b.value));
1343        b_vec.sort_by(|a, b| a.value.cmp(&b.value));
1344        a_vec == b_vec
1345    }
1346
1347    fn hashset_vec_equal<I, J>(a: I, b: J) -> bool
1348    where
1349        I: IntoIterator,
1350        I::Item: Into<String>,
1351        J: IntoIterator,
1352        J::Item: Into<String>,
1353    {
1354        let mut a_vec: Vec<String> = a.into_iter().map(Into::into).collect();
1355        let mut b_vec: Vec<String> = b.into_iter().map(Into::into).collect();
1356        a_vec.sort();
1357        b_vec.sort();
1358        a_vec == b_vec
1359    }
1360
1361    #[test]
1362    fn test_find_in_options() {
1363        let config = SConfig::builder()
1364            .role(
1365                SRole::builder("test")
1366                    .options(|opt| {
1367                        opt.path(
1368                            SPathOptions::builder(PathBehavior::Inherit)
1369                                .add(["path2"])
1370                                .build(),
1371                        )
1372                        .build()
1373                    })
1374                    .build(),
1375            )
1376            .options(|opt| {
1377                opt.path(
1378                    SPathOptions::builder(PathBehavior::Delete)
1379                        .add(["path1"])
1380                        .build(),
1381                )
1382                .build()
1383            })
1384            .build();
1385        let options = OptStack::from_role(config.as_ref().borrow().roles[0].clone());
1386        let res: Option<(Level, SPathOptions)> =
1387            options.find_in_options(|opt| opt.path.clone().map(|value| (opt.level, value)));
1388        assert_eq!(
1389            res,
1390            Some((
1391                Level::Role,
1392                SPathOptions::builder(PathBehavior::Inherit)
1393                    .add(["path2"])
1394                    .build()
1395            ))
1396        );
1397    }
1398
1399    #[cfg(feature = "finder")]
1400    #[test]
1401    fn test_get_path() {
1402        let config = SConfig::builder()
1403            .role(
1404                SRole::builder("test")
1405                    .options(|opt| {
1406                        opt.path(
1407                            SPathOptions::builder(PathBehavior::Inherit)
1408                                .add(["path2"])
1409                                .build(),
1410                        )
1411                        .build()
1412                    })
1413                    .build(),
1414            )
1415            .options(|opt| {
1416                opt.path(
1417                    SPathOptions::builder(PathBehavior::Delete)
1418                        .add(["path1"])
1419                        .build(),
1420                )
1421                .build()
1422            })
1423            .build();
1424        let options = OptStack::from_role(config.as_ref().borrow().roles.first().unwrap().clone());
1425        let res = options.calculate_path();
1426        assert_eq!(res, "path2:path1");
1427    }
1428
1429    #[cfg(feature = "finder")]
1430    #[test]
1431    fn test_get_path_delete() {
1432        let config = SConfig::builder()
1433            .role(
1434                SRole::builder("test")
1435                    .options(|opt| {
1436                        opt.path(
1437                            SPathOptions::builder(PathBehavior::Delete)
1438                                .add(["path2"])
1439                                .build(),
1440                        )
1441                        .build()
1442                    })
1443                    .build(),
1444            )
1445            .options(|opt| {
1446                opt.path(
1447                    SPathOptions::builder(PathBehavior::Delete)
1448                        .add(["path1"])
1449                        .build(),
1450                )
1451                .build()
1452            })
1453            .build();
1454        let options = OptStack::from_role(config.role("test").unwrap()).calculate_path();
1455        assert!(options.contains("path2"));
1456    }
1457
1458    #[cfg(feature = "finder")]
1459    #[test]
1460    fn test_opt_add_sub() {
1461        let config = SConfig::builder()
1462            .role(
1463                SRole::builder("test")
1464                    .options(|opt| {
1465                        opt.path(
1466                            SPathOptions::builder(PathBehavior::Delete)
1467                                .sub(["path1"])
1468                                .build(),
1469                        )
1470                        .build()
1471                    })
1472                    .build(),
1473            )
1474            .options(|opt| {
1475                opt.path(
1476                    SPathOptions::builder(PathBehavior::Delete)
1477                        .add(["path1"])
1478                        .build(),
1479                )
1480                .build()
1481            })
1482            .build();
1483        let options = OptStack::from_role(config.role("test").unwrap()).calculate_path();
1484        assert!(!options.contains("path1"));
1485    }
1486
1487    #[test]
1488    fn test_env_global_to_task() {
1489        let config = SConfig::builder()
1490            .role(
1491                SRole::builder("test")
1492                    .task(
1493                        STask::builder(1)
1494                            .options(|opt| {
1495                                opt.env(
1496                                    SEnvOptions::builder(EnvBehavior::Delete)
1497                                        .keep(["env1"])
1498                                        .unwrap()
1499                                        .build(),
1500                                )
1501                                .build()
1502                            })
1503                            .build(),
1504                    )
1505                    .options(|opt| {
1506                        opt.env(
1507                            SEnvOptions::builder(EnvBehavior::Delete)
1508                                .keep(["env2"])
1509                                .unwrap()
1510                                .build(),
1511                        )
1512                        .build()
1513                    })
1514                    .build(),
1515            )
1516            .options(|opt| {
1517                opt.env(
1518                    SEnvOptions::builder(EnvBehavior::Delete)
1519                        .keep(["env3"])
1520                        .unwrap()
1521                        .build(),
1522                )
1523                .build()
1524            })
1525            .build();
1526        let binding = OptStack::from_task(config.task("test", 1).unwrap()).to_opt();
1527        let options = binding.as_ref().borrow();
1528        let res = &options.env.as_ref().unwrap().keep;
1529        assert!(res
1530            .as_ref()
1531            .unwrap_or(&LinkedHashSet::new())
1532            .contains(&EnvKey::from("env1")));
1533    }
1534
1535    // test to_opt() for OptStack
1536    #[test]
1537    fn test_to_opt() {
1538        let config = SConfig::builder()
1539            .role(
1540                SRole::builder("test")
1541                    .task(
1542                        STask::builder(1)
1543                            .options(|opt| {
1544                                opt.path(
1545                                    SPathOptions::builder(PathBehavior::Inherit)
1546                                        .add(["path3"])
1547                                        .build(),
1548                                )
1549                                .env(
1550                                    SEnvOptions::builder(EnvBehavior::Inherit)
1551                                        .keep(["env3"])
1552                                        .unwrap()
1553                                        .build(),
1554                                )
1555                                .root(SPrivileged::User)
1556                                .bounding(SBounding::Strict)
1557                                .authentication(SAuthentication::Perform)
1558                                .timeout(
1559                                    STimeout::builder()
1560                                        .type_field(TimestampType::TTY)
1561                                        .duration(Duration::minutes(3))
1562                                        .build(),
1563                                )
1564                                .wildcard_denied("c")
1565                                .build()
1566                            })
1567                            .build(),
1568                    )
1569                    .options(|opt| {
1570                        opt.path(
1571                            SPathOptions::builder(PathBehavior::Inherit)
1572                                .add(["path2"])
1573                                .build(),
1574                        )
1575                        .env(
1576                            SEnvOptions::builder(EnvBehavior::Delete)
1577                                .keep(["env1"])
1578                                .unwrap()
1579                                .build(),
1580                        )
1581                        .root(SPrivileged::Privileged)
1582                        .bounding(SBounding::Strict)
1583                        .authentication(SAuthentication::Skip)
1584                        .timeout(
1585                            STimeout::builder()
1586                                .type_field(TimestampType::PPID)
1587                                .duration(Duration::minutes(2))
1588                                .build(),
1589                        )
1590                        .wildcard_denied("b")
1591                        .build()
1592                    })
1593                    .build(),
1594            )
1595            .options(|opt| {
1596                opt.path(
1597                    SPathOptions::builder(PathBehavior::Delete)
1598                        .add(["path1"])
1599                        .build(),
1600                )
1601                .env(
1602                    SEnvOptions::builder(EnvBehavior::Delete)
1603                        .keep(["env2"])
1604                        .unwrap()
1605                        .build(),
1606                )
1607                .root(SPrivileged::Privileged)
1608                .bounding(SBounding::Ignore)
1609                .authentication(SAuthentication::Perform)
1610                .timeout(
1611                    STimeout::builder()
1612                        .type_field(TimestampType::TTY)
1613                        .duration(Duration::minutes(1))
1614                        .build(),
1615                )
1616                .wildcard_denied("a")
1617                .build()
1618            })
1619            .build();
1620        let default = LinkedHashSet::new();
1621        let stack = OptStack::from_roles(config.clone());
1622        let opt = stack.to_opt();
1623        let global_options = opt.as_ref().borrow();
1624        assert_eq!(
1625            global_options.path.as_ref().unwrap().default_behavior,
1626            PathBehavior::Delete
1627        );
1628        assert!(hashset_vec_equal(
1629            global_options
1630                .path
1631                .as_ref()
1632                .unwrap()
1633                .add
1634                .as_ref()
1635                .unwrap_or(&default)
1636                .clone(),
1637            vec!["path1"]
1638        ));
1639        assert_eq!(
1640            global_options.env.as_ref().unwrap().default_behavior,
1641            EnvBehavior::Delete
1642        );
1643        assert!(env_key_set_equal(
1644            global_options
1645                .env
1646                .as_ref()
1647                .unwrap()
1648                .keep
1649                .as_ref()
1650                .unwrap_or(&LinkedHashSet::new())
1651                .clone(),
1652            vec![EnvKey::from("env2")]
1653        ));
1654        assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged);
1655        assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore);
1656        assert_eq!(
1657            global_options.authentication.unwrap(),
1658            SAuthentication::Perform
1659        );
1660        assert_eq!(
1661            global_options.timeout.as_ref().unwrap().duration.unwrap(),
1662            Duration::minutes(1)
1663        );
1664        assert_eq!(
1665            global_options.timeout.as_ref().unwrap().type_field.unwrap(),
1666            TimestampType::TTY
1667        );
1668        assert_eq!(global_options.wildcard_denied.as_ref().unwrap(), "a");
1669        let opt = OptStack::from_role(config.clone().role("test").unwrap()).to_opt();
1670        let role_options = opt.as_ref().borrow();
1671        assert_eq!(
1672            role_options.path.as_ref().unwrap().default_behavior,
1673            PathBehavior::Delete
1674        );
1675        assert!(hashset_vec_equal(
1676            role_options
1677                .path
1678                .as_ref()
1679                .unwrap()
1680                .add
1681                .as_ref()
1682                .unwrap_or(&default)
1683                .clone(),
1684            vec!["path1", "path2"]
1685        ));
1686        assert_eq!(
1687            role_options.env.as_ref().unwrap().default_behavior,
1688            EnvBehavior::Delete
1689        );
1690        assert!(env_key_set_equal(
1691            role_options
1692                .env
1693                .as_ref()
1694                .unwrap()
1695                .keep
1696                .as_ref()
1697                .unwrap_or(&LinkedHashSet::new())
1698                .clone(),
1699            vec![EnvKey::from("env1")]
1700        ));
1701        assert_eq!(role_options.root.unwrap(), SPrivileged::Privileged);
1702        assert_eq!(role_options.bounding.unwrap(), SBounding::Strict);
1703        assert_eq!(role_options.authentication.unwrap(), SAuthentication::Skip);
1704        assert_eq!(
1705            role_options.timeout.as_ref().unwrap().duration.unwrap(),
1706            Duration::minutes(2)
1707        );
1708        assert_eq!(
1709            role_options.timeout.as_ref().unwrap().type_field.unwrap(),
1710            TimestampType::PPID
1711        );
1712        assert_eq!(role_options.wildcard_denied.as_ref().unwrap(), "b");
1713        let opt = OptStack::from_task(config.task("test", 1).unwrap()).to_opt();
1714        let task_options = opt.as_ref().borrow();
1715        assert_eq!(
1716            task_options.path.as_ref().unwrap().default_behavior,
1717            PathBehavior::Delete
1718        );
1719        assert!(hashset_vec_equal(
1720            task_options
1721                .path
1722                .as_ref()
1723                .unwrap()
1724                .add
1725                .as_ref()
1726                .unwrap_or(&default)
1727                .clone(),
1728            vec!["path1", "path2", "path3"]
1729        ));
1730        assert_eq!(
1731            task_options.env.as_ref().unwrap().default_behavior,
1732            EnvBehavior::Delete
1733        );
1734        assert!(env_key_set_equal(
1735            task_options
1736                .env
1737                .as_ref()
1738                .unwrap()
1739                .keep
1740                .as_ref()
1741                .unwrap_or(&LinkedHashSet::new())
1742                .clone(),
1743            vec![EnvKey::from("env1"), EnvKey::from("env3")]
1744        ));
1745        assert_eq!(task_options.root.unwrap(), SPrivileged::User);
1746        assert_eq!(task_options.bounding.unwrap(), SBounding::Strict);
1747        assert_eq!(
1748            task_options.authentication.unwrap(),
1749            SAuthentication::Perform
1750        );
1751        assert_eq!(
1752            task_options.timeout.as_ref().unwrap().duration.unwrap(),
1753            Duration::minutes(3)
1754        );
1755        assert_eq!(
1756            task_options.timeout.as_ref().unwrap().type_field.unwrap(),
1757            TimestampType::TTY
1758        );
1759        assert_eq!(task_options.wildcard_denied.as_ref().unwrap(), "c");
1760    }
1761
1762    #[test]
1763    fn test_get_timeout() {
1764        let config = SConfig::builder()
1765            .role(
1766                SRole::builder("test")
1767                    .options(|opt| {
1768                        opt.timeout(STimeout::builder().duration(Duration::minutes(5)).build())
1769                            .build()
1770                    })
1771                    .build(),
1772            )
1773            .options(|opt| {
1774                opt.timeout(
1775                    STimeout::builder()
1776                        .type_field(TimestampType::PPID)
1777                        .duration(Duration::minutes(10))
1778                        .build(),
1779                )
1780                .build()
1781            })
1782            .build();
1783        let options = OptStack::from_role(config.role("test").unwrap()).get_timeout();
1784        assert_eq!(options.1.duration.unwrap(), Duration::minutes(5));
1785        assert_eq!(options.0, Level::Role);
1786        assert!(options.1.type_field.is_none());
1787    }
1788
1789    #[test]
1790    fn test_get_root_behavior() {
1791        let config = SConfig::builder()
1792            .role(
1793                SRole::builder("test")
1794                    .task(STask::builder(1).build())
1795                    .options(|opt| opt.root(SPrivileged::User).build())
1796                    .build(),
1797            )
1798            .options(|opt| opt.root(SPrivileged::Privileged).build())
1799            .build();
1800        let (level, sprivilege) =
1801            OptStack::from_task(config.task("test", 1).unwrap()).get_root_behavior();
1802        assert_eq!(level, Level::Role);
1803        assert_eq!(sprivilege, SPrivileged::User);
1804    }
1805
1806    #[test]
1807    fn test_get_bounding() {
1808        let config = SConfig::builder()
1809            .role(
1810                SRole::builder("test")
1811                    .options(|opt| opt.bounding(SBounding::Strict).build())
1812                    .build(),
1813            )
1814            .options(|opt| opt.bounding(SBounding::Ignore).build())
1815            .build();
1816        let (level, bounding) = OptStack::from_role(config.role("test").unwrap()).get_bounding();
1817        assert_eq!(level, Level::Role);
1818        assert_eq!(bounding, SBounding::Strict);
1819    }
1820
1821    #[test]
1822    fn test_get_wildcard() {
1823        let config = SConfig::builder()
1824            .role(
1825                SRole::builder("test")
1826                    .options(|opt| opt.wildcard_denied("b").build())
1827                    .build(),
1828            )
1829            .options(|opt| opt.wildcard_denied("a").build())
1830            .build();
1831        let (level, wildcard) = OptStack::from_role(config.role("test").unwrap()).get_wildcard();
1832        assert_eq!(level, Level::Role);
1833        assert_eq!(wildcard, "b");
1834    }
1835
1836    #[cfg(feature = "finder")]
1837    #[test]
1838    fn test_tz_is_safe() {
1839        assert!(tz_is_safe("America/New_York"));
1840        assert!(!tz_is_safe("/America/New_York"));
1841        assert!(!tz_is_safe("America/New_York/.."));
1842        //assert path max
1843        assert!(!tz_is_safe(
1844            String::from_utf8(vec![b'a'; (PATH_MAX + 1).try_into().unwrap()])
1845                .unwrap()
1846                .as_str()
1847        ));
1848    }
1849
1850    #[cfg(feature = "finder")]
1851    #[test]
1852    fn test_check_env() {
1853        let config = SConfig::builder()
1854            .role(
1855                SRole::builder("test")
1856                    .options(|opt| {
1857                        opt.env(
1858                            SEnvOptions::builder(EnvBehavior::Inherit)
1859                                .check(["env2"])
1860                                .unwrap()
1861                                .build(),
1862                        )
1863                        .build()
1864                    })
1865                    .task(
1866                        STask::builder(IdTask::Number(1))
1867                            .options(|opt| {
1868                                opt.env(
1869                                    SEnvOptions::builder(EnvBehavior::Inherit)
1870                                        .keep(["env1"])
1871                                        .unwrap()
1872                                        .build(),
1873                                )
1874                                .build()
1875                            })
1876                            .build(),
1877                    )
1878                    .build(),
1879            )
1880            .options(|opt| {
1881                opt.env(
1882                    SEnvOptions::builder(EnvBehavior::Delete)
1883                        .check(["env3"])
1884                        .unwrap()
1885                        .set([("env4".to_string(), "value4".to_string())])
1886                        .build(),
1887                )
1888                .build()
1889            })
1890            .build();
1891        let options = OptStack::from_task(config.task("test", 1).unwrap());
1892        let mut test_env = HashMap::new();
1893        test_env.insert("env1".to_string(), "value1".to_string());
1894        test_env.insert("env2".into(), "va%lue2".into());
1895        test_env.insert("env3".into(), "value3".into());
1896        let cred = Cred::builder()
1897            .user_id(0)
1898            .group_id(0)
1899            .ppid(Pid::from_raw(0))
1900            .build();
1901        let result = options
1902            .calculate_filtered_env(None, cred, test_env.into_iter())
1903            .unwrap();
1904        assert_eq!(result.get("env1").unwrap(), "value1");
1905        assert_eq!(result.get("env3").unwrap(), "value3");
1906        assert!(result.get("env2").is_none());
1907        assert_eq!(result.get("env4").unwrap(), "value4");
1908    }
1909
1910    #[cfg(feature = "finder")]
1911    #[test]
1912    fn test_override_env() {
1913        let config = SConfig::builder()
1914            .role(
1915                SRole::builder("test")
1916                    .task(
1917                        STask::builder(IdTask::Number(1))
1918                            .options(|opt| {
1919                                opt.env(
1920                                    SEnvOptions::builder(EnvBehavior::Inherit)
1921                                        .keep(["env1"])
1922                                        .unwrap()
1923                                        .build(),
1924                                )
1925                                .build()
1926                            })
1927                            .build(),
1928                    )
1929                    .options(|opt| {
1930                        opt.env(
1931                            SEnvOptions::builder(EnvBehavior::Inherit)
1932                                .check(["env2"])
1933                                .unwrap()
1934                                .build(),
1935                        )
1936                        .build()
1937                    })
1938                    .build(),
1939            )
1940            .options(|opt| {
1941                opt.env(
1942                    SEnvOptions::builder(EnvBehavior::Delete)
1943                        .check(["env3"])
1944                        .unwrap()
1945                        .set([("env4".to_string(), "value4".to_string())])
1946                        .build(),
1947                )
1948                .build()
1949            })
1950            .build();
1951
1952        let options = OptStack::from_task(config.task("test", 1).unwrap());
1953        let mut test_env = HashMap::new();
1954        test_env.insert("env1".to_string(), "value1".to_string());
1955        test_env.insert("env2".into(), "va%lue2".into());
1956        test_env.insert("env3".into(), "value3".into());
1957        let cred = Cred::builder().user_id(0).group_id(0).build();
1958        let result = options
1959            .calculate_filtered_env(None, cred, test_env.into_iter())
1960            .unwrap();
1961        assert_eq!(result.get("env1").unwrap(), "value1");
1962        assert_eq!(result.get("env3").unwrap(), "value3");
1963        assert!(result.get("env2").is_none());
1964        assert_eq!(result.get("env4").unwrap(), "value4");
1965    }
1966
1967    #[test]
1968    fn is_wildcard_env_key() {
1969        assert!(!is_valid_env_name("TEST_.*"));
1970        assert!(!is_valid_env_name("123"));
1971        assert!(!is_valid_env_name(""));
1972        assert!(is_regex("TEST_.*"));
1973    }
1974
1975    #[test]
1976    fn test_wildcard_env() {
1977        let config = SConfig::builder()
1978            .role(
1979                SRole::builder("test")
1980                    .task(
1981                        STask::builder(IdTask::Number(1))
1982                            .options(|opt| {
1983                                opt.env(
1984                                    SEnvOptions::builder(EnvBehavior::Delete)
1985                                        .keep(["TEST_.*"])
1986                                        .unwrap()
1987                                        .build(),
1988                                )
1989                                .build()
1990                            })
1991                            .build(),
1992                    )
1993                    .build(),
1994            )
1995            .build();
1996        let options = OptStack::from_task(config.task("test", 1).unwrap());
1997        let mut test_env = HashMap::new();
1998        test_env.insert("TEST_A".to_string(), "value1".to_string());
1999        test_env.insert("TEST_B".into(), "value2".into());
2000        test_env.insert("TESTaA".into(), "value3".into());
2001        let cred = Cred::builder().user_id(0).group_id(0).build();
2002        let result = options
2003            .calculate_filtered_env(None, cred, test_env.into_iter())
2004            .unwrap();
2005        assert_eq!(result.get("TEST_A").unwrap(), "value1");
2006        assert_eq!(result.get("TEST_B").unwrap(), "value2");
2007        assert!(result.get("TESTaA").is_none());
2008    }
2009
2010    #[test]
2011    fn test_safe_path() {
2012        let path = std::env::var("PATH").unwrap();
2013        std::env::set_var("PATH", "/sys:./proc:/tmp:/bin");
2014        let config = SConfig::builder()
2015            .role(
2016                SRole::builder("test")
2017                    .task(
2018                        STask::builder(IdTask::Number(1))
2019                            .options(|opt| {
2020                                opt.path(
2021                                    SPathOptions::builder(PathBehavior::KeepSafe)
2022                                        .add(Vec::<String>::new())
2023                                        .sub(["/sys"])
2024                                        .build(),
2025                                )
2026                                .build()
2027                            })
2028                            .build(),
2029                    )
2030                    .build(),
2031            )
2032            .build();
2033        let options = OptStack::from_task(config.task("test", 1).unwrap());
2034        let res = options.calculate_path();
2035
2036        assert_eq!(res, "/tmp:/bin");
2037        std::env::set_var("PATH", path);
2038    }
2039
2040    #[test]
2041    fn test_unsafe_path() {
2042        let path = std::env::var("PATH").unwrap();
2043
2044        let config = SConfig::builder()
2045            .role(
2046                SRole::builder("test")
2047                    .task(
2048                        STask::builder(IdTask::Number(1))
2049                            .options(|opt| {
2050                                opt.path(
2051                                    SPathOptions::builder(PathBehavior::KeepUnsafe)
2052                                        .add(Vec::<String>::new())
2053                                        .sub(["/sys"])
2054                                        .build(),
2055                                )
2056                                .build()
2057                            })
2058                            .build(),
2059                    )
2060                    .build(),
2061            )
2062            .build();
2063        let options = OptStack::from_task(config.task("test", 1).unwrap());
2064        std::env::set_var("PATH", "/sys:./proc:/tmp:/bin");
2065        let res = options.calculate_path();
2066        assert_eq!(res, "./proc:/tmp:/bin");
2067        std::env::set_var("PATH", path);
2068    }
2069
2070    #[test]
2071    fn test_inherit_keep_path() {
2072        let path = std::env::var("PATH").unwrap();
2073        let config = SConfig::builder()
2074            .role(
2075                SRole::builder("test")
2076                    .task(
2077                        STask::builder(IdTask::Number(1))
2078                            .options(|opt| {
2079                                opt.path(
2080                                    SPathOptions::builder(PathBehavior::Inherit)
2081                                        .add(Vec::<String>::new())
2082                                        .sub(["/sys"])
2083                                        .build(),
2084                                )
2085                                .build()
2086                            })
2087                            .build(),
2088                    )
2089                    .options(|opt| {
2090                        opt.path(
2091                            SPathOptions::builder(PathBehavior::KeepSafe)
2092                                .add(Vec::<String>::new())
2093                                .sub(["/tmp"])
2094                                .build(),
2095                        )
2096                        .build()
2097                    })
2098                    .build(),
2099            )
2100            .build();
2101        let options = OptStack::from_task(config.task("test", 1).unwrap());
2102        std::env::set_var("PATH", "/sys:./proc:/tmp:/bin");
2103        let res = options.calculate_path();
2104
2105        assert_eq!(res, "/bin");
2106        std::env::set_var("PATH", path);
2107    }
2108
2109    #[test]
2110    fn test_final_env_keep() {
2111        let config = SConfig::builder()
2112            .role(
2113                SRole::builder("test")
2114                    .task(
2115                        STask::builder(IdTask::Number(1))
2116                            .options(|opt| {
2117                                opt.env(
2118                                    SEnvOptions::builder(EnvBehavior::Inherit)
2119                                        .delete(["env1"])
2120                                        .unwrap()
2121                                        .build(),
2122                                )
2123                                .build()
2124                            })
2125                            .build(),
2126                    )
2127                    .options(|opt| {
2128                        opt.env(
2129                            SEnvOptions::builder(EnvBehavior::Inherit)
2130                                .delete(["env2"])
2131                                .unwrap()
2132                                .build(),
2133                        )
2134                        .build()
2135                    })
2136                    .build(),
2137            )
2138            .options(|opt| {
2139                opt.env(
2140                    SEnvOptions::builder(EnvBehavior::Keep)
2141                        .delete(["env3"])
2142                        .unwrap()
2143                        .build(),
2144                )
2145                .build()
2146            })
2147            .build();
2148        let options = OptStack::from_task(config.task("test", 1).unwrap());
2149        let test_env = [
2150            ("env1", "value1"),
2151            ("env2", "value2"),
2152            ("env3", "value3"),
2153            ("env4", "value4"),
2154            ("env5", "value5"),
2155        ]
2156        .iter()
2157        .map(|(k, v)| (k.to_string(), v.to_string()));
2158
2159        let cred = Cred::builder().user_id(0).group_id(0).build();
2160        let result = options
2161            .calculate_filtered_env(None, cred, test_env.into_iter())
2162            .unwrap();
2163        assert!(result.get("env1").is_none());
2164        assert!(result.get("env2").is_none());
2165        assert!(result.get("env3").is_none());
2166        assert_eq!(result.get("env4").unwrap(), "value4");
2167        assert_eq!(result.get("env5").unwrap(), "value5");
2168    }
2169
2170    #[test]
2171    fn test_opt_filter_env() {
2172        let config = SConfig::builder()
2173            .role(
2174                SRole::builder("test")
2175                    .task(
2176                        STask::builder(IdTask::Number(1))
2177                            .options(|opt| {
2178                                opt.env(
2179                                    SEnvOptions::builder(EnvBehavior::Delete)
2180                                        .delete(["envA"])
2181                                        .unwrap()
2182                                        .override_behavior(true)
2183                                        .build(),
2184                                )
2185                                .build()
2186                            })
2187                            .build(),
2188                    )
2189                    .build(),
2190            )
2191            .build();
2192        let options = OptStack::from_task(config.task("test", 1).unwrap());
2193        let test_env = [("envA", "value1"), ("envB", "value2"), ("envC", "value3")]
2194            .iter()
2195            .map(|(k, v)| (k.to_string(), v.to_string()));
2196
2197        let cred = Cred::builder().user_id(0).group_id(0).build();
2198        let result = options
2199            .calculate_filtered_env(
2200                Some(
2201                    FilterMatcher::builder()
2202                        .env_behavior(EnvBehavior::Keep)
2203                        .build(),
2204                ),
2205                cred,
2206                test_env.into_iter(),
2207            )
2208            .unwrap();
2209        assert!(result.get("envA").is_none());
2210        assert_eq!(result.get("envB").unwrap(), "value2");
2211        assert_eq!(result.get("envC").unwrap(), "value3");
2212    }
2213}