use std::collections::BTreeMap;
use regex::Regex;
use serde::Deserialize;
use crate::engines::actionlint::LintError;
#[derive(Debug, Clone)]
pub struct PathConfig {
pub pattern: String,
pub ignore: Vec<Regex>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub self_hosted_runner_labels: Vec<String>,
pub variables: Option<Vec<String>>,
pub paths: Vec<PathConfig>,
}
#[derive(Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct RawRunner {
#[serde(default)]
labels: Option<Vec<String>>,
}
#[derive(Deserialize, Default)]
struct RawPath {
#[serde(default)]
ignore: Option<Vec<String>>,
}
#[derive(Deserialize, Default)]
struct Raw {
#[serde(rename = "self-hosted-runner", default)]
self_hosted_runner: Option<RawRunner>,
#[serde(rename = "config-variables", default)]
config_variables: Option<Vec<String>>,
#[serde(default)]
paths: Option<BTreeMap<String, RawPath>>,
}
impl Config {
pub fn parse(text: &str) -> Result<Self, String> {
let raw: Raw =
serde_norway::from_str(text).map_err(|e| e.to_string().replace('\n', " "))?;
let mut paths = Vec::new();
for (pattern, cfg) in raw.paths.unwrap_or_default() {
let mut ignore = Vec::new();
for p in cfg.ignore.unwrap_or_default() {
let re = Regex::new(&p)
.map_err(|e| format!("invalid regular expression {p:?} in \"ignore\": {e}"))?;
ignore.push(re);
}
paths.push(PathConfig { pattern, ignore });
}
Ok(Self {
self_hosted_runner_labels: raw
.self_hosted_runner
.and_then(|r| r.labels)
.unwrap_or_default(),
variables: raw.config_variables,
paths,
})
}
pub fn path_configs(&self, path: &str) -> Vec<&PathConfig> {
self.paths
.iter()
.filter(|p| doublestar_match(&p.pattern, path))
.collect()
}
pub fn filter_errors(&self, path: &str, errors: Vec<LintError>) -> Vec<LintError> {
let cfgs = self.path_configs(path);
if cfgs.is_empty() {
return errors;
}
errors
.into_iter()
.filter(|e| {
!cfgs
.iter()
.any(|c| c.ignore.iter().any(|re| re.is_match(&e.message)))
})
.collect()
}
}
pub fn doublestar_match(pattern: &str, path: &str) -> bool {
let pattern_segments: Vec<&str> = pattern.split('/').collect();
let path_segments: Vec<&str> = path.split('/').collect();
match_segments(&pattern_segments, &path_segments)
}
fn match_segments(pattern_segments: &[&str], path_segments: &[&str]) -> bool {
match pattern_segments.split_first() {
None => path_segments.is_empty(),
Some((&"**", rest)) => {
(0..=path_segments.len()).any(|skip| match_segments(rest, &path_segments[skip..]))
}
Some((pat, rest)) => match path_segments.split_first() {
Some((segment, remaining)) => {
match_segment(pat, segment) && match_segments(rest, remaining)
}
None => false,
},
}
}
fn match_segment(pat: &str, s: &str) -> bool {
if let Some(open) = pat.find('{')
&& let Some(close) = pat[open..].find('}')
{
let close = open + close;
let (head, tail) = (&pat[..open], &pat[close + 1..]);
return pat[open + 1..close]
.split(',')
.any(|alt| match_segment(&format!("{head}{alt}{tail}"), s));
}
glob_match(
&pat.chars().collect::<Vec<_>>(),
&s.chars().collect::<Vec<_>>(),
)
}
pub fn glob_match(pat: &[char], s: &[char]) -> bool {
match pat.first() {
None => s.is_empty(),
Some('*') => (0..=s.len()).any(|i| glob_match(&pat[1..], &s[i..])),
Some('?') => !s.is_empty() && glob_match(&pat[1..], &s[1..]),
Some('[') => {
let Some(end) = pat.iter().skip(1).position(|c| *c == ']').map(|i| i + 1) else {
return false;
};
let Some(&c) = s.first() else {
return false;
};
let class = &pat[1..end];
let (negate, class) = match class.first() {
Some('!' | '^') => (true, &class[1..]),
_ => (false, class),
};
let mut matched = false;
let mut i = 0;
while i < class.len() {
if i + 2 < class.len() && class[i + 1] == '-' {
if class[i] <= c && c <= class[i + 2] {
matched = true;
}
i += 3;
} else {
if class[i] == c {
matched = true;
}
i += 1;
}
}
matched != negate && glob_match(&pat[end + 1..], &s[1..])
}
Some('\\') if pat.len() > 1 => s.first() == Some(&pat[1]) && glob_match(&pat[2..], &s[1..]),
Some(&p) => s.first() == Some(&p) && glob_match(&pat[1..], &s[1..]),
}
}