use std::borrow::Cow;
use std::collections::HashMap;
use std::num::ParseIntError;
use std::result::Result;
use std::str::FromStr;
use std::{borrow::Borrow, cell::RefCell, rc::Rc};
use bon::{bon, Builder};
use chrono::Duration;
use konst::eq_str;
use linked_hash_set::LinkedHashSet;
use nix::sys::stat::Mode;
#[cfg(feature = "pcre2")]
use pcre2::bytes::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use strum::{Display, EnumIs, EnumIter, EnumString, FromRepr};
use log::debug;
use crate::rc_refcell;
use crate::util::{
AUTHENTICATION, BOUNDING, ENV_CHECK_LIST, ENV_DEFAULT_BEHAVIOR, ENV_DELETE_LIST, ENV_KEEP_LIST,
ENV_OVERRIDE_BEHAVIOR, ENV_PATH_ADD_LIST_SLICE, ENV_PATH_BEHAVIOR, ENV_PATH_REMOVE_LIST_SLICE,
ENV_SET_LIST, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2,
HARDENED_ENUM_VALUE_3, INFO, PRIVILEGED, TIMEOUT_DURATION, TIMEOUT_TYPE, UMASK,
};
use super::{deserialize_duration, is_default, serialize_duration, FilterMatcher};
use super::{
lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey,
structs::{SConfig, SRole, STask},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
#[repr(u8)]
pub enum Level {
#[default]
None,
Default,
Global,
Role,
Task,
}
#[derive(Debug, Clone, Copy, FromRepr, EnumIter, Display)]
pub enum OptType {
Path,
Env,
Root,
Bounding,
Timeout,
Authentication,
ExecInfo,
UMask,
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
#[repr(u32)]
pub enum PathBehavior {
Delete = HARDENED_ENUM_VALUE_0,
KeepSafe = HARDENED_ENUM_VALUE_1,
KeepUnsafe = HARDENED_ENUM_VALUE_2,
#[default]
Inherit = HARDENED_ENUM_VALUE_3,
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
#[repr(u8)]
pub enum TimestampType {
#[default]
PPID,
TTY,
UID,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)]
pub struct STimeout {
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub type_field: Option<TimestampType>,
#[serde(
serialize_with = "serialize_duration",
deserialize_with = "deserialize_duration",
skip_serializing_if = "Option::is_none"
)]
pub duration: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_usage: Option<u64>,
#[serde(default)]
#[serde(flatten, skip_serializing_if = "Map::is_empty")]
#[builder(default)]
pub _extra_fields: Map<String, Value>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder, Default)]
pub struct SPathOptions {
#[serde(rename = "default", default, skip_serializing_if = "is_default")]
#[builder(start_fn)]
pub default_behavior: PathBehavior,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "lhs_deserialize",
serialize_with = "lhs_serialize"
)]
#[builder(with = |v : impl IntoIterator<Item = impl ToString>| { v.into_iter().map(|s| s.to_string()).collect() })]
pub add: Option<LinkedHashSet<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "lhs_deserialize",
serialize_with = "lhs_serialize",
alias = "del"
)]
#[builder(with = |v : impl IntoIterator<Item = impl ToString>| { v.into_iter().map(|s| s.to_string()).collect() })]
pub sub: Option<LinkedHashSet<String>>,
}
impl SPathOptions {}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
#[repr(u32)]
pub enum EnvBehavior {
Delete = HARDENED_ENUM_VALUE_0,
Keep = HARDENED_ENUM_VALUE_1,
#[default]
Inherit = HARDENED_ENUM_VALUE_2,
}
#[derive(Serialize, Hash, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)]
pub enum EnvKeyType {
Wildcarded,
Normal,
}
#[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone, Builder)]
#[serde(transparent)]
pub struct EnvKey {
#[serde(skip)]
env_type: EnvKeyType,
value: String,
}
impl std::fmt::Display for EnvKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)]
pub struct SEnvOptions {
#[serde(rename = "default", default, skip_serializing_if = "is_default")]
#[builder(start_fn)]
pub default_behavior: EnvBehavior,
#[serde(alias = "override", default, skip_serializing_if = "Option::is_none")]
pub override_behavior: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(with = |iter: impl IntoIterator<Item = (impl ToString, impl ToString)>| {
let mut map = HashMap::with_hasher(Default::default());
map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string())));
map
})]
pub set: Option<HashMap<String, String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "lhs_deserialize_envkey",
serialize_with = "lhs_serialize_envkey"
)]
#[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)})]
pub keep: Option<LinkedHashSet<EnvKey>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "lhs_deserialize_envkey",
serialize_with = "lhs_serialize_envkey"
)]
#[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)})]
pub check: Option<LinkedHashSet<EnvKey>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "lhs_deserialize_envkey",
serialize_with = "lhs_serialize_envkey"
)]
#[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)})]
pub delete: Option<LinkedHashSet<EnvKey>>,
#[serde(default, flatten)]
#[builder(default)]
pub _extra_fields: Map<String, Value>,
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
#[repr(u32)]
pub enum SBounding {
Strict = HARDENED_ENUM_VALUE_0,
Ignore = HARDENED_ENUM_VALUE_2,
}
impl Default for SBounding {
fn default() -> Self {
BOUNDING
}
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "kebab-case")]
#[repr(u32)]
pub enum SPrivileged {
User = HARDENED_ENUM_VALUE_0,
Privileged = HARDENED_ENUM_VALUE_1,
}
impl Default for SPrivileged {
fn default() -> Self {
PRIVILEGED
}
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "kebab-case")]
#[repr(u32)]
pub enum SAuthentication {
Perform = HARDENED_ENUM_VALUE_0,
Skip = HARDENED_ENUM_VALUE_1,
}
impl Default for SAuthentication {
fn default() -> Self {
AUTHENTICATION
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SUMask(
#[serde(
deserialize_with = "deserialize_umask",
serialize_with = "serialize_umask"
)]
pub u16,
);
impl Default for SUMask {
fn default() -> Self {
UMASK
}
}
impl From<SUMask> for Mode {
fn from(umask: SUMask) -> Self {
Mode::from_bits_truncate(umask.0 as u32)
}
}
impl FromStr for SUMask {
type Err = ParseIntError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
u16::from_str_radix(s, 8).map(SUMask)
}
}
fn serialize_umask<S>(value: &u16, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{:03o}", value))
}
fn deserialize_umask<'de, D>(deserializer: D) -> Result<u16, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Cow<'de, str> = Deserialize::deserialize(deserializer)?;
SUMask::from_str(&s)
.map(|umask| umask.0)
.map_err(serde::de::Error::custom)
}
impl From<SUMask> for u16 {
fn from(val: SUMask) -> Self {
val.0
}
}
impl From<u16> for SUMask {
fn from(val: u16) -> Self {
SUMask(val)
}
}
#[derive(
Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "kebab-case")]
#[derive(Default)]
#[repr(u32)]
pub enum SInfo {
#[default]
Hide = HARDENED_ENUM_VALUE_0,
Show = HARDENED_ENUM_VALUE_1,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Opt {
#[serde(skip)]
pub level: Level,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<SPathOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<SEnvOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<SPrivileged>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounding: Option<SBounding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<SAuthentication>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execinfo: Option<SInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<STimeout>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub umask: Option<SUMask>,
#[serde(default, flatten)]
pub _extra_fields: Map<String, Value>,
}
#[bon]
impl Opt {
#[builder]
pub fn new(
#[builder(start_fn)] level: Level,
path: Option<SPathOptions>,
env: Option<SEnvOptions>,
root: Option<SPrivileged>,
bounding: Option<SBounding>,
authentication: Option<SAuthentication>,
execinfo: Option<SInfo>,
timeout: Option<STimeout>,
umask: Option<SUMask>,
#[builder(default)] _extra_fields: Map<String, Value>,
) -> Self {
Opt {
level,
path,
env,
root,
bounding,
authentication,
execinfo,
timeout,
umask,
_extra_fields,
}
}
pub fn level_default() -> Self {
Self::builder(Level::Default)
.root(PRIVILEGED)
.bounding(BOUNDING)
.path(SPathOptions::level_default())
.authentication(AUTHENTICATION)
.execinfo(INFO)
.umask(UMASK)
.env(
SEnvOptions::builder(ENV_DEFAULT_BEHAVIOR)
.keep(ENV_KEEP_LIST)
.unwrap()
.check(ENV_CHECK_LIST)
.unwrap()
.delete(ENV_DELETE_LIST)
.unwrap()
.set(ENV_SET_LIST)
.override_behavior(ENV_OVERRIDE_BEHAVIOR)
.build(),
)
.timeout(
STimeout::builder()
.type_field(TIMEOUT_TYPE)
.duration(TIMEOUT_DURATION)
.build(),
)
.build()
}
}
impl SPathOptions {
pub fn level_default() -> Self {
SPathOptions::builder(ENV_PATH_BEHAVIOR)
.add(ENV_PATH_ADD_LIST_SLICE)
.sub(ENV_PATH_REMOVE_LIST_SLICE)
.build()
}
}
fn is_valid_env_name(s: &str) -> bool {
let mut chars = s.chars();
if let Some(first_char) = chars.next() {
if !(first_char.is_ascii_alphabetic() || first_char == '_') {
return false;
}
} else {
return false; }
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(feature = "pcre2")]
fn is_regex(s: &str) -> bool {
Regex::new(&format!("^{}$", s)).is_ok()
}
#[cfg(not(feature = "pcre2"))]
fn is_regex(_s: &str) -> bool {
false }
impl EnvKey {
pub fn new(s: String) -> Result<Self, String> {
if is_valid_env_name(&s) {
Ok(EnvKey {
env_type: EnvKeyType::Normal,
value: s,
})
} else if is_regex(&s) {
Ok(EnvKey {
env_type: EnvKeyType::Wildcarded,
value: s,
})
} else {
Err(format!(
"env key {}, must be a valid env, or a valid regex",
s
))
}
}
}
impl PartialEq<str> for EnvKey {
fn eq(&self, other: &str) -> bool {
self.value == *other
}
}
impl From<EnvKey> for String {
fn from(val: EnvKey) -> Self {
val.value
}
}
impl From<String> for EnvKey {
fn from(s: String) -> Self {
EnvKey::new(s).expect("Invalid env key")
}
}
impl From<&str> for EnvKey {
fn from(s: &str) -> Self {
EnvKey::new(s.into()).expect("Invalid env key")
}
}
impl<'de> Deserialize<'de> for EnvKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
EnvKey::new(s).map_err(serde::de::Error::custom)
}
}
trait EnvSet {
fn env_matches(&self, wildcarded: &EnvKey) -> bool;
}
impl EnvSet for LinkedHashSet<EnvKey> {
fn env_matches(&self, needle: &EnvKey) -> bool {
self.iter().any(|s| match s.env_type {
EnvKeyType::Normal => s == needle,
EnvKeyType::Wildcarded => check_wildcarded(s, &needle.value),
})
}
}
impl EnvSet for Option<LinkedHashSet<EnvKey>> {
fn env_matches(&self, needle: &EnvKey) -> bool {
self.as_ref().is_some_and(|set| set.env_matches(needle))
}
}
#[cfg(feature = "pcre2")]
fn check_wildcarded(wildcarded: &EnvKey, s: &String) -> bool {
Regex::new(&format!("^{}$", wildcarded.value)) .unwrap()
.is_match(s.as_bytes())
.is_ok_and(|m| m)
}
#[cfg(not(feature = "pcre2"))]
fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool {
true
}
#[derive(Debug, PartialEq)]
pub struct ConstParseError(pub &'static str);
use std::fmt::{self, Display};
impl Display for ConstParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"Failed to parse the const {} defined in .cargo/config.toml",
self.0
))
}
}
impl ConstParseError {
const fn panic(&self) -> ! {
panic!("failed to parse a const")
}
}
impl PathBehavior {
pub const fn try_parse(input: &str) -> std::result::Result<PathBehavior, ConstParseError> {
match input {
_ if eq_str(input, "delete") => Ok(PathBehavior::Delete),
_ if eq_str(input, "keep_safe") => Ok(PathBehavior::KeepSafe),
_ if eq_str(input, "keep_unsafe") => Ok(PathBehavior::KeepUnsafe),
_ if eq_str(input, "inherit") => Ok(PathBehavior::Inherit),
_ => ConstParseError("PathBehavior").panic(),
}
}
}
impl EnvBehavior {
pub const fn try_parse(input: &str) -> std::result::Result<EnvBehavior, ConstParseError> {
match input {
_ if eq_str(input, "delete") => Ok(EnvBehavior::Delete),
_ if eq_str(input, "keep") => Ok(EnvBehavior::Keep),
_ if eq_str(input, "inherit") => Ok(EnvBehavior::Inherit),
_ => ConstParseError("EnvBehavior").panic(),
}
}
}
impl SPrivileged {
pub const fn try_parse(input: &str) -> std::result::Result<SPrivileged, ConstParseError> {
match input {
_ if eq_str(input, "user") => Ok(SPrivileged::User),
_ if eq_str(input, "privileged") => Ok(SPrivileged::Privileged),
_ => ConstParseError("SPrivileged").panic(),
}
}
}
impl SInfo {
pub const fn try_parse(input: &str) -> std::result::Result<SInfo, ConstParseError> {
match input {
_ if eq_str(input, "hide") => Ok(SInfo::Hide),
_ if eq_str(input, "show") => Ok(SInfo::Show),
_ => ConstParseError("SInfo").panic(),
}
}
}
impl TimestampType {
pub const fn try_parse(input: &str) -> std::result::Result<TimestampType, ConstParseError> {
match input {
_ if eq_str(input, "ppid") => Ok(TimestampType::PPID),
_ if eq_str(input, "tty") => Ok(TimestampType::TTY),
_ if eq_str(input, "uid") => Ok(TimestampType::UID),
_ => ConstParseError("TimestampType").panic(),
}
}
}
impl SBounding {
pub const fn try_parse(input: &str) -> std::result::Result<SBounding, ConstParseError> {
match input {
_ if eq_str(input, "strict") => Ok(SBounding::Strict),
_ if eq_str(input, "ignore") => Ok(SBounding::Ignore),
_ => ConstParseError("SBounding").panic(),
}
}
}
impl SAuthentication {
pub const fn try_parse(input: &str) -> std::result::Result<SAuthentication, ConstParseError> {
match input {
_ if eq_str(input, "perform") => Ok(SAuthentication::Perform),
_ if eq_str(input, "skip") => Ok(SAuthentication::Skip),
_ => ConstParseError("SAuthentication").panic(),
}
}
}
impl Default for Opt {
fn default() -> Self {
Opt {
level: Level::None,
..Opt::level_default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptStack {
pub(crate) stack: [Option<Rc<RefCell<Opt>>>; 5],
roles: Option<Rc<RefCell<SConfig>>>,
role: Option<Rc<RefCell<SRole>>>,
task: Option<Rc<RefCell<STask>>>,
}
#[cfg(not(tarpaulin_include))]
impl<S: opt_stack_builder::State> OptStackBuilder<S> {
fn opt(mut self, opt: Option<Rc<RefCell<Opt>>>) -> Self {
if let Some(opt) = opt {
self.stack[opt.as_ref().borrow().level as usize] = Some(opt.clone());
}
self
}
fn with_task(
self,
task: Rc<RefCell<STask>>,
) -> OptStackBuilder<
opt_stack_builder::SetTask<opt_stack_builder::SetRole<opt_stack_builder::SetRoles<S>>>,
>
where
<S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
<S as opt_stack_builder::State>::Role: opt_stack_builder::IsUnset,
<S as opt_stack_builder::State>::Task: opt_stack_builder::IsUnset,
{
self.with_role(
task.as_ref()
.borrow()
._role
.as_ref()
.unwrap()
.upgrade()
.unwrap(),
)
.task(task.to_owned())
.opt(task.as_ref().borrow().options.to_owned())
}
fn with_role(
self,
role: Rc<RefCell<SRole>>,
) -> OptStackBuilder<opt_stack_builder::SetRole<opt_stack_builder::SetRoles<S>>>
where
<S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
<S as opt_stack_builder::State>::Role: opt_stack_builder::IsUnset,
{
self.with_roles(
role.as_ref()
.borrow()
._config
.as_ref()
.unwrap()
.upgrade()
.unwrap(),
)
.role(role.to_owned())
.opt(role.as_ref().borrow().options.to_owned())
}
fn with_roles(
self,
roles: Rc<RefCell<SConfig>>,
) -> OptStackBuilder<opt_stack_builder::SetRoles<S>>
where
<S as opt_stack_builder::State>::Roles: opt_stack_builder::IsUnset,
{
self.with_default()
.roles(roles.to_owned())
.opt(roles.as_ref().borrow().options.to_owned())
}
fn with_default(self) -> Self {
self.opt(Some(rc_refcell!(Opt::level_default())))
}
}
#[bon]
impl OptStack {
#[builder]
pub fn new(
#[builder(field)] stack: [Option<Rc<RefCell<Opt>>>; 5],
roles: Option<Rc<RefCell<SConfig>>>,
role: Option<Rc<RefCell<SRole>>>,
task: Option<Rc<RefCell<STask>>>,
) -> Self {
OptStack {
stack,
roles,
role,
task,
}
}
pub fn from_task(task: Rc<RefCell<STask>>) -> Self {
OptStack::builder().with_task(task).build()
}
pub fn from_role(role: Rc<RefCell<SRole>>) -> Self {
OptStack::builder().with_role(role).build()
}
pub fn from_roles(roles: Rc<RefCell<SConfig>>) -> Self {
OptStack::builder().with_roles(roles).build()
}
fn find_in_options<F: Fn(&Opt) -> Option<(Level, V)>, V>(&self, f: F) -> Option<(Level, V)> {
for opt in self.stack.iter().rev() {
if let Some(opt) = opt.to_owned() {
let res = f(&opt.as_ref().borrow());
if res.is_some() {
debug!("res: {:?}", res.as_ref().unwrap().0);
return res;
}
}
}
None
}
fn iter_in_options<F: FnMut(&Opt)>(&self, mut f: F) {
for opt in self.stack.iter() {
if let Some(opt) = opt.to_owned() {
f(&opt.as_ref().borrow());
}
}
}
fn get_final_path(&self) -> SPathOptions {
let mut final_behavior = PathBehavior::Delete;
let default = LinkedHashSet::new();
let final_add = rc_refcell!(LinkedHashSet::new());
let final_sub = rc_refcell!(LinkedHashSet::new());
self.iter_in_options(|opt| {
let final_add_clone = Rc::clone(&final_add);
let final_sub_clone = Rc::clone(&final_sub);
if let Some(p) = opt.path.borrow().as_ref() {
match p.default_behavior {
PathBehavior::KeepSafe | PathBehavior::KeepUnsafe | PathBehavior::Delete => {
if let Some(add) = p.add.as_ref() {
final_add_clone.as_ref().replace(add.clone());
}
if let Some(sub) = p.sub.as_ref() {
final_sub_clone.as_ref().replace(sub.clone());
}
}
PathBehavior::Inherit => {
if final_behavior.is_delete() {
let union: LinkedHashSet<String> = final_add_clone
.as_ref()
.borrow()
.union(p.add.as_ref().unwrap_or(&default))
.filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e))
.cloned()
.collect();
final_add_clone.as_ref().borrow_mut().extend(union);
debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow());
} else {
let union: LinkedHashSet<String> = final_sub_clone
.as_ref()
.borrow()
.union(p.sub.as_ref().unwrap_or(&default))
.filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e))
.cloned()
.collect();
final_sub_clone.as_ref().borrow_mut().extend(union);
}
}
}
if !p.default_behavior.is_inherit() {
final_behavior = p.default_behavior;
}
}
});
SPathOptions::builder(final_behavior)
.add(
final_add
.clone()
.as_ref()
.borrow()
.iter()
.collect::<Vec<_>>()
.as_slice(),
)
.sub(
final_sub
.clone()
.as_ref()
.borrow()
.iter()
.collect::<Vec<_>>()
.as_slice(),
)
.build()
}
fn get_final_env(&self, cmd_filter: Option<FilterMatcher>) -> SEnvOptions {
let mut final_behavior = EnvBehavior::default();
let mut final_set = HashMap::new();
let mut final_keep = LinkedHashSet::new();
let mut final_check = LinkedHashSet::new();
let mut final_delete = LinkedHashSet::new();
let overriden_behavior = cmd_filter.as_ref().and_then(|f| f.env_behavior);
self.iter_in_options(|opt| {
if let Some(p) = opt.env.borrow().as_ref() {
final_behavior = match p.default_behavior {
EnvBehavior::Delete | EnvBehavior::Keep => {
final_keep = p
.keep
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.iter()
.filter(|e| {
!p.check.env_matches(e) || !p.delete.env_matches(e)
})
.cloned()
.collect();
final_check = p
.check
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.iter()
.filter(|e| {
!p.delete.env_matches(e)
})
.cloned()
.collect();
final_delete = p
.delete
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.iter()
.filter(|e| {
!p.check.env_matches(e)
})
.cloned()
.collect();
if let Some(set) = &p.set {
final_set = set.clone();
}
debug!("check: {:?}", final_check);
p.default_behavior
}
EnvBehavior::Inherit => {
final_keep = final_keep
.union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new()))
.cloned()
.collect();
final_check = final_check
.union(p.check.as_ref().unwrap_or(&LinkedHashSet::new()))
.cloned()
.collect();
final_delete = final_delete
.union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new()))
.cloned()
.collect();
if let Some(set) = &p.set {
final_set.extend(set.clone());
}
debug!("check: {:?}", final_check);
final_behavior
}
};
}
});
SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior))
.set(final_set)
.keep(final_keep)
.unwrap()
.check(final_check)
.unwrap()
.delete(final_delete)
.unwrap()
.build()
}
fn get_level(&self) -> Level {
let (level, _) = self
.find_in_options(|opt| Some((opt.level, ())))
.unwrap_or((Level::None, ()));
level
}
pub fn to_opt(&self) -> Rc<RefCell<Opt>> {
rc_refcell!(Opt::builder(self.get_level())
.path(self.get_final_path())
.env(self.get_final_env(None))
.maybe_root(
self.find_in_options(|opt| opt.root.map(|root| (opt.level, root)))
.map(|(_, root)| root),
)
.maybe_bounding(
self.find_in_options(|opt| opt.bounding.map(|bounding| (opt.level, bounding)))
.map(|(_, bounding)| bounding),
)
.maybe_authentication(
self.find_in_options(|opt| {
opt.authentication
.map(|authentication| (opt.level, authentication))
})
.map(|(_, authentication)| authentication),
)
.maybe_timeout(
self.find_in_options(|opt| opt.timeout.clone().map(|timeout| (opt.level, timeout)))
.map(|(_, timeout)| timeout),
)
.build())
}
}
#[cfg(test)]
mod tests {
use serde_test::assert_de_tokens;
use serde_test::assert_de_tokens_error;
use serde_test::assert_tokens;
use serde_test::Token;
use super::super::options::*;
use super::super::structs::*;
fn env_key_set_equal<I, J>(a: I, b: J) -> bool
where
I: IntoIterator<Item = EnvKey>,
J: IntoIterator<Item = EnvKey>,
{
let mut a_vec: Vec<_> = a.into_iter().collect();
let mut b_vec: Vec<_> = b.into_iter().collect();
a_vec.sort_by(|a, b| a.value.cmp(&b.value));
b_vec.sort_by(|a, b| a.value.cmp(&b.value));
a_vec == b_vec
}
fn hashset_vec_equal<I, J>(a: I, b: J) -> bool
where
I: IntoIterator,
I::Item: Into<String>,
J: IntoIterator,
J::Item: Into<String>,
{
let mut a_vec: Vec<String> = a.into_iter().map(Into::into).collect();
let mut b_vec: Vec<String> = b.into_iter().map(Into::into).collect();
a_vec.sort();
b_vec.sort();
a_vec == b_vec
}
#[test]
fn test_find_in_options() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Inherit)
.add(["path2"])
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Delete)
.add(["path1"])
.build(),
)
.build()
})
.build();
let options = OptStack::from_role(config.as_ref().borrow().roles[0].clone());
let res: Option<(Level, SPathOptions)> =
options.find_in_options(|opt| opt.path.clone().map(|value| (opt.level, value)));
assert_eq!(
res,
Some((
Level::Role,
SPathOptions::builder(PathBehavior::Inherit)
.add(["path2"])
.build()
))
);
}
#[test]
fn test_env_global_to_task() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.task(
STask::builder(1)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Delete)
.keep(["env1"])
.unwrap()
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Delete)
.keep(["env2"])
.unwrap()
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Delete)
.keep(["env3"])
.unwrap()
.build(),
)
.build()
})
.build();
let binding = OptStack::from_task(config.task("test", 1).unwrap()).to_opt();
let options = binding.as_ref().borrow();
let res = &options.env.as_ref().unwrap().keep;
assert!(res
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.contains(&EnvKey::from("env1")));
}
#[test]
fn test_to_opt() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.task(
STask::builder(1)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Inherit)
.add(["path3"])
.build(),
)
.env(
SEnvOptions::builder(EnvBehavior::Inherit)
.keep(["env3"])
.unwrap()
.build(),
)
.root(SPrivileged::User)
.bounding(SBounding::Strict)
.authentication(SAuthentication::Perform)
.timeout(
STimeout::builder()
.type_field(TimestampType::TTY)
.duration(Duration::minutes(3))
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Inherit)
.add(["path2"])
.build(),
)
.env(
SEnvOptions::builder(EnvBehavior::Delete)
.keep(["env1"])
.unwrap()
.build(),
)
.root(SPrivileged::Privileged)
.bounding(SBounding::Strict)
.authentication(SAuthentication::Skip)
.timeout(
STimeout::builder()
.type_field(TimestampType::PPID)
.duration(Duration::minutes(2))
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Delete)
.add(["path1"])
.build(),
)
.env(
SEnvOptions::builder(EnvBehavior::Delete)
.keep(["env2"])
.unwrap()
.build(),
)
.root(SPrivileged::Privileged)
.bounding(SBounding::Ignore)
.authentication(SAuthentication::Perform)
.timeout(
STimeout::builder()
.type_field(TimestampType::TTY)
.duration(Duration::minutes(1))
.build(),
)
.build()
})
.build();
let default = LinkedHashSet::new();
let stack = OptStack::from_roles(config.clone());
let opt = stack.to_opt();
let global_options = opt.as_ref().borrow();
assert_eq!(
global_options.path.as_ref().unwrap().default_behavior,
PathBehavior::Delete
);
assert!(hashset_vec_equal(
global_options
.path
.as_ref()
.unwrap()
.add
.as_ref()
.unwrap_or(&default)
.clone(),
vec!["path1"]
));
assert_eq!(
global_options.env.as_ref().unwrap().default_behavior,
EnvBehavior::Delete
);
assert!(env_key_set_equal(
global_options
.env
.as_ref()
.unwrap()
.keep
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.clone(),
vec![EnvKey::from("env2")]
));
assert_eq!(
global_options
.env
.as_ref()
.unwrap()
.keep
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.iter()
.map(|e| e.clone().into())
.collect::<Vec<String>>(),
vec!["env2".to_string()]
);
assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged);
assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore);
assert_eq!(
global_options.authentication.unwrap(),
SAuthentication::Perform
);
assert_eq!(
global_options.timeout.as_ref().unwrap().duration.unwrap(),
Duration::minutes(1)
);
assert_eq!(
global_options.timeout.as_ref().unwrap().type_field.unwrap(),
TimestampType::TTY
);
let opt = OptStack::from_role(config.clone().role("test").unwrap()).to_opt();
let role_options = opt.as_ref().borrow();
assert_eq!(
role_options.path.as_ref().unwrap().default_behavior,
PathBehavior::Delete
);
assert!(hashset_vec_equal(
role_options
.path
.as_ref()
.unwrap()
.add
.as_ref()
.unwrap_or(&default)
.clone(),
vec!["path1", "path2"]
));
assert_eq!(
role_options.env.as_ref().unwrap().default_behavior,
EnvBehavior::Delete
);
assert!(env_key_set_equal(
role_options
.env
.as_ref()
.unwrap()
.keep
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.clone(),
vec![EnvKey::from("env1")]
));
assert_eq!(role_options.root.unwrap(), SPrivileged::Privileged);
assert_eq!(role_options.bounding.unwrap(), SBounding::Strict);
assert_eq!(role_options.authentication.unwrap(), SAuthentication::Skip);
assert_eq!(
role_options.timeout.as_ref().unwrap().duration.unwrap(),
Duration::minutes(2)
);
assert_eq!(
role_options.timeout.as_ref().unwrap().type_field.unwrap(),
TimestampType::PPID
);
let opt = OptStack::from_task(config.task("test", 1).unwrap()).to_opt();
let task_options = opt.as_ref().borrow();
assert_eq!(
task_options.path.as_ref().unwrap().default_behavior,
PathBehavior::Delete
);
assert!(hashset_vec_equal(
task_options
.path
.as_ref()
.unwrap()
.add
.as_ref()
.unwrap_or(&default)
.clone(),
vec!["path1", "path2", "path3"]
));
assert_eq!(
task_options.env.as_ref().unwrap().default_behavior,
EnvBehavior::Delete
);
assert!(env_key_set_equal(
task_options
.env
.as_ref()
.unwrap()
.keep
.as_ref()
.unwrap_or(&LinkedHashSet::new())
.clone(),
vec![EnvKey::from("env1"), EnvKey::from("env3")]
));
assert_eq!(task_options.root.unwrap(), SPrivileged::User);
assert_eq!(task_options.bounding.unwrap(), SBounding::Strict);
assert_eq!(
task_options.authentication.unwrap(),
SAuthentication::Perform
);
assert_eq!(
task_options.timeout.as_ref().unwrap().duration.unwrap(),
Duration::minutes(3)
);
assert_eq!(
task_options.timeout.as_ref().unwrap().type_field.unwrap(),
TimestampType::TTY
);
}
#[test]
fn is_wildcard_env_key() {
assert!(!is_valid_env_name("TEST_.*"));
assert!(!is_valid_env_name("123"));
assert!(!is_valid_env_name(""));
#[cfg(feature = "pcre2")]
assert!(is_regex("TEST_.*"));
#[cfg(not(feature = "pcre2"))]
assert!(!is_regex("TEST_.*"));
}
#[test]
fn test_get_final_env_set_inherit() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.task(
STask::builder(1)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Inherit)
.set([("env1", "value3")])
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Inherit)
.set([("env2", "value2")])
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.env(
SEnvOptions::builder(EnvBehavior::Delete)
.set([("env1", "value1")])
.build(),
)
.build()
})
.build();
let stack = OptStack::from_task(config.task("test", 1).unwrap());
let opt = stack.to_opt();
let options = opt.as_ref().borrow();
assert_eq!(
options
.env
.as_ref()
.unwrap()
.set
.as_ref()
.unwrap_or(&HashMap::new())
.get("env1")
.unwrap(),
"value3"
);
}
#[test]
fn test_get_final_path_inherit() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.task(
STask::builder(1)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Inherit)
.sub(["/path3"])
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::Inherit)
.sub(["/path2"])
.build(),
)
.build()
})
.build(),
)
.options(|opt| {
opt.path(
SPathOptions::builder(PathBehavior::KeepSafe)
.sub(["/path1"])
.build(),
)
.build()
})
.build();
let stack = OptStack::from_task(config.task("test", 1).unwrap());
let opt = stack.to_opt();
let options = opt.as_ref().borrow();
assert!(options
.path
.as_ref()
.unwrap()
.sub
.as_ref()
.unwrap()
.contains("/path1"));
assert!(options
.path
.as_ref()
.unwrap()
.sub
.as_ref()
.unwrap()
.contains("/path2"));
assert!(options
.path
.as_ref()
.unwrap()
.sub
.as_ref()
.unwrap()
.contains("/path3"));
}
#[test]
fn test_find_in_options_none() {
let config = SConfig::builder()
.role(
SRole::builder("test")
.task(STask::builder(1).build())
.build(),
)
.build();
let stack = OptStack::from_task(config.task("test", 1).unwrap());
let res: Option<(Level, SPathOptions)> = stack.find_in_options(|_| None);
assert_eq!(res, None);
}
#[test]
fn test_invalid_envkey() {
let invalid_env = "3TE(ST_a";
let env_key = EnvKey::new(invalid_env.to_string());
assert!(env_key.is_err());
assert_eq!(
env_key.unwrap_err(),
format!(
"env key {}, must be a valid env, or a valid regex",
invalid_env
)
);
}
#[test]
fn test_sumask_from_u16() {
let umask = SUMask::from(0o755);
assert_eq!(umask.0, 0o755);
}
#[test]
fn test_u16_from_sumask() {
let umask = SUMask(0o644);
let value: u16 = umask.into();
assert_eq!(value, 0o644);
}
#[test]
fn test_mode_from_sumask() {
let umask = SUMask(0o22);
let mode: Mode = umask.into();
assert_eq!(mode, Mode::from_bits_truncate(0o22));
}
#[test]
fn test_sumask_serde_standard_umask() {
let umask = SUMask(0o22);
assert_tokens(&umask, &[Token::Str("022")]);
}
#[test]
fn test_sumask_serde_three_digits() {
let umask = SUMask(0o755);
assert_tokens(&umask, &[Token::Str("755")]);
}
#[test]
fn test_sumask_serde_single_digit() {
let umask = SUMask(0o7);
assert_tokens(&umask, &[Token::Str("007")]);
}
#[test]
fn test_sumask_serde_zero() {
let umask = SUMask(0);
assert_tokens(&umask, &[Token::Str("000")]);
}
#[test]
fn test_sumask_serde_max_value() {
let umask = SUMask(0o777);
assert_tokens(&umask, &[Token::Str("777")]);
}
#[test]
fn test_sumask_deserialize_various_formats() {
assert_de_tokens(&SUMask(0o7), &[Token::Str("7")]);
assert_de_tokens(&SUMask(0o22), &[Token::Str("22")]);
assert_de_tokens(&SUMask(0o022), &[Token::Str("022")]);
assert_de_tokens(&SUMask(0o1755), &[Token::Str("1755")]);
}
#[test]
fn test_sumask_deserialize_invalid_octal() {
assert_de_tokens_error::<SUMask>(&[Token::Str("888")], "invalid digit found in string");
assert_de_tokens_error::<SUMask>(&[Token::Str("729")], "invalid digit found in string");
}
#[test]
fn test_sumask_deserialize_invalid_format() {
assert_de_tokens_error::<SUMask>(&[Token::Str("abc")], "invalid digit found in string");
assert_de_tokens_error::<SUMask>(
&[Token::Str("")],
"cannot parse integer from empty string",
);
assert_de_tokens_error::<SUMask>(&[Token::Str(" 22")], "invalid digit found in string");
assert_de_tokens_error::<SUMask>(&[Token::Str("0x22")], "invalid digit found in string");
}
#[test]
fn test_sumask_partial_eq() {
let umask1 = SUMask(0o22);
let umask2 = SUMask(0o22);
let umask3 = SUMask(0o755);
assert_eq!(umask1, umask2);
assert_ne!(umask1, umask3);
}
#[test]
fn test_sumask_debug() {
let umask = SUMask(0o22);
let debug_str = format!("{:?}", umask);
assert_eq!(debug_str, "SUMask(18)"); }
#[test]
fn test_sumask_copy_clone() {
let umask1 = SUMask(0o644);
let umask2 = umask1; let umask3 = umask1.clone();
assert_eq!(umask1, umask2);
assert_eq!(umask1, umask3);
}
#[test]
fn test_sumask_common_umask_values() {
assert_tokens(&SUMask(0o022), &[Token::Str("022")]); assert_tokens(&SUMask(0o002), &[Token::Str("002")]); assert_tokens(&SUMask(0o077), &[Token::Str("077")]); assert_tokens(&SUMask(0o000), &[Token::Str("000")]); assert_tokens(&SUMask(0o027), &[Token::Str("027")]); }
#[test]
fn test_sumask_transparent_serde() {
let umask = SUMask(0o644);
assert_tokens(&umask, &[Token::Str("644")]);
}
}