use std::{ io::Read, path::{Path, PathBuf}, }; use bstr::{BStr, BString, ByteSlice, ByteVec}; use crate::{pattern::Case, search::Pattern}; /// A list of patterns which optionally know where they were loaded from and what their base is. /// /// Knowing their base which is relative to a source directory, it will ignore all path to match against /// that don't also start with said base. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Default)] pub struct List { /// Patterns and their associated data in the order they were loaded in or specified, /// the line number in its source file or its sequence number (_`(pattern, value, line_number)`_). /// /// During matching, this order is reversed. pub patterns: Vec>, /// The path from which the patterns were read, or `None` if the patterns /// don't originate in a file on disk. pub source: Option, /// The parent directory of source, or `None` if the patterns are _global_ to match against the repository root. /// It's processed to contain slashes only and to end with a trailing slash, and is relative to the repository root. pub base: Option, } /// An association of a pattern with its value, along with a sequence number providing a sort order in relation to its peers. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub struct Mapping { /// The pattern itself, like `/target/*` pub pattern: crate::Pattern, /// The value associated with the pattern. pub value: T, /// Typically the line number in the file the pattern was parsed from. pub sequence_number: usize, } fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec) -> std::io::Result { buf.clear(); let file = if follow_symlinks { std::fs::File::open(path) } else { gix_features::fs::open_options_no_follow().read(true).open(path) }; Ok(match file { Ok(mut file) => { file.read_to_end(buf)?; true } Err(err) if err.kind() == std::io::ErrorKind::NotFound || // TODO: use the enum variant NotADirectory for this once stabilized err.raw_os_error() == Some(20) /* Not a directory */ => false, Err(err) => return Err(err), }) } /// Instantiation impl List where T: Pattern, { /// `source_file` is the location of the `bytes` which represents a list of patterns, one pattern per line. /// If `root` is `Some(…)` it's used to see `source_file` as relative to itself, if `source_file` is absolute. /// If source is relative and should be treated as base, set `root` to `Some("")`. pub fn from_bytes(bytes: &[u8], source_file: PathBuf, root: Option<&Path>) -> Self { let patterns = T::bytes_to_patterns(bytes, source_file.as_path()); let base = root .and_then(|root| source_file.parent().expect("file").strip_prefix(root).ok()) .and_then(|base| { (!base.as_os_str().is_empty()).then(|| { let mut base: BString = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(base)).into_owned(); base.push_byte(b'/'); base }) }); List { patterns, source: Some(source_file), base, } } /// Create a pattern list from the `source` file, which may be located underneath `root`, while optionally /// following symlinks with `follow_symlinks`, providing `buf` to temporarily store the data contained in the file. pub fn from_file( source: impl Into, root: Option<&Path>, follow_symlinks: bool, buf: &mut Vec, ) -> std::io::Result> { let source = source.into(); Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)?.then(|| Self::from_bytes(buf, source, root))) } } /// Utilities impl List where T: Pattern, { /// If this list is anchored to a base path, return `relative_path` as being relative to our base and return /// an updated `basename_pos` as well if it was set. /// `case` is respected for the comparison. /// /// This is useful to turn repository-relative paths into paths relative to a particular search base. pub fn strip_base_handle_recompute_basename_pos<'a>( &self, relative_path: &'a BStr, basename_pos: Option, case: Case, ) -> Option<(&'a BStr, Option)> { match self.base.as_deref() { Some(base) => strip_base_handle_recompute_basename_pos(base.as_bstr(), relative_path, basename_pos, case)?, None => (relative_path, basename_pos), } .into() } } /// Return`relative_path` as being relative to `base` along with an updated `basename_pos` if it was set. /// `case` is respected for the comparison. /// /// This is useful to turn repository-relative paths into paths relative to a particular search base. pub fn strip_base_handle_recompute_basename_pos<'a>( base: &BStr, relative_path: &'a BStr, basename_pos: Option, case: Case, ) -> Option<(&'a BStr, Option)> { Some(( match case { Case::Sensitive => relative_path.strip_prefix(base.as_bytes())?.as_bstr(), Case::Fold => { let rela_dir = relative_path.get(..base.len())?; if !rela_dir.eq_ignore_ascii_case(base) { return None; } &relative_path[base.len()..] } }, basename_pos.and_then(|pos| { let pos = pos - base.len(); (pos != 0).then_some(pos) }), )) }