use super::{Policy, PolicyError, RuleGroup};
pub const TITLE_MAX_CHARS: usize = 80;
pub const WHY_MAX_CHARS: usize = 160;
pub(super) fn check(group: &RuleGroup) -> Result<(), PolicyError> {
let fields = [
("title", group.title.as_deref(), TITLE_MAX_CHARS),
("why", group.why.as_deref(), WHY_MAX_CHARS),
];
for (field, text, max) in fields {
let Some(text) = text else { continue };
let problem = if text.trim().is_empty() {
format!("`{field}` is empty")
} else if text.chars().any(char::is_control) {
format!("`{field}` must be one line without control characters")
} else if text.chars().count() > max {
format!("`{field}` is longer than {max} characters")
} else {
continue;
};
return Err(PolicyError::Rule {
rule: group.id.clone(),
problem,
});
}
Ok(())
}
impl Policy {
#[must_use]
pub fn own_group(&self, id: &str) -> Option<&RuleGroup> {
self.deny
.iter()
.chain(&self.allow)
.chain(&self.review)
.chain(&self.defer)
.find(|g| g.id == id)
}
#[must_use]
pub fn untitled(&self) -> Vec<&str> {
self.deny
.iter()
.chain(&self.review)
.filter(|g| g.title.is_none())
.map(|g| g.id.as_str())
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::{Policy, PolicyError};
fn parse(group: &str) -> Result<Policy, PolicyError> {
Policy::parse(&format!(
"version: 1\ndeny:\n - id: keys\n fs.read: ['~/.ssh/**']\n{group}"
))
}
#[test]
fn a_title_and_why_are_read_and_found_by_rule_id() {
let p =
parse(" title: reading your SSH private key\n why: it cannot be taken back\n")
.unwrap();
let group = p.own_group("keys").unwrap();
assert_eq!(group.title.as_deref(), Some("reading your SSH private key"));
assert_eq!(group.why.as_deref(), Some("it cannot be taken back"));
assert_eq!(p.untitled(), Vec::<&str>::new());
assert!(p.own_group("other").is_none());
}
#[test]
fn a_group_without_a_title_is_listed_and_still_valid() {
let p = parse("").unwrap();
assert_eq!(p.untitled(), ["keys"]);
assert_eq!(p.own_group("keys").unwrap().title, None);
}
#[test]
fn titles_and_whys_are_one_short_line() {
let long = "x".repeat(super::TITLE_MAX_CHARS + 1);
for (group, says) in [
(" title: \"a\\nb\"\n", "one line"),
(" why: \"a\\u001b[2Jb\"\n", "control characters"),
(" title: ' '\n", "`title` is empty"),
(&*format!(" title: {long}\n"), "longer than 80"),
] {
let error = parse(group).unwrap_err().to_string();
assert!(
error.contains("rule `keys`") && error.contains(says),
"{error}"
);
}
}
#[test]
fn repository_review_rules_are_not_own_groups() {
let mut p = parse("").unwrap();
p.repo_review.push(crate::RuleGroup {
id: "repo:x".into(),
title: Some("run this to fix it".into()),
shell: vec!["make".into()],
..Default::default()
});
assert!(p.own_group("repo:x").is_none());
}
}