git2/
util.rs

1use libc::{c_char, c_int, size_t};
2use std::cmp::Ordering;
3use std::ffi::{CString, OsStr, OsString};
4use std::path::{Component, Path, PathBuf};
5
6use crate::{raw, Error};
7
8#[doc(hidden)]
9pub trait IsNull {
10    fn is_ptr_null(&self) -> bool;
11}
12impl<T> IsNull for *const T {
13    fn is_ptr_null(&self) -> bool {
14        self.is_null()
15    }
16}
17impl<T> IsNull for *mut T {
18    fn is_ptr_null(&self) -> bool {
19        self.is_null()
20    }
21}
22
23#[doc(hidden)]
24pub trait Binding: Sized {
25    type Raw;
26
27    unsafe fn from_raw(raw: Self::Raw) -> Self;
28    fn raw(&self) -> Self::Raw;
29
30    unsafe fn from_raw_opt<T>(raw: T) -> Option<Self>
31    where
32        T: Copy + IsNull,
33        Self: Binding<Raw = T>,
34    {
35        if raw.is_ptr_null() {
36            None
37        } else {
38            Some(Binding::from_raw(raw))
39        }
40    }
41}
42
43/// Converts an iterator of repo paths into a git2-compatible array of cstrings.
44///
45/// Only use this for repo-relative paths or pathspecs.
46///
47/// See `iter2cstrs` for more details.
48pub fn iter2cstrs_paths<T, I>(
49    iter: I,
50) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
51where
52    T: IntoCString,
53    I: IntoIterator<Item = T>,
54{
55    let cstrs = iter
56        .into_iter()
57        .map(|i| fixup_windows_path(i.into_c_string()?))
58        .collect::<Result<Vec<CString>, _>>()?;
59    iter2cstrs(cstrs)
60}
61
62/// Converts an iterator of things into a git array of c-strings.
63///
64/// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values
65/// should not be dropped before `git_strarray`.
66pub fn iter2cstrs<T, I>(
67    iter: I,
68) -> Result<(Vec<CString>, Vec<*const c_char>, raw::git_strarray), Error>
69where
70    T: IntoCString,
71    I: IntoIterator<Item = T>,
72{
73    let cstrs = iter
74        .into_iter()
75        .map(|i| i.into_c_string())
76        .collect::<Result<Vec<CString>, _>>()?;
77    let ptrs = cstrs.iter().map(|i| i.as_ptr()).collect::<Vec<_>>();
78    let raw = raw::git_strarray {
79        strings: ptrs.as_ptr() as *mut _,
80        count: ptrs.len() as size_t,
81    };
82    Ok((cstrs, ptrs, raw))
83}
84
85#[cfg(unix)]
86pub fn bytes2path(b: &[u8]) -> &Path {
87    use std::os::unix::prelude::*;
88    Path::new(OsStr::from_bytes(b))
89}
90#[cfg(windows)]
91pub fn bytes2path(b: &[u8]) -> &Path {
92    use std::str;
93    Path::new(str::from_utf8(b).unwrap())
94}
95
96/// A class of types that can be converted to C strings.
97///
98/// These types are represented internally as byte slices and it is quite rare
99/// for them to contain an interior 0 byte.
100pub trait IntoCString {
101    /// Consume this container, converting it into a CString
102    fn into_c_string(self) -> Result<CString, Error>;
103}
104
105impl<'a, T: IntoCString + Clone> IntoCString for &'a T {
106    fn into_c_string(self) -> Result<CString, Error> {
107        self.clone().into_c_string()
108    }
109}
110
111impl<'a> IntoCString for &'a str {
112    fn into_c_string(self) -> Result<CString, Error> {
113        Ok(CString::new(self)?)
114    }
115}
116
117impl IntoCString for String {
118    fn into_c_string(self) -> Result<CString, Error> {
119        Ok(CString::new(self.into_bytes())?)
120    }
121}
122
123impl IntoCString for CString {
124    fn into_c_string(self) -> Result<CString, Error> {
125        Ok(self)
126    }
127}
128
129impl<'a> IntoCString for &'a Path {
130    fn into_c_string(self) -> Result<CString, Error> {
131        let s: &OsStr = self.as_ref();
132        s.into_c_string()
133    }
134}
135
136impl IntoCString for PathBuf {
137    fn into_c_string(self) -> Result<CString, Error> {
138        let s: OsString = self.into();
139        s.into_c_string()
140    }
141}
142
143impl<'a> IntoCString for &'a OsStr {
144    fn into_c_string(self) -> Result<CString, Error> {
145        self.to_os_string().into_c_string()
146    }
147}
148
149impl IntoCString for OsString {
150    #[cfg(unix)]
151    fn into_c_string(self) -> Result<CString, Error> {
152        use std::os::unix::prelude::*;
153        let s: &OsStr = self.as_ref();
154        Ok(CString::new(s.as_bytes())?)
155    }
156    #[cfg(windows)]
157    fn into_c_string(self) -> Result<CString, Error> {
158        match self.to_str() {
159            Some(s) => s.into_c_string(),
160            None => Err(Error::from_str(
161                "only valid unicode paths are accepted on windows",
162            )),
163        }
164    }
165}
166
167impl<'a> IntoCString for &'a [u8] {
168    fn into_c_string(self) -> Result<CString, Error> {
169        Ok(CString::new(self)?)
170    }
171}
172
173impl IntoCString for Vec<u8> {
174    fn into_c_string(self) -> Result<CString, Error> {
175        Ok(CString::new(self)?)
176    }
177}
178
179pub fn into_opt_c_string<S>(opt_s: Option<S>) -> Result<Option<CString>, Error>
180where
181    S: IntoCString,
182{
183    match opt_s {
184        None => Ok(None),
185        Some(s) => Ok(Some(s.into_c_string()?)),
186    }
187}
188
189pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering {
190    match cmp {
191        0 => Ordering::Equal,
192        n if n < 0 => Ordering::Less,
193        _ => Ordering::Greater,
194    }
195}
196
197/// Converts a path to a CString that is usable by the libgit2 API.
198///
199/// Checks if it is a relative path.
200///
201/// On Windows, this also requires the path to be valid Unicode, and translates
202/// back slashes to forward slashes.
203pub fn path_to_repo_path(path: &Path) -> Result<CString, Error> {
204    macro_rules! err {
205        ($msg:literal, $path:expr) => {
206            return Err(Error::from_str(&format!($msg, $path.display())))
207        };
208    }
209    match path.components().next() {
210        None => return Err(Error::from_str("repo path should not be empty")),
211        Some(Component::Prefix(_)) => err!(
212            "repo path `{}` should be relative, not a windows prefix",
213            path
214        ),
215        Some(Component::RootDir) => err!("repo path `{}` should be relative", path),
216        Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path),
217        Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path),
218        Some(Component::Normal(_)) => {}
219    }
220    #[cfg(windows)]
221    {
222        match path.to_str() {
223            None => {
224                return Err(Error::from_str(
225                    "only valid unicode paths are accepted on windows",
226                ))
227            }
228            Some(s) => return fixup_windows_path(s),
229        }
230    }
231    #[cfg(not(windows))]
232    {
233        path.into_c_string()
234    }
235}
236
237pub fn cstring_to_repo_path<T: IntoCString>(path: T) -> Result<CString, Error> {
238    fixup_windows_path(path.into_c_string()?)
239}
240
241#[cfg(windows)]
242fn fixup_windows_path<P: Into<Vec<u8>>>(path: P) -> Result<CString, Error> {
243    let mut bytes: Vec<u8> = path.into();
244    for i in 0..bytes.len() {
245        if bytes[i] == b'\\' {
246            bytes[i] = b'/';
247        }
248    }
249    Ok(CString::new(bytes)?)
250}
251
252#[cfg(not(windows))]
253fn fixup_windows_path(path: CString) -> Result<CString, Error> {
254    Ok(path)
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    macro_rules! assert_err {
262        ($path:expr, $msg:expr) => {
263            match path_to_repo_path(Path::new($path)) {
264                Ok(_) => panic!("expected `{}` to err", $path),
265                Err(e) => assert_eq!(e.message(), $msg),
266            }
267        };
268    }
269
270    macro_rules! assert_repo_path_ok {
271        ($path:expr) => {
272            assert_repo_path_ok!($path, $path)
273        };
274        ($path:expr, $expect:expr) => {
275            assert_eq!(
276                path_to_repo_path(Path::new($path)),
277                Ok(CString::new($expect).unwrap())
278            );
279        };
280    }
281
282    #[test]
283    #[cfg(windows)]
284    fn path_to_repo_path_translate() {
285        assert_repo_path_ok!("foo");
286        assert_repo_path_ok!("foo/bar");
287        assert_repo_path_ok!(r"foo\bar", "foo/bar");
288        assert_repo_path_ok!(r"foo\bar\", "foo/bar/");
289    }
290
291    #[test]
292    fn path_to_repo_path_no_weird() {
293        assert_err!("", "repo path should not be empty");
294        assert_err!("./foo", "repo path `./foo` should not start with `.`");
295        assert_err!("../foo", "repo path `../foo` should not start with `..`");
296    }
297
298    #[test]
299    #[cfg(not(windows))]
300    fn path_to_repo_path_no_absolute() {
301        assert_err!("/", "repo path `/` should be relative");
302        assert_repo_path_ok!("foo/bar");
303    }
304
305    #[test]
306    #[cfg(windows)]
307    fn path_to_repo_path_no_absolute() {
308        assert_err!(
309            r"c:",
310            r"repo path `c:` should be relative, not a windows prefix"
311        );
312        assert_err!(
313            r"c:\",
314            r"repo path `c:\` should be relative, not a windows prefix"
315        );
316        assert_err!(
317            r"c:temp",
318            r"repo path `c:temp` should be relative, not a windows prefix"
319        );
320        assert_err!(
321            r"\\?\UNC\a\b\c",
322            r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix"
323        );
324        assert_err!(
325            r"\\?\c:\foo",
326            r"repo path `\\?\c:\foo` should be relative, not a windows prefix"
327        );
328        assert_err!(
329            r"\\.\COM42",
330            r"repo path `\\.\COM42` should be relative, not a windows prefix"
331        );
332        assert_err!(
333            r"\\a\b",
334            r"repo path `\\a\b` should be relative, not a windows prefix"
335        );
336        assert_err!(r"\", r"repo path `\` should be relative");
337        assert_err!(r"/", r"repo path `/` should be relative");
338        assert_err!(r"\foo", r"repo path `\foo` should be relative");
339        assert_err!(r"/foo", r"repo path `/foo` should be relative");
340    }
341}