use devboy_secret_patterns::Catalogue;
use crate::index::IndexEntry;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatCheck {
NoRule,
Ok {
source: FormatRuleSource,
},
Mismatch {
source: FormatRuleSource,
expected: String,
},
Error {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatRuleSource {
Inline,
PatternId(String),
}
pub fn validate_format(entry: &IndexEntry, value: &str, catalogue: &Catalogue) -> FormatCheck {
if let Some(pattern) = entry.format_regex.as_deref() {
let re = match regex::Regex::new(pattern) {
Ok(r) => r,
Err(e) => {
return FormatCheck::Error {
message: format!("invalid format_regex `{pattern}`: {e}"),
};
}
};
return if re.is_match(value) {
FormatCheck::Ok {
source: FormatRuleSource::Inline,
}
} else {
FormatCheck::Mismatch {
source: FormatRuleSource::Inline,
expected: pattern.to_owned(),
}
};
}
if let Some(id) = entry.pattern_id.as_deref() {
let pattern = match catalogue.find(id) {
Some(p) => p,
None => {
return FormatCheck::NoRule;
}
};
let re = pattern.format_regex();
return if re.is_match(value) {
FormatCheck::Ok {
source: FormatRuleSource::PatternId(id.to_owned()),
}
} else {
FormatCheck::Mismatch {
source: FormatRuleSource::PatternId(id.to_owned()),
expected: re.as_str().to_owned(),
}
};
}
FormatCheck::NoRule
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::IndexEntry;
use devboy_secret_patterns::Catalogue;
fn empty_catalogue() -> Catalogue {
Catalogue::builtins_only()
}
fn entry_with_inline(regex: &str) -> IndexEntry {
IndexEntry {
format_regex: Some(regex.to_owned()),
..IndexEntry::default()
}
}
fn entry_with_pattern_id(id: &str) -> IndexEntry {
IndexEntry {
pattern_id: Some(id.to_owned()),
..IndexEntry::default()
}
}
#[test]
fn no_rule_when_neither_field_is_set() {
let entry = IndexEntry::default();
let r = validate_format(&entry, "anything", &empty_catalogue());
assert!(matches!(r, FormatCheck::NoRule));
}
#[test]
fn inline_regex_matching_value_returns_ok_with_inline_source() {
let entry = entry_with_inline(r"^ghp_[A-Za-z0-9]{36}$");
let value = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
let r = validate_format(&entry, value, &empty_catalogue());
match r {
FormatCheck::Ok { source } => assert_eq!(source, FormatRuleSource::Inline),
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn inline_regex_mismatching_value_returns_mismatch_with_pattern_echoed() {
let entry = entry_with_inline(r"^ghp_[A-Za-z0-9]{36}$");
let r = validate_format(&entry, "not-a-token", &empty_catalogue());
match r {
FormatCheck::Mismatch { source, expected } => {
assert_eq!(source, FormatRuleSource::Inline);
assert_eq!(expected, r"^ghp_[A-Za-z0-9]{36}$");
}
other => panic!("expected Mismatch, got {other:?}"),
}
}
#[test]
fn invalid_inline_regex_surfaces_compile_error() {
let entry = entry_with_inline("[unterminated");
let r = validate_format(&entry, "anything", &empty_catalogue());
match r {
FormatCheck::Error { message } => {
assert!(message.contains("invalid format_regex"));
assert!(message.contains("[unterminated"));
}
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn known_pattern_id_matches_a_real_token() {
let entry = entry_with_pattern_id("github-pat");
let value = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
let r = validate_format(&entry, value, &empty_catalogue());
match r {
FormatCheck::Ok { source } => {
assert_eq!(source, FormatRuleSource::PatternId("github-pat".into()));
}
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn known_pattern_id_rejects_gibberish() {
let entry = entry_with_pattern_id("github-pat");
let r = validate_format(&entry, "definitely-not-a-token", &empty_catalogue());
match r {
FormatCheck::Mismatch { source, .. } => {
assert_eq!(source, FormatRuleSource::PatternId("github-pat".into()));
}
other => panic!("expected Mismatch, got {other:?}"),
}
}
#[test]
fn unknown_pattern_id_treated_as_no_rule() {
let entry = entry_with_pattern_id("not-a-real-pattern-id");
let r = validate_format(&entry, "anything", &empty_catalogue());
assert!(matches!(r, FormatCheck::NoRule));
}
#[test]
fn inline_format_regex_wins_over_pattern_id() {
let mut entry = entry_with_pattern_id("github-pat");
entry.format_regex = Some(r"^tighter-prefix-[a-z]+$".to_owned());
let r = validate_format(&entry, "tighter-prefix-abc", &empty_catalogue());
match r {
FormatCheck::Ok { source } => assert_eq!(source, FormatRuleSource::Inline),
other => panic!("expected Inline Ok, got {other:?}"),
}
let r = validate_format(
&entry,
"ghp_abcdefghijklmnopqrstuvwxyz0123456789",
&empty_catalogue(),
);
assert!(matches!(
r,
FormatCheck::Mismatch {
source: FormatRuleSource::Inline,
..
}
));
}
#[test]
fn user_pattern_via_catalogue_is_used() {
let entry = entry_with_pattern_id("user-defined-not-loaded");
let r = validate_format(&entry, "x", &empty_catalogue());
assert!(matches!(r, FormatCheck::NoRule));
}
}