use libc::{c_char, c_int, size_t}; use std::cmp::Ordering; use std::ffi::{CString, OsStr, OsString}; use std::iter::IntoIterator; use std::path::{Component, Path, PathBuf}; use crate::{raw, Error}; #[doc(hidden)] pub trait IsNull { fn is_ptr_null(&self) -> bool; } impl IsNull for *const T { fn is_ptr_null(&self) -> bool { self.is_null() } } impl IsNull for *mut T { fn is_ptr_null(&self) -> bool { self.is_null() } } #[doc(hidden)] pub trait Binding: Sized { type Raw; unsafe fn from_raw(raw: Self::Raw) -> Self; fn raw(&self) -> Self::Raw; unsafe fn from_raw_opt(raw: T) -> Option where T: Copy + IsNull, Self: Binding, { if raw.is_ptr_null() { None } else { Some(Binding::from_raw(raw)) } } } /// Converts an iterator of repo paths into a git2-compatible array of cstrings. /// /// Only use this for repo-relative paths or pathspecs. /// /// See `iter2cstrs` for more details. pub fn iter2cstrs_paths( iter: I, ) -> Result<(Vec, Vec<*const c_char>, raw::git_strarray), Error> where T: IntoCString, I: IntoIterator, { let cstrs = iter .into_iter() .map(|i| fixup_windows_path(i.into_c_string()?)) .collect::, _>>()?; iter2cstrs(cstrs) } /// Converts an iterator of things into a git array of c-strings. /// /// Returns a tuple `(cstrings, pointers, git_strarray)`. The first two values /// should not be dropped before `git_strarray`. pub fn iter2cstrs( iter: I, ) -> Result<(Vec, Vec<*const c_char>, raw::git_strarray), Error> where T: IntoCString, I: IntoIterator, { let cstrs = iter .into_iter() .map(|i| i.into_c_string()) .collect::, _>>()?; let ptrs = cstrs.iter().map(|i| i.as_ptr()).collect::>(); let raw = raw::git_strarray { strings: ptrs.as_ptr() as *mut _, count: ptrs.len() as size_t, }; Ok((cstrs, ptrs, raw)) } #[cfg(unix)] pub fn bytes2path(b: &[u8]) -> &Path { use std::os::unix::prelude::*; Path::new(OsStr::from_bytes(b)) } #[cfg(windows)] pub fn bytes2path(b: &[u8]) -> &Path { use std::str; Path::new(str::from_utf8(b).unwrap()) } /// A class of types that can be converted to C strings. /// /// These types are represented internally as byte slices and it is quite rare /// for them to contain an interior 0 byte. pub trait IntoCString { /// Consume this container, converting it into a CString fn into_c_string(self) -> Result; } impl<'a, T: IntoCString + Clone> IntoCString for &'a T { fn into_c_string(self) -> Result { self.clone().into_c_string() } } impl<'a> IntoCString for &'a str { fn into_c_string(self) -> Result { Ok(CString::new(self)?) } } impl IntoCString for String { fn into_c_string(self) -> Result { Ok(CString::new(self.into_bytes())?) } } impl IntoCString for CString { fn into_c_string(self) -> Result { Ok(self) } } impl<'a> IntoCString for &'a Path { fn into_c_string(self) -> Result { let s: &OsStr = self.as_ref(); s.into_c_string() } } impl IntoCString for PathBuf { fn into_c_string(self) -> Result { let s: OsString = self.into(); s.into_c_string() } } impl<'a> IntoCString for &'a OsStr { fn into_c_string(self) -> Result { self.to_os_string().into_c_string() } } impl IntoCString for OsString { #[cfg(unix)] fn into_c_string(self) -> Result { use std::os::unix::prelude::*; let s: &OsStr = self.as_ref(); Ok(CString::new(s.as_bytes())?) } #[cfg(windows)] fn into_c_string(self) -> Result { match self.to_str() { Some(s) => s.into_c_string(), None => Err(Error::from_str( "only valid unicode paths are accepted on windows", )), } } } impl<'a> IntoCString for &'a [u8] { fn into_c_string(self) -> Result { Ok(CString::new(self)?) } } impl IntoCString for Vec { fn into_c_string(self) -> Result { Ok(CString::new(self)?) } } pub fn into_opt_c_string(opt_s: Option) -> Result, Error> where S: IntoCString, { match opt_s { None => Ok(None), Some(s) => Ok(Some(s.into_c_string()?)), } } pub fn c_cmp_to_ordering(cmp: c_int) -> Ordering { match cmp { 0 => Ordering::Equal, n if n < 0 => Ordering::Less, _ => Ordering::Greater, } } /// Converts a path to a CString that is usable by the libgit2 API. /// /// Checks if it is a relative path. /// /// On Windows, this also requires the path to be valid Unicode, and translates /// back slashes to forward slashes. pub fn path_to_repo_path(path: &Path) -> Result { macro_rules! err { ($msg:literal, $path:expr) => { return Err(Error::from_str(&format!($msg, $path.display()))) }; } match path.components().next() { None => return Err(Error::from_str("repo path should not be empty")), Some(Component::Prefix(_)) => err!( "repo path `{}` should be relative, not a windows prefix", path ), Some(Component::RootDir) => err!("repo path `{}` should be relative", path), Some(Component::CurDir) => err!("repo path `{}` should not start with `.`", path), Some(Component::ParentDir) => err!("repo path `{}` should not start with `..`", path), Some(Component::Normal(_)) => {} } #[cfg(windows)] { match path.to_str() { None => { return Err(Error::from_str( "only valid unicode paths are accepted on windows", )) } Some(s) => return fixup_windows_path(s), } } #[cfg(not(windows))] { path.into_c_string() } } pub fn cstring_to_repo_path(path: T) -> Result { fixup_windows_path(path.into_c_string()?) } #[cfg(windows)] fn fixup_windows_path>>(path: P) -> Result { let mut bytes: Vec = path.into(); for i in 0..bytes.len() { if bytes[i] == b'\\' { bytes[i] = b'/'; } } Ok(CString::new(bytes)?) } #[cfg(not(windows))] fn fixup_windows_path(path: CString) -> Result { Ok(path) } #[cfg(test)] mod tests { use super::*; macro_rules! assert_err { ($path:expr, $msg:expr) => { match path_to_repo_path(Path::new($path)) { Ok(_) => panic!("expected `{}` to err", $path), Err(e) => assert_eq!(e.message(), $msg), } }; } macro_rules! assert_repo_path_ok { ($path:expr) => { assert_repo_path_ok!($path, $path) }; ($path:expr, $expect:expr) => { assert_eq!( path_to_repo_path(Path::new($path)), Ok(CString::new($expect).unwrap()) ); }; } #[test] #[cfg(windows)] fn path_to_repo_path_translate() { assert_repo_path_ok!("foo"); assert_repo_path_ok!("foo/bar"); assert_repo_path_ok!(r"foo\bar", "foo/bar"); assert_repo_path_ok!(r"foo\bar\", "foo/bar/"); } #[test] fn path_to_repo_path_no_weird() { assert_err!("", "repo path should not be empty"); assert_err!("./foo", "repo path `./foo` should not start with `.`"); assert_err!("../foo", "repo path `../foo` should not start with `..`"); } #[test] #[cfg(not(windows))] fn path_to_repo_path_no_absolute() { assert_err!("/", "repo path `/` should be relative"); assert_repo_path_ok!("foo/bar"); } #[test] #[cfg(windows)] fn path_to_repo_path_no_absolute() { assert_err!( r"c:", r"repo path `c:` should be relative, not a windows prefix" ); assert_err!( r"c:\", r"repo path `c:\` should be relative, not a windows prefix" ); assert_err!( r"c:temp", r"repo path `c:temp` should be relative, not a windows prefix" ); assert_err!( r"\\?\UNC\a\b\c", r"repo path `\\?\UNC\a\b\c` should be relative, not a windows prefix" ); assert_err!( r"\\?\c:\foo", r"repo path `\\?\c:\foo` should be relative, not a windows prefix" ); assert_err!( r"\\.\COM42", r"repo path `\\.\COM42` should be relative, not a windows prefix" ); assert_err!( r"\\a\b", r"repo path `\\a\b` should be relative, not a windows prefix" ); assert_err!(r"\", r"repo path `\` should be relative"); assert_err!(r"/", r"repo path `/` should be relative"); assert_err!(r"\foo", r"repo path `\foo` should be relative"); assert_err!(r"/foo", r"repo path `/foo` should be relative"); } }