use crate::bash_danger_census::CensusVerdict;
use crate::bash_danger_removal::OperandVerdict;
pub use crate::bash_danger_shape::Branch;
pub const TAIL_POSSIBLY_EMPTY: &str = "on possibly-empty variable path";
pub const NOTE_NEEDS_FS: &str = "removal target needs the filesystem state at the time";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Generation {
Gen1,
Gen2,
Gen3,
}
impl Generation {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Generation::Gen1 => "gen1",
Generation::Gen2 => "gen2",
Generation::Gen3 => "gen3",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Ask,
Passthrough,
Unresolved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Checker {
Lexical,
Structured,
Census,
}
impl Checker {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Checker::Lexical => "lexical",
Checker::Structured => "structured",
Checker::Census => "census",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
pub decision: Decision,
pub immune: bool,
pub checker: Checker,
pub reason: String,
pub generation: Generation,
pub generation_assumed: bool,
pub branch: Branch,
}
impl Verdict {
#[must_use]
pub fn blocks(&self) -> bool {
self.decision == Decision::Ask && self.immune
}
#[must_use]
pub fn disclosure(&self) -> String {
let assumed = if self.generation_assumed {
" assumed"
} else {
""
};
format!(
"path: {} · checker: {} · {}{}",
self.branch.label(),
self.checker.label(),
self.generation.label(),
assumed
)
}
}
#[must_use]
pub fn classify(command: &str, version: Option<&str>) -> Verdict {
let (generation, generation_assumed) = generation_of(version);
let branch = crate::bash_danger_shape::branch_of(command);
let build = |decision, checker, reason: String| Verdict {
decision,
immune: decision == Decision::Ask,
checker,
reason,
generation,
generation_assumed,
branch,
};
match branch {
Branch::Structured => match crate::bash_danger_removal::structured(command) {
OperandVerdict::Ask { tail, target } => build(
Decision::Ask,
Checker::Structured,
format!("{tail}: {target}"),
),
OperandVerdict::NeedsFs => build(
Decision::Unresolved,
Checker::Structured,
NOTE_NEEDS_FS.to_string(),
),
OperandVerdict::Clear => {
build(Decision::Passthrough, Checker::Structured, String::new())
}
},
Branch::Lexical(_) => {
if let Some((verb, target)) = lexical_hit(command, generation) {
let _ = verb;
return build(
Decision::Ask,
Checker::Lexical,
format!("{TAIL_POSSIBLY_EMPTY}: {target}"),
);
}
if generation == Generation::Gen1 {
return build(Decision::Passthrough, Checker::Lexical, String::new());
}
match crate::bash_danger_census::census(command, generation == Generation::Gen3) {
CensusVerdict::Bail { count } => build(
Decision::Ask,
Checker::Census,
format!("{} ({count})", crate::bash_danger_census::TAIL_TOO_MANY),
),
CensusVerdict::Lexical { verb, target } => {
let _ = verb;
build(
Decision::Ask,
Checker::Census,
format!(
"{}: {target}",
crate::bash_danger_census::TAIL_IN_SUBSTITUTION
),
)
}
CensusVerdict::Structured { tail, target } => {
build(Decision::Ask, Checker::Census, format!("{tail}: {target}"))
}
CensusVerdict::NeedsFs => build(
Decision::Unresolved,
Checker::Census,
NOTE_NEEDS_FS.to_string(),
),
CensusVerdict::Clear => {
build(Decision::Passthrough, Checker::Census, String::new())
}
}
}
}
}
fn lexical_hit(command: &str, generation: Generation) -> Option<(&'static str, String)> {
if generation == Generation::Gen3 {
crate::bash_danger_out::out(command).map(|h| (h.command, h.target))
} else {
crate::bash_danger_lexical::hnt(command).map(|h| (h.command, h.target))
}
}
const CENSUS_FLOOR: (u32, u32, u32) = (2, 1, 208);
const REWRITE_FLOOR: (u32, u32, u32) = (2, 1, 261);
#[must_use]
pub fn generation_of(version: Option<&str>) -> (Generation, bool) {
let Some(triple) = version.and_then(parse_triple) else {
return (Generation::Gen2, true);
};
if triple >= REWRITE_FLOOR {
(Generation::Gen3, false)
} else if triple >= CENSUS_FLOOR {
(Generation::Gen2, false)
} else {
(Generation::Gen1, false)
}
}
fn parse_triple(v: &str) -> Option<(u32, u32, u32)> {
let mut parts = v.split('.');
let major = leading_number(parts.next()?)?;
let minor = leading_number(parts.next()?)?;
let patch = leading_number(parts.next()?)?;
Some((major, minor, patch))
}
fn leading_number(s: &str) -> Option<u32> {
let digits: String = s.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
}
#[cfg(test)]
#[path = "bash_danger_tests.rs"]
mod tests;