Skip to main content

gix_ref/store/file/
find.rs

1use std::{
2    borrow::Cow,
3    io::{self, Read},
4    path::{Path, PathBuf},
5};
6
7pub use error::Error;
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) -> Result<Option<Reference>, Error>
38    where
39        Name: TryInto<&'a PartialNameRef, Error = E>,
40        Error: From<E>,
41    {
42        let packed = self.assure_packed_refs_uptodate()?;
43        self.find_one_with_verified_input(partial.try_into()?, packed.as_ref().map(|b| &***b))
44    }
45
46    /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error.
47    ///
48    /// Find only loose references, that is references that aren't in the packed-refs buffer.
49    /// All symbolic references are loose references.
50    /// `HEAD` is always a loose reference.
51    pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> Result<Option<loose::Reference>, Error>
52    where
53        Name: TryInto<&'a PartialNameRef, Error = E>,
54        Error: From<E>,
55    {
56        self.find_one_with_verified_input(partial.try_into()?, None)
57            .map(|r| r.map(Into::into))
58    }
59
60    /// Similar to [`file::Store::find()`], but allows to pass a snapshotted packed buffer instead.
61    pub fn try_find_packed<'a, Name, E>(
62        &self,
63        partial: Name,
64        packed: Option<&packed::Buffer>,
65    ) -> Result<Option<Reference>, Error>
66    where
67        Name: TryInto<&'a PartialNameRef, Error = E>,
68        Error: From<E>,
69    {
70        self.find_one_with_verified_input(partial.try_into()?, packed)
71    }
72
73    pub(crate) fn find_one_with_verified_input(
74        &self,
75        partial_name: &PartialNameRef,
76        packed: Option<&packed::Buffer>,
77    ) -> Result<Option<Reference>, Error> {
78        fn decompose_if(mut r: Reference, input_changed_to_precomposed: bool) -> Reference {
79            if input_changed_to_precomposed {
80                use gix_object::bstr::ByteSlice;
81                let decomposed = r
82                    .name
83                    .0
84                    .to_str()
85                    .ok()
86                    .map(|name| gix_utils::str::decompose(name.into()));
87                if let Some(Cow::Owned(decomposed)) = decomposed {
88                    r.name.0 = decomposed.into();
89                }
90            }
91            r
92        }
93        let mut buf = BString::default();
94        let mut precomposed_partial_name_storage = packed.filter(|_| self.precompose_unicode).and_then(|_| {
95            use gix_object::bstr::ByteSlice;
96            let precomposed = partial_name.0.to_str().ok()?;
97            let precomposed = gix_utils::str::precompose(precomposed.into());
98            match precomposed {
99                Cow::Owned(precomposed) => Some(PartialName(precomposed.into())),
100                Cow::Borrowed(_) => None,
101            }
102        });
103        let precomposed_partial_name = precomposed_partial_name_storage
104            .as_ref()
105            .map(std::convert::AsRef::as_ref);
106        for consider_pseudo_ref in [true, false] {
107            if !consider_pseudo_ref && !is_pseudo_ref(partial_name.as_bstr()) {
108                break;
109            }
110            'try_directories: for inbetween in &["", "tags", "heads", "remotes"] {
111                match self.find_inner(
112                    inbetween,
113                    partial_name,
114                    precomposed_partial_name,
115                    packed,
116                    &mut buf,
117                    consider_pseudo_ref,
118                ) {
119                    Ok(Some(r)) => return Ok(Some(decompose_if(r, precomposed_partial_name.is_some()))),
120                    Ok(None) => {
121                        if consider_pseudo_ref && is_pseudo_ref(partial_name.as_bstr()) {
122                            break 'try_directories;
123                        }
124                        continue;
125                    }
126                    Err(err) => return Err(err),
127                }
128            }
129        }
130        if partial_name.as_bstr() != "HEAD" {
131            if let Some(mut precomposed) = precomposed_partial_name_storage {
132                precomposed = precomposed.join("HEAD".into()).expect("HEAD is valid name");
133                precomposed_partial_name_storage = Some(precomposed);
134            }
135            self.find_inner(
136                "remotes",
137                partial_name
138                    .to_owned()
139                    .join("HEAD".into())
140                    .expect("HEAD is valid name")
141                    .as_ref(),
142                precomposed_partial_name_storage
143                    .as_ref()
144                    .map(std::convert::AsRef::as_ref),
145                None,
146                &mut buf,
147                true, /* consider-pseudo-ref */
148            )
149            .map(|res| res.map(|r| decompose_if(r, precomposed_partial_name_storage.is_some())))
150        } else {
151            Ok(None)
152        }
153    }
154
155    fn find_inner(
156        &self,
157        inbetween: &str,
158        partial_name: &PartialNameRef,
159        precomposed_partial_name: Option<&PartialNameRef>,
160        packed: Option<&packed::Buffer>,
161        path_buf: &mut BString,
162        consider_pseudo_ref: bool,
163    ) -> Result<Option<Reference>, Error> {
164        let full_name = precomposed_partial_name
165            .unwrap_or(partial_name)
166            .construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref);
167        let content_buf = match self.ref_contents(full_name) {
168            Ok(content_buf) => content_buf,
169            Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None),
170            Err(err) => {
171                return Err(Error::ReadFileContents {
172                    source: err,
173                    path: self.reference_path(full_name),
174                });
175            }
176        };
177
178        match content_buf {
179            None => {
180                if let Some(packed) = packed {
181                    if let Some(full_name) = packed::find::transform_full_name_for_lookup(full_name) {
182                        let full_name_backing;
183                        let full_name = match &self.namespace {
184                            Some(namespace) => {
185                                full_name_backing = namespace.to_owned().into_namespaced_name(full_name);
186                                full_name_backing.as_ref()
187                            }
188                            None => full_name,
189                        };
190                        if let Some(packed_ref) = packed.try_find_full_name(full_name)? {
191                            let mut res: Reference = packed_ref.into();
192                            if let Some(namespace) = &self.namespace {
193                                res.strip_namespace(namespace);
194                            }
195                            return Ok(Some(res));
196                        }
197                    }
198                }
199                Ok(None)
200            }
201            Some(content) => Ok(Some(
202                loose::Reference::try_from_path(full_name.to_owned(), &content, self.object_hash)
203                    .map(Into::into)
204                    .map(|mut r: Reference| {
205                        if let Some(namespace) = &self.namespace {
206                            r.strip_namespace(namespace);
207                        }
208                        r
209                    })
210                    .map_err(|err| Error::ReferenceCreation {
211                        source: err,
212                        relative_path: full_name.to_path().to_owned(),
213                    })?,
214            )),
215        }
216    }
217}
218
219impl file::Store {
220    pub(crate) fn to_base_dir_and_relative_name<'a>(
221        &self,
222        name: &'a FullNameRef,
223        is_reflog: bool,
224    ) -> (Cow<'_, Path>, &'a FullNameRef) {
225        let commondir = self.common_dir_resolved();
226        let linked_git_dir =
227            |worktree_name: &BStr| commondir.join("worktrees").join(gix_path::from_bstr(worktree_name));
228        name.category_and_short_name()
229            .map(|(c, sn)| {
230                use crate::Category::*;
231                let sn = FullNameRef::new_unchecked(sn);
232                match c {
233                    LinkedPseudoRef { name: worktree_name } => {
234                        if is_reflog {
235                            (linked_git_dir(worktree_name).into(), sn)
236                        } else {
237                            (commondir.into(), name)
238                        }
239                    }
240                    Tag | LocalBranch | RemoteBranch | Note => (commondir.into(), name),
241                    MainRef | MainPseudoRef => (commondir.into(), sn),
242                    LinkedRef { name: worktree_name } => {
243                        if sn.category().is_some_and(|cat| cat.is_worktree_private()) {
244                            if is_reflog {
245                                (linked_git_dir(worktree_name).into(), sn)
246                            } else {
247                                (commondir.into(), name)
248                            }
249                        } else {
250                            (commondir.into(), sn)
251                        }
252                    }
253                    PseudoRef | Bisect | Rewritten | WorktreePrivate => (self.git_dir.as_path().into(), name),
254                }
255            })
256            .unwrap_or((commondir.into(), name))
257    }
258
259    /// Implements the logic required to transform a fully qualified refname into a filesystem path
260    pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> (Cow<'_, Path>, Cow<'b, Path>) {
261        let (base, name) = self.to_base_dir_and_relative_name(name, false);
262        (
263            base,
264            match &self.namespace {
265                None => gix_path::to_native_path_on_windows(name.as_bstr()),
266                Some(namespace) => {
267                    gix_path::to_native_path_on_windows(namespace.to_owned().into_namespaced_name(name).into_inner())
268                }
269            },
270        )
271    }
272
273    /// Implements the logic required to transform a fully qualified refname into a filesystem path
274    pub(crate) fn reference_path(&self, name: &FullNameRef) -> PathBuf {
275        let (base, relative_path) = self.reference_path_with_base(name);
276        base.join(relative_path)
277    }
278
279    /// Read the file contents with a verified full reference path and return it in the given vector if possible.
280    pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result<Option<Vec<u8>>> {
281        let (base, relative_path) = self.reference_path_with_base(name);
282        if self.prohibit_windows_device_names
283            && relative_path
284                .components()
285                .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok())
286                .any(|c| gix_validate::path::component_is_windows_device(c.as_ref()))
287        {
288            return Err(std::io::Error::other(format!(
289                "Illegal use of reserved Windows device name in \"{}\"",
290                name.as_bstr()
291            )));
292        }
293
294        let ref_path = base.join(&relative_path);
295        match std::fs::File::open(&ref_path) {
296            Ok(mut file) => {
297                let mut buf = Vec::with_capacity(128);
298                if let Err(err) = file.read_to_end(&mut buf) {
299                    return if ref_path.is_dir() { Ok(None) } else { Err(err) };
300                }
301                Ok(buf.into())
302            }
303            Err(err) if err.kind() == io::ErrorKind::NotFound => {
304                #[cfg(windows)]
305                if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) {
306                    return Err(io::Error::new(io::ErrorKind::NotADirectory, err));
307                }
308                Ok(None)
309            }
310            #[cfg(windows)]
311            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
312                if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) {
313                    Err(io::Error::new(io::ErrorKind::NotADirectory, err))
314                } else {
315                    Ok(None)
316                }
317            }
318            Err(err) => Err(err),
319        }
320    }
321}
322
323#[cfg(windows)]
324fn path_has_file_prefix(base: &Path, relative_path: &Path) -> bool {
325    let mut path = base.to_owned();
326    let mut components = relative_path.components().peekable();
327    while let Some(component) = components.next() {
328        if components.peek().is_none() {
329            break;
330        }
331        path.push(component.as_os_str());
332        match std::fs::metadata(&path) {
333            Ok(metadata) if metadata.is_file() => return true,
334            Ok(_) => {}
335            Err(err) if err.kind() == io::ErrorKind::NotFound => return false,
336            Err(_) => {}
337        }
338    }
339    false
340}
341
342///
343pub mod existing {
344    pub use error::Error;
345
346    use crate::{
347        PartialNameRef, Reference,
348        file::{self},
349        store_impl::{
350            file::{find, loose},
351            packed,
352        },
353    };
354
355    impl file::Store {
356        /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error.
357        pub fn find<'a, Name, E>(&self, partial: Name) -> Result<Reference, Error>
358        where
359            Name: TryInto<&'a PartialNameRef, Error = E>,
360            crate::name::Error: From<E>,
361        {
362            let packed = self.assure_packed_refs_uptodate().map_err(find::Error::PackedOpen)?;
363            self.find_existing_inner(partial, packed.as_ref().map(|b| &***b))
364        }
365
366        /// Similar to [`file::Store::find()`], but supports a stable packed buffer.
367        pub fn find_packed<'a, Name, E>(
368            &self,
369            partial: Name,
370            packed: Option<&packed::Buffer>,
371        ) -> Result<Reference, Error>
372        where
373            Name: TryInto<&'a PartialNameRef, Error = E>,
374            crate::name::Error: From<E>,
375        {
376            self.find_existing_inner(partial, packed)
377        }
378
379        /// Similar to [`file::Store::find()`] won't handle packed-refs.
380        pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result<loose::Reference, Error>
381        where
382            Name: TryInto<&'a PartialNameRef, Error = E>,
383            crate::name::Error: From<E>,
384        {
385            self.find_existing_inner(partial, None).map(Into::into)
386        }
387
388        /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error.
389        pub(crate) fn find_existing_inner<'a, Name, E>(
390            &self,
391            partial: Name,
392            packed: Option<&packed::Buffer>,
393        ) -> Result<Reference, Error>
394        where
395            Name: TryInto<&'a PartialNameRef, Error = E>,
396            crate::name::Error: From<E>,
397        {
398            let path = partial
399                .try_into()
400                .map_err(|err| Error::Find(find::Error::RefnameValidation(err.into())))?;
401            match self.find_one_with_verified_input(path, packed) {
402                Ok(Some(r)) => Ok(r),
403                Ok(None) => Err(Error::NotFound {
404                    name: path.to_partial_path().to_owned(),
405                }),
406                Err(err) => Err(err.into()),
407            }
408        }
409    }
410
411    mod error {
412        use std::path::PathBuf;
413
414        use crate::store_impl::file::find;
415
416        /// The error returned by [file::Store::find_existing()][crate::file::Store::find()].
417        #[derive(Debug, thiserror::Error)]
418        #[allow(missing_docs)]
419        pub enum Error {
420            #[error("An error occurred while trying to find a reference")]
421            Find(#[from] find::Error),
422            #[error("The ref partially named {name:?} could not be found")]
423            NotFound { name: PathBuf },
424        }
425    }
426}
427
428mod error {
429    use std::{convert::Infallible, io, path::PathBuf};
430
431    use crate::{file, store_impl::packed};
432
433    /// The error returned by [file::Store::find()].
434    #[derive(Debug, thiserror::Error)]
435    #[allow(missing_docs)]
436    pub enum Error {
437        #[error("The ref name or path is not a valid ref name")]
438        RefnameValidation(#[from] crate::name::Error),
439        #[error("The ref file {path:?} could not be read in full")]
440        ReadFileContents { source: io::Error, path: PathBuf },
441        #[error("The reference at \"{relative_path}\" could not be instantiated")]
442        ReferenceCreation {
443            source: file::loose::reference::decode::Error,
444            relative_path: PathBuf,
445        },
446        #[error("A packed ref lookup failed")]
447        PackedRef(#[from] packed::find::Error),
448        #[error("Could not open the packed refs buffer when trying to find references.")]
449        PackedOpen(#[from] packed::buffer::open::Error),
450    }
451
452    impl From<Infallible> for Error {
453        fn from(_: Infallible) -> Self {
454            unreachable!("this impl is needed to allow passing a known valid partial path as parameter")
455        }
456    }
457}