gix_attributes/search/
attributes.rs

1use std::path::{Path, PathBuf};
2
3use bstr::{BStr, ByteSlice};
4use gix_glob::search::{pattern, Pattern};
5
6use super::Attributes;
7use crate::{
8    search::{Assignments, MetadataCollection, Outcome, TrackedAssignment, Value},
9    Search,
10};
11
12/// Instantiation and initialization.
13impl Search {
14    /// Create a search instance preloaded with *built-ins* followed by attribute `files` from various global locations.
15    ///
16    /// See [`Source`][crate::Source] for a way to obtain these paths.
17    ///
18    /// Note that parsing is lenient and errors are logged.
19    ///
20    /// * `buf` is used to read `files` from disk which will be ignored if they do not exist.
21    /// * `collection` will be updated with information necessary to perform lookups later.
22    pub fn new_globals(
23        files: impl IntoIterator<Item = impl Into<PathBuf>>,
24        buf: &mut Vec<u8>,
25        collection: &mut MetadataCollection,
26    ) -> std::io::Result<Self> {
27        let mut group = Self::default();
28        group.add_patterns_buffer(
29            b"[attr]binary -diff -merge -text",
30            "[builtin]".into(),
31            None,
32            collection,
33            true, /* allow macros */
34        );
35
36        for path in files.into_iter() {
37            group.add_patterns_file(path.into(), true, None, buf, collection, true /* allow macros */)?;
38        }
39        Ok(group)
40    }
41}
42
43/// Mutation
44impl Search {
45    /// Add the given file at `source` to our patterns if it exists, otherwise do nothing.
46    /// Update `collection` with newly added attribute names.
47    /// If a `root` is provided, it's not considered a global file anymore.
48    /// If `allow_macros` is `true`, macros will be processed like normal, otherwise they will be skipped entirely.
49    /// Returns `true` if the file was added, or `false` if it didn't exist.
50    pub fn add_patterns_file(
51        &mut self,
52        source: PathBuf,
53        follow_symlinks: bool,
54        root: Option<&Path>,
55        buf: &mut Vec<u8>,
56        collection: &mut MetadataCollection,
57        allow_macros: bool,
58    ) -> std::io::Result<bool> {
59        // TODO: should `Pattern` trait use an instance as first argument to carry this information
60        //       (so no `retain` later, it's slower than skipping)
61        let was_added = gix_glob::search::add_patterns_file(&mut self.patterns, source, follow_symlinks, root, buf)?;
62        if was_added {
63            let last = self.patterns.last_mut().expect("just added");
64            if !allow_macros {
65                last.patterns
66                    .retain(|p| !matches!(p.value, Value::MacroAssignments { .. }));
67            }
68            collection.update_from_list(last);
69        }
70        Ok(was_added)
71    }
72    /// Add patterns as parsed from `bytes`, providing their `source` path and possibly their `root` path, the path they
73    /// are relative to. This also means that `source` is contained within `root` if `root` is provided.
74    /// If `allow_macros` is `true`, macros will be processed like normal, otherwise they will be skipped entirely.
75    pub fn add_patterns_buffer(
76        &mut self,
77        bytes: &[u8],
78        source: PathBuf,
79        root: Option<&Path>,
80        collection: &mut MetadataCollection,
81        allow_macros: bool,
82    ) {
83        self.patterns.push(pattern::List::from_bytes(bytes, source, root));
84        let last = self.patterns.last_mut().expect("just added");
85        if !allow_macros {
86            last.patterns
87                .retain(|p| !matches!(p.value, Value::MacroAssignments { .. }));
88        }
89        collection.update_from_list(last);
90    }
91
92    /// Pop the last attribute patterns list from our queue.
93    pub fn pop_pattern_list(&mut self) -> Option<gix_glob::search::pattern::List<Attributes>> {
94        self.patterns.pop()
95    }
96}
97
98/// Access and matching
99impl Search {
100    /// Match `relative_path`, a path relative to the repository, while respective `case`-sensitivity and write them to `out`
101    /// Return `true` if at least one pattern matched.
102    pub fn pattern_matching_relative_path(
103        &self,
104        relative_path: &BStr,
105        case: gix_glob::pattern::Case,
106        is_dir: Option<bool>,
107        out: &mut Outcome,
108    ) -> bool {
109        let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
110        let mut has_match = false;
111        self.patterns.iter().rev().any(|pl| {
112            has_match |= pattern_matching_relative_path(pl, relative_path, basename_pos, case, is_dir, out);
113            out.is_done()
114        });
115        has_match
116    }
117
118    /// Return the amount of pattern lists contained in this instance.
119    pub fn num_pattern_lists(&self) -> usize {
120        self.patterns.len()
121    }
122}
123
124impl Pattern for Attributes {
125    type Value = Value;
126
127    fn bytes_to_patterns(bytes: &[u8], _source: &std::path::Path) -> Vec<pattern::Mapping<Self::Value>> {
128        fn into_owned_assignments<'a>(
129            attrs: impl Iterator<Item = Result<crate::AssignmentRef<'a>, crate::name::Error>>,
130        ) -> Option<Assignments> {
131            let res = attrs
132                .map(|res| {
133                    res.map(|a| TrackedAssignment {
134                        id: Default::default(),
135                        inner: a.to_owned(),
136                    })
137                })
138                .collect::<Result<Assignments, _>>();
139            match res {
140                Ok(res) => Some(res),
141                Err(_err) => {
142                    gix_trace::warn!("{}", _err);
143                    None
144                }
145            }
146        }
147
148        crate::parse(bytes)
149            .filter_map(|res| match res {
150                Ok(pattern) => Some(pattern),
151                Err(_err) => {
152                    gix_trace::warn!("{}: {}", _source.display(), _err);
153                    None
154                }
155            })
156            .filter_map(|(pattern_kind, assignments, line_number)| {
157                let (pattern, value) = match pattern_kind {
158                    crate::parse::Kind::Macro(macro_name) => (
159                        gix_glob::Pattern {
160                            text: macro_name.as_str().into(),
161                            mode: macro_mode(),
162                            first_wildcard_pos: None,
163                        },
164                        Value::MacroAssignments {
165                            id: Default::default(),
166                            assignments: into_owned_assignments(assignments)?,
167                        },
168                    ),
169                    crate::parse::Kind::Pattern(p) => (
170                        (!p.is_negative()).then_some(p)?,
171                        Value::Assignments(into_owned_assignments(assignments)?),
172                    ),
173                };
174                pattern::Mapping {
175                    pattern,
176                    value,
177                    sequence_number: line_number,
178                }
179                .into()
180            })
181            .collect()
182    }
183}
184
185impl Attributes {
186    fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool {
187        pattern.mode != macro_mode()
188    }
189}
190
191fn macro_mode() -> gix_glob::pattern::Mode {
192    gix_glob::pattern::Mode::all()
193}
194
195/// Append all matches of patterns matching `relative_path` to `out`,
196/// providing a pre-computed `basename_pos` which is the starting position of the basename of `relative_path`.
197/// `case` specifies whether cases should be folded during matching or not.
198/// `is_dir` is true if `relative_path` is a directory.
199/// Return `true` if at least one pattern matched.
200#[allow(unused_variables)]
201fn pattern_matching_relative_path(
202    list: &gix_glob::search::pattern::List<Attributes>,
203    relative_path: &BStr,
204    basename_pos: Option<usize>,
205    case: gix_glob::pattern::Case,
206    is_dir: Option<bool>,
207    out: &mut Outcome,
208) -> bool {
209    let (relative_path, basename_start_pos) =
210        match list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case) {
211            Some(r) => r,
212            None => return false,
213        };
214    let cur_len = out.remaining();
215    'outer: for pattern::Mapping {
216        pattern,
217        value,
218        sequence_number,
219    } in list
220        .patterns
221        .iter()
222        .rev()
223        .filter(|pm| Attributes::may_use_glob_pattern(&pm.pattern))
224    {
225        let value: &Value = value;
226        let attrs = match value {
227            Value::MacroAssignments { .. } => {
228                unreachable!("we can't match on macros as they have no pattern")
229            }
230            Value::Assignments(attrs) => attrs,
231        };
232        if out.has_unspecified_attributes(attrs.iter().map(|attr| attr.id))
233            && pattern.matches_repo_relative_path(
234                relative_path,
235                basename_start_pos,
236                is_dir,
237                case,
238                gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
239            )
240        {
241            let all_filled = out.fill_attributes(attrs.iter(), pattern, list.source.as_ref(), *sequence_number);
242            if all_filled {
243                break 'outer;
244            }
245        }
246    }
247    cur_len != out.remaining()
248}