use super::pattern::{self, Pattern, Spans};
use super::prefilter::Prefilter;
use super::{secrets, terms};
use crate::config::{Config, GuardDef};
use crate::workspace::exposure;
use crate::workspace::owners::{PathMatch, matcher};
use anyhow::Result;
use regex::Regex;
pub struct Compiled {
limited: bool,
paths: Vec<PathMatch>,
exclude: Vec<PathMatch>,
deny_files: Vec<PathMatch>,
terms: Option<Regex>,
pub patterns: Vec<Pattern>,
prefilter: Prefilter,
pub path_patterns: Vec<Pattern>,
allow: Vec<String>,
pub missing: Vec<String>,
pub published: Vec<String>,
pub bases: Vec<String>,
}
fn bases_of(limited: bool, patterns: &[String]) -> Vec<String> {
let bases: Vec<String> = patterns.iter().map(|p| crate::placement::literal_base(p)).collect();
if !limited || bases.iter().any(|b| b.is_empty()) {
return vec![];
}
bases
}
pub fn compile(cfg: &Config, g: &GuardDef) -> Result<Compiled> {
let compile_all = |rules: &[crate::config::RegexRule]| -> Result<Vec<Pattern>> {
rules.iter().map(|r| pattern::compile(&g.name, r.spec())).collect()
};
let mut paths: Vec<PathMatch> = g.paths.iter().filter_map(|p| matcher(p)).collect();
let mut scope_patterns = g.paths.clone();
let (mut missing, mut published) = (vec![], vec![]);
if g.public {
for e in exposure::scan(cfg).exposed {
match cfg.root.join(&e.path).exists() {
true => published.push(e.path.clone()),
false => missing.push(e.path.clone()),
}
paths.extend(matcher(&e.path));
scope_patterns.push(e.path);
}
}
let limited = !g.paths.is_empty() || g.public;
let patterns = compile_all(&g.deny_regex)?;
let sources: Vec<&str> = patterns.iter().map(|p| p.re.as_str()).collect();
Ok(Compiled {
bases: bases_of(limited, &scope_patterns),
limited,
paths,
exclude: g.exclude.iter().filter_map(|p| matcher(p)).collect(),
deny_files: g.deny_files.iter().filter_map(|p| matcher(p)).collect(),
terms: terms::matcher(&terms::load(cfg, g)?),
prefilter: Prefilter::new(&sources),
patterns,
path_patterns: compile_all(&g.deny_path_regex)?,
allow: g.allow.iter().map(|a| a.to_lowercase()).collect(),
missing,
published,
})
}
fn line_of(newlines: &[usize], at: usize) -> usize {
newlines.partition_point(|&n| n < at) + 1
}
impl Compiled {
fn allowed(&self, text: &str) -> bool {
let t = text.to_lowercase();
self.allow.iter().any(|a| t.contains(a.as_str()))
}
pub fn text_hits(&self, g: &GuardDef, path: &str, text: &str) -> Vec<(usize, String)> {
let newlines: Vec<usize> = text.match_indices('\n').map(|(i, _)| i).collect();
let mut out: Vec<(usize, String)> = vec![];
if let Some(t) = &self.terms {
for m in t.find_iter(text).filter(|m| !self.allowed(m.as_str())) {
out.push((line_of(&newlines, m.start()), format!("denied term {}", terms::mask(m.as_str()))));
}
}
let mut spans = Spans::default();
for i in self.prefilter.candidates(text) {
let p = &self.patterns[i];
if !p.applies_to(path) {
continue;
}
let lines = self.prefilter.lines(i, text).unwrap_or_else(|| vec![(0, text.len())]);
for (at, hit) in p.hits_in(path, text, &lines, &mut spans) {
if !self.allowed(hit) {
out.push((line_of(&newlines, at), format!("matches {}", p.label)));
}
}
}
if g.secrets {
for s in secrets::all() {
for m in s.re.find_iter(text).filter(|m| !self.allowed(m.as_str())) {
out.push((line_of(&newlines, m.start()), format!("looks like a {}", s.name)));
}
}
}
out.sort();
out.dedup();
out
}
pub fn path_hits(&self, path: &str) -> Vec<String> {
self.path_patterns
.iter()
.filter(|p| p.applies_to(path) && p.re.is_match(path).unwrap_or(false))
.map(|p| format!("path matches {}", p.label))
.collect()
}
pub fn in_scope(&self, path: &str) -> bool {
(!self.limited || self.paths.iter().any(|m| m.is_match(path))) && !self.exclude.iter().any(|m| m.is_match(path))
}
pub fn denies_file(&self, path: &str) -> bool {
self.deny_files.iter().any(|m| m.is_match(path))
}
pub fn terms_source(&self) -> &str {
self.terms.as_ref().map(|t| t.as_str()).unwrap_or("")
}
}