git_config_value/
path.rs

1use std::{borrow::Cow, path::PathBuf};
2
3use bstr::BStr;
4
5use crate::Path;
6
7///
8pub mod interpolate {
9    use std::path::PathBuf;
10
11    /// Options for interpolating paths with [`Path::interpolate()`][crate::Path::interpolate()].
12    #[derive(Clone, Copy)]
13    pub struct Context<'a> {
14        /// The location where gitoxide or git is installed. If `None`, `%(prefix)` in paths will cause an error.
15        pub git_install_dir: Option<&'a std::path::Path>,
16        /// The home directory of the current user. If `None`, `~/` in paths will cause an error.
17        pub home_dir: Option<&'a std::path::Path>,
18        /// A function returning the home directory of a given user. If `None`, `~name/` in paths will cause an error.
19        pub home_for_user: Option<fn(&str) -> Option<PathBuf>>,
20    }
21
22    impl Default for Context<'_> {
23        fn default() -> Self {
24            Context {
25                git_install_dir: None,
26                home_dir: None,
27                home_for_user: Some(home_for_user),
28            }
29        }
30    }
31
32    /// The error returned by [`Path::interpolate()`][crate::Path::interpolate()].
33    #[derive(Debug, thiserror::Error)]
34    #[allow(missing_docs)]
35    pub enum Error {
36        #[error("{} is missing", .what)]
37        Missing { what: &'static str },
38        #[error("Ill-formed UTF-8 in {}", .what)]
39        Utf8Conversion {
40            what: &'static str,
41            #[source]
42            err: git_path::Utf8Error,
43        },
44        #[error("Ill-formed UTF-8 in username")]
45        UsernameConversion(#[from] std::str::Utf8Error),
46        #[error("User interpolation is not available on this platform")]
47        UserInterpolationUnsupported,
48    }
49
50    /// Obtain the home directory for the given user `name` or return `None` if the user wasn't found
51    /// or any other error occurred.
52    /// It can be used as `home_for_user` parameter in [`Path::interpolate()`][crate::Path::interpolate()].
53    #[cfg_attr(windows, allow(unused_variables))]
54    pub fn home_for_user(name: &str) -> Option<PathBuf> {
55        #[cfg(not(any(target_os = "android", target_os = "windows")))]
56        {
57            let cname = std::ffi::CString::new(name).ok()?;
58            // SAFETY: calling this in a threaded program that modifies the pw database is not actually safe.
59            //         TODO: use the `*_r` version, but it's much harder to use.
60            #[allow(unsafe_code)]
61            let pwd = unsafe { libc::getpwnam(cname.as_ptr()) };
62            if pwd.is_null() {
63                None
64            } else {
65                use std::os::unix::ffi::OsStrExt;
66                // SAFETY: pw_dir is a cstr and it lives as long as… well, we hope nobody changes the pw database while we are at it
67                //         from another thread. Otherwise it lives long enough.
68                #[allow(unsafe_code)]
69                let cstr = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) };
70                Some(std::ffi::OsStr::from_bytes(cstr.to_bytes()).into())
71            }
72        }
73        #[cfg(any(target_os = "android", target_os = "windows"))]
74        {
75            None
76        }
77    }
78}
79
80impl<'a> std::ops::Deref for Path<'a> {
81    type Target = BStr;
82
83    fn deref(&self) -> &Self::Target {
84        self.value.as_ref()
85    }
86}
87
88impl<'a> AsRef<[u8]> for Path<'a> {
89    fn as_ref(&self) -> &[u8] {
90        self.value.as_ref()
91    }
92}
93
94impl<'a> AsRef<BStr> for Path<'a> {
95    fn as_ref(&self) -> &BStr {
96        self.value.as_ref()
97    }
98}
99
100impl<'a> From<Cow<'a, BStr>> for Path<'a> {
101    fn from(value: Cow<'a, BStr>) -> Self {
102        Path { value }
103    }
104}
105
106impl<'a> Path<'a> {
107    /// Interpolates this path into a path usable on the file system.
108    ///
109    /// If this path starts with `~/` or `~user/` or `%(prefix)/`
110    ///  - `~/` is expanded to the value of `home_dir`. The caller can use the [dirs](https://crates.io/crates/dirs) crate to obtain it.
111    ///    It it is required but not set, an error is produced.
112    ///  - `~user/` to the specified user’s home directory, e.g `~alice` might get expanded to `/home/alice` on linux, but requires
113    ///    the `home_for_user` function to be provided.
114    ///    The interpolation uses `getpwnam` sys call and is therefore not available on windows.
115    ///  - `%(prefix)/` is expanded to the location where `gitoxide` is installed.
116    ///     This location is not known at compile time and therefore need to be
117    ///     optionally provided by the caller through `git_install_dir`.
118    ///
119    /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value or if required input
120    /// wasn't provided.
121    pub fn interpolate(
122        self,
123        interpolate::Context {
124            git_install_dir,
125            home_dir,
126            home_for_user,
127        }: interpolate::Context<'_>,
128    ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
129        if self.is_empty() {
130            return Err(interpolate::Error::Missing { what: "path" });
131        }
132
133        const PREFIX: &[u8] = b"%(prefix)/";
134        const USER_HOME: &[u8] = b"~/";
135        if self.starts_with(PREFIX) {
136            let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing {
137                what: "git install dir",
138            })?;
139            let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
140            let path_without_trailing_slash =
141                git_path::try_from_bstring(path_without_trailing_slash).map_err(|err| {
142                    interpolate::Error::Utf8Conversion {
143                        what: "path past %(prefix)",
144                        err,
145                    }
146                })?;
147            Ok(git_install_dir.join(path_without_trailing_slash).into())
148        } else if self.starts_with(USER_HOME) {
149            let home_path = home_dir.ok_or(interpolate::Error::Missing { what: "home dir" })?;
150            let (_prefix, val) = self.split_at(USER_HOME.len());
151            let val = git_path::try_from_byte_slice(val).map_err(|err| interpolate::Error::Utf8Conversion {
152                what: "path past ~/",
153                err,
154            })?;
155            Ok(home_path.join(val).into())
156        } else if self.starts_with(b"~") && self.contains(&b'/') {
157            self.interpolate_user(home_for_user.ok_or(interpolate::Error::Missing {
158                what: "home for user lookup",
159            })?)
160        } else {
161            Ok(git_path::from_bstr(self.value))
162        }
163    }
164
165    #[cfg(any(target_os = "windows", target_os = "android"))]
166    fn interpolate_user(
167        self,
168        _home_for_user: fn(&str) -> Option<PathBuf>,
169    ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
170        Err(interpolate::Error::UserInterpolationUnsupported)
171    }
172
173    #[cfg(not(any(target_os = "windows", target_os = "android")))]
174    fn interpolate_user(
175        self,
176        home_for_user: fn(&str) -> Option<PathBuf>,
177    ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> {
178        let (_prefix, val) = self.split_at("/".len());
179        let i = val
180            .iter()
181            .position(|&e| e == b'/')
182            .ok_or(interpolate::Error::Missing { what: "/" })?;
183        let (username, path_with_leading_slash) = val.split_at(i);
184        let username = std::str::from_utf8(username)?;
185        let home = home_for_user(username).ok_or(interpolate::Error::Missing { what: "pwd user info" })?;
186        let path_past_user_prefix =
187            git_path::try_from_byte_slice(&path_with_leading_slash["/".len()..]).map_err(|err| {
188                interpolate::Error::Utf8Conversion {
189                    what: "path past ~user/",
190                    err,
191                }
192            })?;
193        Ok(home.join(path_past_user_prefix).into())
194    }
195}