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 =
62            gix_glob::search::add_patterns_file(&mut self.patterns, source, follow_symlinks, root, buf, Attributes)?;
63        if was_added {
64            let last = self.patterns.last_mut().expect("just added");
65            if !allow_macros {
66                last.patterns
67                    .retain(|p| !matches!(p.value, Value::MacroAssignments { .. }));
68            }
69            collection.update_from_list(last);
70        }
71        Ok(was_added)
72    }
73    /// Add patterns as parsed from `bytes`, providing their `source` path and possibly their `root` path, the path they
74    /// are relative to. This also means that `source` is contained within `root` if `root` is provided.
75    /// If `allow_macros` is `true`, macros will be processed like normal, otherwise they will be skipped entirely.
76    pub fn add_patterns_buffer(
77        &mut self,
78        bytes: &[u8],
79        source: PathBuf,
80        root: Option<&Path>,
81        collection: &mut MetadataCollection,
82        allow_macros: bool,
83    ) {
84        self.patterns
85            .push(pattern::List::from_bytes(bytes, source, root, Attributes));
86        let last = self.patterns.last_mut().expect("just added");
87        if !allow_macros {
88            last.patterns
89                .retain(|p| !matches!(p.value, Value::MacroAssignments { .. }));
90        }
91        collection.update_from_list(last);
92    }
93
94    /// Pop the last attribute patterns list from our queue.
95    pub fn pop_pattern_list(&mut self) -> Option<gix_glob::search::pattern::List<Attributes>> {
96        self.patterns.pop()
97    }
98}
99
100/// Access and matching
101impl Search {
102    /// Match `relative_path`, a path relative to the repository, while respective `case`-sensitivity and write them to `out`
103    /// Return `true` if at least one pattern matched.
104    pub fn pattern_matching_relative_path(
105        &self,
106        relative_path: &BStr,
107        case: gix_glob::pattern::Case,
108        is_dir: Option<bool>,
109        out: &mut Outcome,
110    ) -> bool {
111        let basename_pos = relative_path.rfind(b"/").map(|p| p + 1);
112        let mut has_match = false;
113        self.patterns.iter().rev().any(|pl| {
114            has_match |= pattern_matching_relative_path(pl, relative_path, basename_pos, case, is_dir, out);
115            out.is_done()
116        });
117        has_match
118    }
119
120    /// Return the amount of pattern lists contained in this instance.
121    pub fn num_pattern_lists(&self) -> usize {
122        self.patterns.len()
123    }
124}
125
126impl Pattern for Attributes {
127    type Value = Value;
128
129    fn bytes_to_patterns(&self, bytes: &[u8], _source: &std::path::Path) -> Vec<pattern::Mapping<Self::Value>> {
130        fn into_owned_assignments<'a>(
131            attrs: impl Iterator<Item = Result<crate::AssignmentRef<'a>, crate::name::Error>>,
132        ) -> Option<Assignments> {
133            let res = attrs
134                .map(|res| {
135                    res.map(|a| TrackedAssignment {
136                        id: Default::default(),
137                        inner: a.to_owned(),
138                    })
139                })
140                .collect::<Result<Assignments, _>>();
141            match res {
142                Ok(res) => Some(res),
143                Err(_err) => {
144                    gix_trace::warn!("{}", _err);
145                    None
146                }
147            }
148        }
149
150        crate::parse(bytes)
151            .filter_map(|res| match res {
152                Ok(pattern) => Some(pattern),
153                Err(_err) => {
154                    gix_trace::warn!("{}: {}", _source.display(), _err);
155                    None
156                }
157            })
158            .filter_map(|(pattern_kind, assignments, line_number)| {
159                let (pattern, value) = match pattern_kind {
160                    crate::parse::Kind::Macro(macro_name) => (
161                        gix_glob::Pattern {
162                            text: macro_name.as_str().into(),
163                            mode: macro_mode(),
164                            first_wildcard_pos: None,
165                        },
166                        Value::MacroAssignments {
167                            id: Default::default(),
168                            assignments: into_owned_assignments(assignments)?,
169                        },
170                    ),
171                    crate::parse::Kind::Pattern(p) => (
172                        (!p.is_negative()).then_some(p)?,
173                        Value::Assignments(into_owned_assignments(assignments)?),
174                    ),
175                };
176                pattern::Mapping {
177                    pattern,
178                    value,
179                    sequence_number: line_number,
180                }
181                .into()
182            })
183            .collect()
184    }
185}
186
187impl Attributes {
188    fn may_use_glob_pattern(pattern: &gix_glob::Pattern) -> bool {
189        pattern.mode != macro_mode()
190    }
191}
192
193fn macro_mode() -> gix_glob::pattern::Mode {
194    gix_glob::pattern::Mode::all()
195}
196
197/// Append all matches of patterns matching `relative_path` to `out`,
198/// providing a pre-computed `basename_pos` which is the starting position of the basename of `relative_path`.
199/// `case` specifies whether cases should be folded during matching or not.
200/// `is_dir` is true if `relative_path` is a directory.
201/// Return `true` if at least one pattern matched.
202#[allow(unused_variables)]
203fn pattern_matching_relative_path(
204    list: &gix_glob::search::pattern::List<Attributes>,
205    relative_path: &BStr,
206    basename_pos: Option<usize>,
207    case: gix_glob::pattern::Case,
208    is_dir: Option<bool>,
209    out: &mut Outcome,
210) -> bool {
211    let (relative_path, basename_start_pos) =
212        match list.strip_base_handle_recompute_basename_pos(relative_path, basename_pos, case) {
213            Some(r) => r,
214            None => return false,
215        };
216    let cur_len = out.remaining();
217    'outer: for pattern::Mapping {
218        pattern,
219        value,
220        sequence_number,
221    } in list
222        .patterns
223        .iter()
224        .rev()
225        .filter(|pm| Attributes::may_use_glob_pattern(&pm.pattern))
226    {
227        let value: &Value = value;
228        let attrs = match value {
229            Value::MacroAssignments { .. } => {
230                unreachable!("we can't match on macros as they have no pattern")
231            }
232            Value::Assignments(attrs) => attrs,
233        };
234        if out.has_unspecified_attributes(attrs.iter().map(|attr| attr.id))
235            && pattern.matches_repo_relative_path(
236                relative_path,
237                basename_start_pos,
238                is_dir,
239                case,
240                gix_glob::wildmatch::Mode::NO_MATCH_SLASH_LITERAL,
241            )
242        {
243            let all_filled = out.fill_attributes(attrs.iter(), pattern, list.source.as_ref(), *sequence_number);
244            if all_filled {
245                break 'outer;
246            }
247        }
248    }
249    cur_len != out.remaining()
250}