Skip to main content

gix_ref/store/file/
find.rs

1use gix_error::{ErrorExt, ExnResult, Message, ResultExt, message};
2
3use std::{
4    borrow::Cow,
5    io::{self, Read},
6    path::{Path, PathBuf},
7};
8
9use crate::{
10    BStr, BString, FullNameRef, PartialName, PartialNameRef, Reference, file,
11    name::is_pseudo_ref,
12    store_impl::{file::loose, packed},
13};
14
15/// ### Finding References - notes about precomposed unicode.
16///
17/// Generally, ref names and the target of symbolic refs are stored as-is if [`Self::precompose_unicode`] is `false`.
18/// If `true`, refs are stored as precomposed unicode in `packed-refs`, but stored as is on disk as it is then assumed
19/// to be indifferent, i.e. `"a\u{308}"` is the same as `"รค"`.
20///
21/// This also means that when refs are packed for transmission to another machine, both their names and the target of
22/// symbolic references need to be precomposed.
23///
24/// Namespaces are left as is as they never get past the particular repository that uses them.
25impl file::Store {
26    /// Find a single reference by the given `path` which is required to be a valid reference name.
27    ///
28    /// Returns `Ok(None)` if no such ref exists.
29    ///
30    /// ### Note
31    ///
32    /// * The lookup algorithm follows the one in [the git documentation][git-lookup-docs].
33    /// * The packed buffer is checked for modifications each time the method is called. See [`file::Store::try_find_packed()`]
34    ///   for a version with more control.
35    ///
36    /// [git-lookup-docs]: https://github.com/git/git/blob/5d5b1473453400224ebb126bf3947e0a3276bdf5/Documentation/revisions.txt#L34-L46
37    pub fn try_find<'a, Name, E>(&self, partial: Name) -> ExnResult<Option<Reference>>
38    where
39        Name: TryInto<&'a PartialNameRef, Error = E>,
40        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
41    {
42        let packed = self.assure_packed_refs_uptodate()?;
43        self.find_one_with_verified_input(
44            partial
45                .try_into()
46                .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?,
47            packed.as_ref().map(|b| &***b),
48        )
49    }
50
51    /// Like [`file::Store::try_find()`], returning `None` for a non-existing reference.
52    ///
53    /// Find only loose references, that is references that aren't in the packed-refs buffer.
54    /// All symbolic references are loose references.
55    /// `HEAD` is always a loose reference.
56    pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> ExnResult<Option<loose::Reference>>
57    where
58        Name: TryInto<&'a PartialNameRef, Error = E>,
59        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
60    {
61        self.find_one_with_verified_input(
62            partial
63                .try_into()
64                .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?,
65            None,
66        )
67        .map(|r| r.map(Into::into))
68    }
69
70    /// Similar to [`file::Store::find()`], but allows to pass a snapshotted packed buffer instead.
71    pub fn try_find_packed<'a, Name, E>(
72        &self,
73        partial: Name,
74        packed: Option<&packed::Buffer>,
75    ) -> ExnResult<Option<Reference>>
76    where
77        Name: TryInto<&'a PartialNameRef, Error = E>,
78        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
79    {
80        self.find_one_with_verified_input(
81            partial
82                .try_into()
83                .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?,
84            packed,
85        )
86    }
87
88    pub(crate) fn find_one_with_verified_input(
89        &self,
90        partial_name: &PartialNameRef,
91        packed: Option<&packed::Buffer>,
92    ) -> ExnResult<Option<Reference>> {
93        fn decompose_if(mut r: Reference, input_changed_to_precomposed: bool) -> Reference {
94            if input_changed_to_precomposed {
95                use gix_object::bstr::ByteSlice;
96                let decomposed = r
97                    .name
98                    .0
99                    .to_str()
100                    .ok()
101                    .map(|name| gix_utils::str::decompose(name.into()));
102                if let Some(Cow::Owned(decomposed)) = decomposed {
103                    r.name.0 = decomposed.into();
104                }
105            }
106            r
107        }
108        let mut buf = BString::default();
109        let mut precomposed_partial_name_storage = packed.filter(|_| self.precompose_unicode).and_then(|_| {
110            use gix_object::bstr::ByteSlice;
111            let precomposed = partial_name.0.to_str().ok()?;
112            let precomposed = gix_utils::str::precompose(precomposed.into());
113            match precomposed {
114                Cow::Owned(precomposed) => Some(PartialName(precomposed.into())),
115                Cow::Borrowed(_) => None,
116            }
117        });
118        let precomposed_partial_name = precomposed_partial_name_storage
119            .as_ref()
120            .map(std::convert::AsRef::as_ref);
121        for consider_pseudo_ref in [true, false] {
122            if !consider_pseudo_ref && !is_pseudo_ref(partial_name.as_bstr()) {
123                break;
124            }
125            'try_directories: for inbetween in &["", "tags", "heads", "remotes"] {
126                match self.find_inner(
127                    inbetween,
128                    partial_name,
129                    precomposed_partial_name,
130                    packed,
131                    &mut buf,
132                    consider_pseudo_ref,
133                ) {
134                    Ok(Some(r)) => return Ok(Some(decompose_if(r, precomposed_partial_name.is_some()))),
135                    Ok(None) => {
136                        if consider_pseudo_ref && is_pseudo_ref(partial_name.as_bstr()) {
137                            break 'try_directories;
138                        }
139                        continue;
140                    }
141                    Err(err) => return Err(err),
142                }
143            }
144        }
145        if partial_name.as_bstr() != "HEAD" {
146            if let Some(mut precomposed) = precomposed_partial_name_storage {
147                precomposed = precomposed.join("HEAD".into()).expect("HEAD is valid name");
148                precomposed_partial_name_storage = Some(precomposed);
149            }
150            self.find_inner(
151                "remotes",
152                partial_name
153                    .to_owned()
154                    .join("HEAD".into())
155                    .expect("HEAD is valid name")
156                    .as_ref(),
157                precomposed_partial_name_storage
158                    .as_ref()
159                    .map(std::convert::AsRef::as_ref),
160                None,
161                &mut buf,
162                true, /* consider-pseudo-ref */
163            )
164            .map(|res| res.map(|r| decompose_if(r, precomposed_partial_name_storage.is_some())))
165        } else {
166            Ok(None)
167        }
168    }
169
170    /// Resolve and read a candidate. Read failures include [metadata](gix_error::Exn::metadata()) `path` (native path),
171    /// the file that failed.
172    fn find_inner(
173        &self,
174        inbetween: &str,
175        partial_name: &PartialNameRef,
176        precomposed_partial_name: Option<&PartialNameRef>,
177        packed: Option<&packed::Buffer>,
178        path_buf: &mut BString,
179        consider_pseudo_ref: bool,
180    ) -> ExnResult<Option<Reference>> {
181        let full_name = precomposed_partial_name
182            .unwrap_or(partial_name)
183            .construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref);
184        let content_buf = match self.ref_contents(full_name) {
185            Ok(content_buf) => content_buf,
186            Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None),
187            Err(err) => {
188                return Err(err
189                    .and_raise(read_reference_error(self.reference_path(full_name)))
190                    .erased());
191            }
192        };
193
194        match content_buf {
195            None => {
196                if let Some(packed) = packed
197                    && let Some(full_name) = packed::find::transform_full_name_for_lookup(full_name)
198                {
199                    let full_name_backing;
200                    let full_name = match &self.namespace {
201                        Some(namespace) => {
202                            full_name_backing = namespace.to_owned().into_namespaced_name(full_name);
203                            full_name_backing.as_ref()
204                        }
205                        None => full_name,
206                    };
207                    if let Some(packed_ref) = packed.try_find_full_name(full_name)? {
208                        let mut res: Reference = packed_ref.into();
209                        if let Some(namespace) = &self.namespace {
210                            res.strip_namespace(namespace);
211                        }
212                        return Ok(Some(res));
213                    }
214                }
215                Ok(None)
216            }
217            Some(content) => Ok(Some(
218                loose::Reference::try_from_path(full_name.to_owned(), &content, self.object_hash)
219                    .map(Into::into)
220                    .map(|mut r: Reference| {
221                        if let Some(namespace) = &self.namespace {
222                            r.strip_namespace(namespace);
223                        }
224                        r
225                    })
226                    .or_raise_erased(|| ReferenceDecode {
227                        relative_path: full_name.to_path().to_owned(),
228                    })?,
229            )),
230        }
231    }
232}
233
234impl file::Store {
235    pub(crate) fn to_base_dir_and_relative_name<'a>(
236        &self,
237        name: &'a FullNameRef,
238        is_reflog: bool,
239    ) -> (Cow<'_, Path>, &'a FullNameRef) {
240        let commondir = self.common_dir_resolved();
241        let linked_git_dir =
242            |worktree_name: &BStr| commondir.join("worktrees").join(gix_path::from_bstr(worktree_name));
243        name.category_and_short_name()
244            .map(|(c, sn)| {
245                use crate::Category::*;
246                let sn = FullNameRef::new_unchecked(sn);
247                match c {
248                    LinkedPseudoRef { name: worktree_name } => {
249                        if is_reflog {
250                            (linked_git_dir(worktree_name).into(), sn)
251                        } else {
252                            (commondir.into(), name)
253                        }
254                    }
255                    Tag | LocalBranch | RemoteBranch | Note => (commondir.into(), name),
256                    MainRef | MainPseudoRef => (commondir.into(), sn),
257                    LinkedRef { name: worktree_name } => {
258                        if sn.category().is_some_and(|cat| cat.is_worktree_private()) {
259                            if is_reflog {
260                                (linked_git_dir(worktree_name).into(), sn)
261                            } else {
262                                (commondir.into(), name)
263                            }
264                        } else {
265                            (commondir.into(), sn)
266                        }
267                    }
268                    PseudoRef | Bisect | Rewritten | WorktreePrivate => (self.git_dir.as_path().into(), name),
269                }
270            })
271            .unwrap_or((commondir.into(), name))
272    }
273
274    /// Implements the logic required to transform a fully qualified refname into a filesystem path
275    pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> (Cow<'_, Path>, Cow<'b, Path>) {
276        let (base, name) = self.to_base_dir_and_relative_name(name, false);
277        (
278            base,
279            match &self.namespace {
280                None => gix_path::to_native_path_on_windows(name.as_bstr()),
281                Some(namespace) => {
282                    gix_path::to_native_path_on_windows(namespace.to_owned().into_namespaced_name(name).into_inner())
283                }
284            },
285        )
286    }
287
288    /// Implements the logic required to transform a fully qualified refname into a filesystem path
289    pub(crate) fn reference_path(&self, name: &FullNameRef) -> PathBuf {
290        let (base, relative_path) = self.reference_path_with_base(name);
291        base.join(relative_path)
292    }
293
294    /// If `prohibit_windows_device_names` is set, check that `name` does not
295    /// contain a path component that matches a reserved Windows device name.
296    pub(crate) fn check_windows_device_name(&self, name: &FullNameRef) -> io::Result<()> {
297        if !self.prohibit_windows_device_names {
298            return Ok(());
299        }
300        let (_, relative_path) = self.reference_path_with_base(name);
301        if relative_path
302            .components()
303            .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok())
304            .any(|c| gix_validate::path::component_is_windows_device(c.as_ref()))
305        {
306            Err(std::io::Error::other(format!(
307                "Illegal use of reserved Windows device name in \"{}\"",
308                name.as_bstr()
309            )))
310        } else {
311            Ok(())
312        }
313    }
314
315    /// Read the file contents with a verified full reference path and return it in the given vector if possible.
316    pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result<Option<Vec<u8>>> {
317        self.check_windows_device_name(name)?;
318        let (base, relative_path) = self.reference_path_with_base(name);
319        let ref_path = base.join(&relative_path);
320        match std::fs::File::open(&ref_path) {
321            Ok(mut file) => {
322                let mut buf = Vec::with_capacity(128);
323                if let Err(err) = file.read_to_end(&mut buf) {
324                    return if ref_path.is_dir() { Ok(None) } else { Err(err) };
325                }
326                Ok(buf.into())
327            }
328            Err(err) if err.kind() == io::ErrorKind::NotFound => {
329                #[cfg(windows)]
330                if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) {
331                    return Err(io::Error::new(io::ErrorKind::NotADirectory, err));
332                }
333                Ok(None)
334            }
335            #[cfg(windows)]
336            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
337                if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) {
338                    Err(io::Error::new(io::ErrorKind::NotADirectory, err))
339                } else {
340                    Ok(None)
341                }
342            }
343            Err(err) => Err(err),
344        }
345    }
346}
347
348#[cfg(windows)]
349fn path_has_file_prefix(base: &Path, relative_path: &Path) -> bool {
350    let mut path = base.to_owned();
351    let mut components = relative_path.components().peekable();
352    while let Some(component) = components.next() {
353        if components.peek().is_none() {
354            break;
355        }
356        path.push(component.as_os_str());
357        match std::fs::metadata(&path) {
358            Ok(metadata) if metadata.is_file() => return true,
359            Ok(_) => {}
360            Err(err) if err.kind() == io::ErrorKind::NotFound => return false,
361            Err(_) => {}
362        }
363    }
364    false
365}
366
367impl file::Store {
368    /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error.
369    pub fn find<'a, Name, E>(&self, partial: Name) -> ExnResult<Reference>
370    where
371        Name: TryInto<&'a PartialNameRef, Error = E>,
372        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
373    {
374        let packed = self.assure_packed_refs_uptodate()?;
375        self.find_existing_inner(partial, packed.as_ref().map(|b| &***b))
376    }
377
378    /// Similar to [`file::Store::find()`], but supports a stable packed buffer.
379    pub fn find_packed<'a, Name, E>(&self, partial: Name, packed: Option<&packed::Buffer>) -> ExnResult<Reference>
380    where
381        Name: TryInto<&'a PartialNameRef, Error = E>,
382        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
383    {
384        self.find_existing_inner(partial, packed)
385    }
386
387    /// Similar to [`file::Store::find()`] won't handle packed-refs.
388    pub fn find_loose<'a, Name, E>(&self, partial: Name) -> ExnResult<loose::Reference>
389    where
390        Name: TryInto<&'a PartialNameRef, Error = E>,
391        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
392    {
393        self.find_existing_inner(partial, None).map(Into::into)
394    }
395
396    /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error.
397    pub(crate) fn find_existing_inner<'a, Name, E>(
398        &self,
399        partial: Name,
400        packed: Option<&packed::Buffer>,
401    ) -> ExnResult<Reference>
402    where
403        Name: TryInto<&'a PartialNameRef, Error = E>,
404        Result<&'a PartialNameRef, E>: ResultExt<Success = &'a PartialNameRef>,
405    {
406        let path = partial
407            .try_into()
408            .or_raise_erased(|| message("The ref name or path is not a valid ref name"))?;
409        match self.find_one_with_verified_input(path, packed) {
410            Ok(Some(r)) => Ok(r),
411            Ok(None) => Err(NotFound {
412                name: path.to_partial_path().to_owned(),
413            }
414            .raise_erased()),
415            Err(err) => Err(err),
416        }
417    }
418}
419
420/// The raised error's [metadata](gix_error::Exn::metadata()) `path` (native path) identifies the reference file that
421/// could not be read.
422pub(super) fn read_reference_error(path: impl Into<PathBuf>) -> Message {
423    Message::new("Could not read reference").with("path", path.into())
424}
425
426/// A reference lookup found no matching name, including a missing symbolic referent.
427#[derive(Debug)]
428pub struct NotFound {
429    /// The name whose lookup failed. It may have been discovered while following symbolic references.
430    pub name: PathBuf,
431}
432
433impl std::fmt::Display for NotFound {
434    #[allow(clippy::unnecessary_debug_formatting)]
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        write!(f, "The ref partially named {:?} could not be found", self.name)
437    }
438}
439
440impl std::error::Error for NotFound {
441    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
442        Some(const { &gix_error::ClassificationMarker::NOT_FOUND })
443    }
444}
445
446/// A loose reference was found at this path, but its contents could not be decoded.
447/// The decoding error is retained as a cause in the exception.
448#[derive(Debug)]
449pub struct ReferenceDecode {
450    /// The resolved reference path, relative to the Git directory.
451    pub relative_path: PathBuf,
452}
453
454impl std::fmt::Display for ReferenceDecode {
455    #[allow(clippy::unnecessary_debug_formatting)]
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        write!(f, "The reference at {:?} could not be decoded", self.relative_path)
458    }
459}
460
461impl std::error::Error for ReferenceDecode {}