use super::pattern::Pattern;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Case {
Sensitive,
AsciiInsensitive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Source<'a> {
Global,
Info,
Directory(&'a [u8]),
Command,
}
#[derive(Clone, Copy, Debug)]
pub struct Limits {
pub bytes: usize,
pub sources: usize,
pub patterns: usize,
pub line_bytes: usize,
pub path_bytes: usize,
pub work: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
bytes: 8 * 1024 * 1024,
sources: 4096,
patterns: 100_000,
line_bytes: 4096,
path_bytes: 4096,
work: 100_000_000,
}
}
}
impl Limits {
pub const fn trusted() -> Self {
Self {
bytes: usize::MAX / 4,
sources: usize::MAX / 4,
patterns: usize::MAX / 4,
line_bytes: usize::MAX / 4,
path_bytes: usize::MAX / 4,
work: usize::MAX / 4,
}
}
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
pub enum Error {
#[error("invalid repository-relative ignore path")]
Path,
#[error("ignore {0} limit exceeded")]
Limit(&'static str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Match {
pub ignored: bool,
pub source: usize,
pub line: usize,
pub matched_bytes: usize,
}
#[derive(Debug)]
pub struct Ignore {
case: Case,
limits: Limits,
sources: Vec<Rules>,
bytes: usize,
patterns: usize,
}
#[derive(Debug)]
struct Rules {
base: Vec<u8>,
rank: (u8, usize, usize),
patterns: Vec<(usize, Pattern)>,
}
impl Ignore {
pub fn new(case: Case, limits: Limits) -> Self {
Self {
case,
limits,
sources: Vec::new(),
bytes: 0,
patterns: 0,
}
}
pub fn add(&mut self, source: Source<'_>, content: &[u8]) -> Result<usize, Error> {
let (level, base) = match source {
Source::Global => (0, &b""[..]),
Source::Info => (1, &b""[..]),
Source::Directory(base) => (2, base),
Source::Command => (3, &b""[..]),
};
validate_path(base, true, self.limits.path_bytes)?;
let bytes = self
.bytes
.saturating_add(content.len())
.saturating_add(base.len());
if bytes > self.limits.bytes {
return Err(Error::Limit("source bytes"));
}
if self.sources.len() >= self.limits.sources {
return Err(Error::Limit("source count"));
}
let content = content.strip_prefix(b"\xef\xbb\xbf").unwrap_or(content);
let mut patterns = Vec::new();
for (line, raw) in content.split(|b| *b == b'\n').enumerate() {
if raw.len() > self.limits.line_bytes {
return Err(Error::Limit("line bytes"));
}
if let Some(pattern) = Pattern::parse(raw, self.case) {
if self.patterns.saturating_add(patterns.len()) >= self.limits.patterns {
return Err(Error::Limit("pattern count"));
}
patterns.push((line + 1, pattern));
}
}
let index = self.sources.len();
let depth = if base.is_empty() {
0
} else {
base.split(|b| *b == b'/').count()
};
self.patterns += patterns.len();
self.bytes = bytes;
self.sources.push(Rules {
base: base.to_vec(),
rank: (level, depth, index),
patterns,
});
self.sources.sort_by_key(|s| s.rank);
Ok(index)
}
pub fn check(&self, path: &[u8], is_directory: bool) -> Result<Option<Match>, Error> {
validate_path(path, false, self.limits.path_bytes)?;
let mut work = self.limits.work;
for end in path
.iter()
.enumerate()
.filter_map(|(i, b)| (*b == b'/').then_some(i))
{
let found = self.direct(&path[..end], true, &mut work)?;
if found.is_some_and(|m| m.ignored) {
return Ok(found);
}
}
self.direct(path, is_directory, &mut work)
}
pub fn check_direct(&self, path: &[u8], is_directory: bool) -> Result<Option<Match>, Error> {
validate_path(path, false, self.limits.path_bytes)?;
let mut work = self.limits.work;
self.direct(path, is_directory, &mut work)
}
fn direct(
&self,
path: &[u8],
directory: bool,
work: &mut usize,
) -> Result<Option<Match>, Error> {
for source in self.sources.iter().rev() {
charge(work, source.base.len().saturating_add(1))?;
let Some(relative) = relative(path, &source.base, self.case) else {
continue;
};
for (line, pattern) in source.patterns.iter().rev() {
charge(work, 1)?;
if pattern.matches(relative, directory, work)? {
return Ok(Some(Match {
ignored: !pattern.negative,
source: source.rank.2,
line: *line,
matched_bytes: path.len(),
}));
}
}
}
Ok(None)
}
}
pub(super) fn charge(work: &mut usize, cost: usize) -> Result<(), Error> {
*work = work
.checked_sub(cost)
.ok_or(Error::Limit("matching work"))?;
Ok(())
}
fn validate_path(path: &[u8], root: bool, limit: usize) -> Result<(), Error> {
if path.len() > limit {
return Err(Error::Limit("path bytes"));
}
if path.is_empty() && root {
return Ok(());
}
if path.contains(&0)
|| path
.split(|b| *b == b'/')
.any(|p| p.is_empty() || p == b"." || p == b"..")
{
return Err(Error::Path);
}
Ok(())
}
fn relative<'a>(path: &'a [u8], base: &[u8], case: Case) -> Option<&'a [u8]> {
if base.is_empty() {
return Some(path);
}
let prefix = path.get(..base.len())?;
let equal = match case {
Case::Sensitive => prefix == base,
Case::AsciiInsensitive => prefix.eq_ignore_ascii_case(base),
};
(equal && path.get(base.len()) == Some(&b'/')).then(|| &path[base.len() + 1..])
}