use anyhow::Context;
use clap::{Args, ValueEnum};
use padlock_core::findings::{Finding, Report, Severity};
use padlock_core::ir::{StructLayout, find_padding};
use crate::config::Config;
#[derive(Clone, ValueEnum)]
pub enum FailSeverity {
High,
Medium,
Low,
}
impl FailSeverity {
pub fn matches(&self, sev: &Severity) -> bool {
match self {
FailSeverity::High => matches!(sev, Severity::High),
FailSeverity::Medium => matches!(sev, Severity::High | Severity::Medium),
FailSeverity::Low => true,
}
}
}
#[derive(Clone, ValueEnum, Default)]
pub enum SortBy {
#[default]
Score,
Size,
Waste,
Name,
}
#[derive(Args, Clone)]
pub struct FilterArgs {
#[arg(long, short = 'F', value_name = "PATTERN")]
pub filter: Option<String>,
#[arg(long, short = 'X', value_name = "PATTERN")]
pub exclude: Option<String>,
#[arg(long, value_name = "N")]
pub min_holes: Option<usize>,
#[arg(long, value_name = "N")]
pub min_size: Option<usize>,
#[arg(long)]
pub packable: bool,
#[arg(long, value_enum, default_value = "score", value_name = "FIELD")]
pub sort_by: SortBy,
#[arg(long)]
pub hide_repr_rust: bool,
}
impl FilterArgs {
pub fn apply_config_defaults(&mut self, cfg: &Config) {
if self.filter.is_none() {
self.filter = cfg.filter.clone();
}
if self.exclude.is_none() {
self.exclude = cfg.exclude.clone();
}
if self.min_size.is_none() {
self.min_size = cfg.min_size;
}
if self.min_holes.is_none() {
self.min_holes = cfg.min_holes;
}
if matches!(self.sort_by, SortBy::Score)
&& let Some(ref s) = cfg.sort_by
{
self.sort_by = match s.to_ascii_lowercase().as_str() {
"size" => SortBy::Size,
"waste" => SortBy::Waste,
"name" => SortBy::Name,
_ => SortBy::Score,
};
}
}
pub fn apply_to_layouts(&self, layouts: &mut Vec<StructLayout>) -> anyhow::Result<()> {
if let Some(ref pat) = self.filter {
let re = regex::Regex::new(pat)
.with_context(|| format!("invalid --filter pattern: {pat:?}"))?;
layouts.retain(|l| re.is_match(&l.name));
}
if let Some(ref pat) = self.exclude {
let re = regex::Regex::new(pat)
.with_context(|| format!("invalid --exclude pattern: {pat:?}"))?;
layouts.retain(|l| !re.is_match(&l.name));
}
if let Some(min_size) = self.min_size {
layouts.retain(|l| l.total_size >= min_size);
}
if let Some(min_holes) = self.min_holes {
layouts.retain(|l| find_padding(l).len() >= min_holes);
}
Ok(())
}
pub fn apply_to_report(&self, report: &mut Report) {
if self.hide_repr_rust {
report.structs.retain(|sr| !sr.is_repr_rust);
}
if self.packable {
report.structs.retain(|sr| {
sr.findings
.iter()
.any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
});
}
match self.sort_by {
SortBy::Score => report.structs.sort_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(std::cmp::Ordering::Equal)
}),
SortBy::Size => report
.structs
.sort_by(|a, b| b.total_size.cmp(&a.total_size)),
SortBy::Waste => report
.structs
.sort_by(|a, b| b.wasted_bytes.cmp(&a.wasted_bytes)),
SortBy::Name => report
.structs
.sort_by(|a, b| a.struct_name.cmp(&b.struct_name)),
}
report.total_structs = report.structs.len();
report.total_wasted_bytes = report.structs.iter().map(|s| s.wasted_bytes).sum();
}
}
#[cfg(test)]
mod tests {
use super::*;
use padlock_core::findings::Report;
use padlock_core::ir::test_fixtures::{connection_layout, packed_layout};
#[test]
fn fail_severity_high_only_matches_high() {
assert!(FailSeverity::High.matches(&Severity::High));
assert!(!FailSeverity::High.matches(&Severity::Medium));
assert!(!FailSeverity::High.matches(&Severity::Low));
}
#[test]
fn fail_severity_medium_matches_high_and_medium() {
assert!(FailSeverity::Medium.matches(&Severity::High));
assert!(FailSeverity::Medium.matches(&Severity::Medium));
assert!(!FailSeverity::Medium.matches(&Severity::Low));
}
#[test]
fn fail_severity_low_matches_all() {
assert!(FailSeverity::Low.matches(&Severity::High));
assert!(FailSeverity::Low.matches(&Severity::Medium));
assert!(FailSeverity::Low.matches(&Severity::Low));
}
fn args(
filter: Option<&str>,
exclude: Option<&str>,
min_holes: Option<usize>,
min_size: Option<usize>,
packable: bool,
sort_by: SortBy,
) -> FilterArgs {
FilterArgs {
filter: filter.map(str::to_owned),
exclude: exclude.map(str::to_owned),
min_holes,
min_size,
packable,
sort_by,
hide_repr_rust: false,
}
}
fn default_args() -> FilterArgs {
args(None, None, None, None, false, SortBy::Score)
}
#[test]
fn filter_keeps_matching_name() {
let mut layouts = vec![connection_layout(), packed_layout()];
args(Some("Connection"), None, None, None, false, SortBy::Score)
.apply_to_layouts(&mut layouts)
.unwrap();
assert_eq!(layouts.len(), 1);
assert_eq!(layouts[0].name, "Connection");
}
#[test]
fn filter_regex_works() {
let mut layouts = vec![connection_layout(), packed_layout()];
args(
Some("^(Connection|Packed)$"),
None,
None,
None,
false,
SortBy::Score,
)
.apply_to_layouts(&mut layouts)
.unwrap();
assert_eq!(layouts.len(), 2);
}
#[test]
fn exclude_removes_matching() {
let mut layouts = vec![connection_layout(), packed_layout()];
args(None, Some("Packed"), None, None, false, SortBy::Score)
.apply_to_layouts(&mut layouts)
.unwrap();
assert_eq!(layouts.len(), 1);
assert_eq!(layouts[0].name, "Connection");
}
#[test]
fn min_holes_removes_zero_hole_structs() {
let mut layouts = vec![connection_layout(), packed_layout()];
args(None, None, Some(1), None, false, SortBy::Score)
.apply_to_layouts(&mut layouts)
.unwrap();
assert_eq!(layouts.len(), 1);
assert_eq!(layouts[0].name, "Connection");
}
#[test]
fn min_size_removes_small_structs() {
let mut layouts = vec![connection_layout(), packed_layout()];
args(None, None, None, Some(16), false, SortBy::Score)
.apply_to_layouts(&mut layouts)
.unwrap();
assert_eq!(layouts.len(), 1);
assert_eq!(layouts[0].name, "Connection");
}
#[test]
fn packable_keeps_only_reorderable() {
let mut report = Report::from_layouts(&[connection_layout(), packed_layout()]);
args(None, None, None, None, true, SortBy::Score).apply_to_report(&mut report);
assert!(report.structs.iter().all(|sr| {
sr.findings
.iter()
.any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
}));
assert!(report.structs.iter().all(|sr| sr.struct_name != "Packed"));
}
#[test]
fn sort_by_name_is_alphabetical() {
let mut report = Report::from_layouts(&[connection_layout(), packed_layout()]);
args(None, None, None, None, false, SortBy::Name).apply_to_report(&mut report);
let names: Vec<&str> = report
.structs
.iter()
.map(|s| s.struct_name.as_str())
.collect();
let mut sorted = names.clone();
sorted.sort_unstable();
assert_eq!(names, sorted);
}
#[test]
fn sort_by_size_is_descending() {
let mut report = Report::from_layouts(&[packed_layout(), connection_layout()]);
args(None, None, None, None, false, SortBy::Size).apply_to_report(&mut report);
let sizes: Vec<usize> = report.structs.iter().map(|s| s.total_size).collect();
assert!(sizes.windows(2).all(|w| w[0] >= w[1]));
}
#[test]
fn sort_by_waste_is_descending() {
let mut report = Report::from_layouts(&[packed_layout(), connection_layout()]);
args(None, None, None, None, false, SortBy::Waste).apply_to_report(&mut report);
let waste: Vec<usize> = report.structs.iter().map(|s| s.wasted_bytes).collect();
assert!(waste.windows(2).all(|w| w[0] >= w[1]));
}
#[test]
fn report_counters_resynced_after_filter() {
let mut report = Report::from_layouts(&[connection_layout(), packed_layout()]);
assert_eq!(report.total_structs, 2);
args(None, None, None, None, true, SortBy::Score).apply_to_report(&mut report);
assert_eq!(report.total_structs, report.structs.len());
assert_eq!(
report.total_wasted_bytes,
report.structs.iter().map(|s| s.wasted_bytes).sum::<usize>()
);
}
#[test]
fn invalid_filter_regex_returns_error() {
let mut layouts = vec![connection_layout()];
let result = args(Some("[invalid"), None, None, None, false, SortBy::Score)
.apply_to_layouts(&mut layouts);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("--filter"));
}
#[test]
fn invalid_exclude_regex_returns_error() {
let mut layouts = vec![connection_layout()];
let result = args(None, Some("(unclosed"), None, None, false, SortBy::Score)
.apply_to_layouts(&mut layouts);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("--exclude"));
}
}