//! Rust containers for valid `git-config` types. use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; use quick_error::quick_error;#[cfg(feature = "serde")]use serde::{Serialize, Serializer}; /// Removes quotes, if any, from the provided inputs. This assumes the input/// contains a even number of unescaped quotes, and will unescape escaped/// quotes. The return values should be safe for value interpretation.////// This has optimizations for fully-quoted values, where the returned value/// will be a borrowed reference if the only mutation necessary is to unquote/// the value.////// This is the function used to normalize raw values from higher level/// abstractions over the [`parser`] implementation. Generally speaking these/// high level abstractions will handle normalization for you, and you do not/// need to call this yourself. However, if you're directly handling events/// from the parser, you may want to use this to help with value interpretation.////// Generally speaking, you'll want to use one of the variants of this function,/// such as [`normalize_str`] or [`normalize_vec`].////// # Examples////// Values don't need modification are returned borrowed, without allocation.////// ```/// # use std::borrow::Cow;/// # use git_config::values::normalize_str;/// assert_eq!(normalize_str("hello world"), Cow::Borrowed(b"hello world".as_slice()));/// ```////// Fully quoted values are optimized to not need allocations.////// ```/// # use std::borrow::Cow;/// # use git_config::values::normalize_str;/// assert_eq!(normalize_str("\"hello world\""), Cow::Borrowed(b"hello world".as_slice()));/// ```////// Quoted values are unwrapped as an owned variant.////// ```/// # use std::borrow::Cow;/// # use git_config::values::normalize_str;/// assert_eq!(normalize_str("hello \"world\""), Cow::<[u8]>::Owned(b"hello world".to_vec()));/// ```////// Escaped quotes are unescaped.////// ```/// # use std::borrow::Cow;/// # use git_config::values::normalize_str;/// assert_eq!(normalize_str(r#"hello "world\"""#), Cow::<[u8]>::Owned(br#"hello world""#.to_vec()));/// ```////// [`parser`]: crate::parser::Parser#[must_use]pub fn normalize_cow(input: Cow<'_, [u8]>) -> Cow<'_, [u8]> { let size = input.len(); if &*input == b"\"\"" { return Cow::Borrowed(&[]); } if size >= 3 && input[0] == b'=' && input[size - 1] == b'=' && input[size - 2] != b'\\' { match input { Cow::Borrowed(input) => return normalize_bytes(&input[1..size]), Cow::Owned(mut input) => { input.pop(); input.remove(0); return normalize_vec(input); } } } let mut owned = vec![]; let mut first_index = 0; let mut last_index = 0; let mut was_escaped = false; for (i, c) in input.iter().enumerate() { if was_escaped { was_escaped = false; if *c == b'"' { if first_index == 0 { owned.extend(&input[last_index..i - 1]); last_index = i; } else { owned.extend(&input[first_index..i - 1]); first_index = i; } } continue; } if *c == b'\\' { was_escaped = true; } else if *c == b'"' { if first_index == 0 { owned.extend(&input[last_index..i]); first_index = i + 1; } else { owned.extend(&input[first_index..i]); first_index = 0; last_index = i + 1; } } } if last_index == 0 { input } else { owned.extend(&input[last_index..]); Cow::Owned(owned) }} /// `&[u8]` variant of [`normalize_cow`].#[inline]#[must_use]pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> { normalize_cow(Cow::Borrowed(input))} /// `Vec[u8]` variant of [`normalize_cow`].#[inline]#[must_use]pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> { normalize_cow(Cow::Owned(input))} /// [`str`] variant of [`normalize_cow`].#[inline]#[must_use]pub fn normalize_str(input: &str) -> Cow<'_, [u8]> { normalize_bytes(input.as_bytes())} /// Converts string to byte slice#[cfg(test)]fn b(s: &str) -> &[u8] { s.as_bytes()} /// Any string value#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]pub struct Bytes<'a> { /// bytes pub value: Cow<'a, [u8]>,} impl<'a> From<&'a [u8]> for Bytes<'a> { #[inline] fn from(s: &'a [u8]) -> Self { Self { value: Cow::Borrowed(s), } }} impl From<Vec<u8>> for Bytes<'_> { fn from(s: Vec<u8>) -> Self { Self { value: Cow::Owned(s) } }} impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> { #[inline] fn from(c: Cow<'a, [u8]>) -> Self { match c { Cow::Borrowed(c) => Self::from(c), Cow::Owned(c) => Self::from(c), } }} ///pub mod path { use std::borrow::Cow; #[cfg(not(target_os = "windows"))] use pwd::Passwd; use quick_error::ResultExt; use crate::values::Path; pub mod interpolate { use quick_error::quick_error; quick_error! { #[derive(Debug)] /// The error returned by [`Path::interpolate()`]. #[allow(missing_docs)] pub enum Error { Missing { what: &'static str } { display("{} is missing", what) } Utf8Conversion(what: &'static str, err: git_features::path::Utf8Error) { display("Ill-formed UTF-8 in {}", what) context(what: &'static str, err: git_features::path::Utf8Error) -> (what, err) source(err) } UsernameConversion(err: std::str::Utf8Error) { display("Ill-formed UTF-8 in username") source(err) from() } PwdFileQuery { display("User home info missing") } UserInterpolationUnsupported { display("User interpolation is not available on this platform") } } } } impl<'a> Path<'a> { /// Interpolates this path into a file system path. /// /// If this path starts with `~/` or `~user/` or `%(prefix)/` /// - `~/` is expanded to the value of `$HOME` on unix based systems. On windows, `SHGetKnownFolderPath` is used. /// See also [dirs](https://crates.io/crates/dirs). /// - `~user/` to the specified user’s home directory, e.g `~alice` might get expanded to `/home/alice` on linux. /// The interpolation uses `getpwnam` sys call and is therefore not available on windows. See also [pwd](https://crates.io/crates/pwd). /// - `%(prefix)/` is expanded to the location where gitoxide is installed. This location is not known at compile time and therefore need to be /// optionally provided by the caller through `git_install_dir`. /// /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value. pub fn interpolate( self, git_install_dir: Option<&std::path::Path>, ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { if self.is_empty() { return Err(interpolate::Error::Missing { what: "path" }); } const PREFIX: &[u8] = b"%(prefix)/"; const USER_HOME: &[u8] = b"~/"; if self.starts_with(PREFIX) { let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing { what: "git install dir", })?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); let path_without_trailing_slash = git_features::path::from_byte_vec(path_without_trailing_slash).context("path past %(prefix)")?; Ok(git_install_dir.join(path_without_trailing_slash).into()) } else if self.starts_with(USER_HOME) { let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?; let (_prefix, val) = self.split_at(USER_HOME.len()); let val = git_features::path::from_bytes(val).context("path past ~/")?; Ok(home_path.join(val).into()) } else if self.starts_with(b"~") && self.contains(&b'/') { self.interpolate_user() } else { Ok(git_features::path::from_bytes(self.value).context("unexpanded path")?) } } #[cfg(target_os = "windows")] fn interpolate_user(self) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { Err(interpolate::Error::UserInterpolationUnsupported) } #[cfg(not(target_os = "windows"))] fn interpolate_user(self) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { let (_prefix, val) = self.split_at("/".len()); let i = val .iter() .position(|&e| e == b'/') .ok_or(interpolate::Error::Missing { what: "/" })?; let (username, path_with_leading_slash) = val.split_at(i); let username = std::str::from_utf8(username)?; let home = Passwd::from_name(username) .map_err(|_| interpolate::Error::PwdFileQuery)? .ok_or(interpolate::Error::Missing { what: "pwd user info" })? .dir; let path_past_user_prefix = git_features::path::from_byte_slice(&path_with_leading_slash["/".len()..]) .context("path past ~user/")?; Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into()) } } #[cfg(test)] mod interpolate_tests { use std::borrow::Cow; use crate::values::{b, path::interpolate::Error, Path}; #[test] fn no_interpolation_for_paths_without_tilde_or_prefix() { let path = &b"/foo/bar"[..]; let actual = Path::from(Cow::Borrowed(path)); assert_eq!(&*actual, path); assert!( matches!(&actual.value, Cow::Borrowed(_)), "it does not unnecessarily copy values" ); } #[test] fn empty_path_is_error() { assert!(matches!( Path::from(Cow::Borrowed(b(""))).interpolate(None), Err(Error::Missing { what: "path" }) )); } #[test] fn prefix_substitutes_git_install_dir() { for git_install_dir in &["/tmp/git", "C:\\git"] { for (val, expected) in &[ (&b"%(prefix)/foo/bar"[..], "foo/bar"), (b"%(prefix)/foo\\bar", "foo\\bar"), ] { let expected = &std::path::PathBuf::from(format!( "{}{}{}", git_install_dir, std::path::MAIN_SEPARATOR, expected )); assert_eq!( &*Path::from(Cow::Borrowed(*val)) .interpolate(Some(std::path::Path::new(git_install_dir))) .unwrap(), expected, "prefix interpolation keeps separators as they are" ); } } } #[test] fn prefix_substitution_skipped_with_dot_slash() { let path = "./%(prefix)/foo/bar"; let git_install_dir = "/tmp/git"; assert_eq!( Path::from(Cow::Borrowed(b(path))) .interpolate(Some(std::path::Path::new(git_install_dir))) .unwrap(), std::path::Path::new(path) ); } #[test] fn tilde_substitutes_current_user() { let path = &b"~/foo/bar"[..]; let expected = format!( "{}{}foo/bar", dirs::home_dir().expect("empty home").display(), std::path::MAIN_SEPARATOR ); assert_eq!( Path::from(Cow::Borrowed(path)).interpolate(None).unwrap().as_ref(), std::path::Path::new(&expected), "note that path separators are not turned into slashes as we work with `std::path::Path`" ); } #[cfg(target_os = "windows")] #[test] fn tilde_with_given_user_is_unsupported_on_windows() { assert!(matches!( Path::from(Cow::Borrowed(&b"~baz/foo/bar"[..])).interpolate(None), Err(Error::UserInterpolationUnsupported) )); } #[cfg(not(target_os = "windows"))] #[test] fn tilde_with_given_user() { let user = std::env::var("USER").unwrap(); let home = std::env::var("HOME").unwrap(); let specific_user_home = format!("~{}", user); for path_suffix in &["foo/bar", "foo\\bar", ""] { let path = format!("{}{}{}", specific_user_home, std::path::MAIN_SEPARATOR, path_suffix); let expected = format!("{}{}{}", home, std::path::MAIN_SEPARATOR, path_suffix); assert_eq!( Path::from(Cow::Borrowed(b(&path))).interpolate(None).unwrap(), std::path::Path::new(&expected), "it keeps path separators as is" ); } } }} /// Any value that can be interpreted as a file path.////// Git represents file paths as byte arrays, modeled here as owned or borrowed byte sequences.#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]pub struct Path<'a> { /// The path string, un-interpolated pub value: Cow<'a, [u8]>,} impl<'a> std::ops::Deref for Path<'a> { type Target = [u8]; fn deref(&self) -> &Self::Target { self.value.as_ref() }} impl<'a> AsRef<[u8]> for Path<'a> { fn as_ref(&self) -> &[u8] { self.value.as_ref() }} impl<'a> From<Cow<'a, [u8]>> for Path<'a> { #[inline] fn from(value: Cow<'a, [u8]>) -> Self { Path { value } }} /// Any value that can be interpreted as a boolean.////// Note that while values can effectively be any byte string, the `git-config`/// documentation has a strict subset of values that may be interpreted as a/// boolean value, all of which are ASCII and thus UTF-8 representable./// Consequently, variants hold [`str`]s rather than [`[u8]`]s.#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]#[allow(missing_docs)]pub enum Boolean<'a> { True(TrueVariant<'a>), False(Cow<'a, str>),} impl Boolean<'_> { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. #[inline] #[must_use] pub fn to_vec(&self) -> Vec<u8> { self.into() } /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. #[inline] #[must_use] pub fn as_bytes(&self) -> &[u8] { self.into() }} quick_error! { #[derive(Debug, PartialEq)] /// The error returned when creating `Boolean` from byte string. #[allow(missing_docs)] pub enum BooleanError { InvalidFormat { display("Invalid argument format") } }} impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { type Error = BooleanError; fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { if let Ok(v) = TrueVariant::try_from(value) { return Ok(Self::True(v)); } if value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") || value.eq_ignore_ascii_case(b"zero") || value == b"\"\"" { return Ok(Self::False( std::str::from_utf8(value).expect("value is already validated").into(), )); } Err(BooleanError::InvalidFormat) }} impl TryFrom<Vec<u8>> for Boolean<'_> { type Error = BooleanError; fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { if value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") || value.eq_ignore_ascii_case(b"zero") || value == b"\"\"" { return Ok(Self::False(Cow::Owned( std::string::String::from_utf8(value).expect("value is already validated"),Lines 1–500 of 1545. Load more
//! Rust containers for valid `git-config` types. use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr}; use quick_error::quick_error; #[cfg(feature = "serde")] use serde::{Serialize, Serializer}; /// Removes quotes, if any, from the provided inputs. This assumes the input /// contains a even number of unescaped quotes, and will unescape escaped /// quotes. The return values should be safe for value interpretation. /// /// This has optimizations for fully-quoted values, where the returned value /// will be a borrowed reference if the only mutation necessary is to unquote /// the value. /// /// This is the function used to normalize raw values from higher level /// abstractions over the [`parser`] implementation. Generally speaking these /// high level abstractions will handle normalization for you, and you do not /// need to call this yourself. However, if you're directly handling events /// from the parser, you may want to use this to help with value interpretation. /// /// Generally speaking, you'll want to use one of the variants of this function, /// such as [`normalize_str`] or [`normalize_vec`]. /// /// # Examples /// /// Values don't need modification are returned borrowed, without allocation. /// /// ``` /// # use std::borrow::Cow; /// # use git_config::values::normalize_str; /// assert_eq!(normalize_str("hello world"), Cow::Borrowed(b"hello world".as_slice())); /// ``` /// /// Fully quoted values are optimized to not need allocations. /// /// ``` /// # use std::borrow::Cow; /// # use git_config::values::normalize_str; /// assert_eq!(normalize_str("\"hello world\""), Cow::Borrowed(b"hello world".as_slice())); /// ``` /// /// Quoted values are unwrapped as an owned variant. /// /// ``` /// # use std::borrow::Cow; /// # use git_config::values::normalize_str; /// assert_eq!(normalize_str("hello \"world\""), Cow::<[u8]>::Owned(b"hello world".to_vec())); /// ``` /// /// Escaped quotes are unescaped. /// /// ``` /// # use std::borrow::Cow; /// # use git_config::values::normalize_str; /// assert_eq!(normalize_str(r#"hello "world\"""#), Cow::<[u8]>::Owned(br#"hello world""#.to_vec())); /// ``` /// /// [`parser`]: crate::parser::Parser #[must_use] pub fn normalize_cow(input: Cow<'_, [u8]>) -> Cow<'_, [u8]> { let size = input.len(); if &*input == b"\"\"" { return Cow::Borrowed(&[]); } if size >= 3 && input[0] == b'=' && input[size - 1] == b'=' && input[size - 2] != b'\\' { match input { Cow::Borrowed(input) => return normalize_bytes(&input[1..size]), Cow::Owned(mut input) => { input.pop(); input.remove(0); return normalize_vec(input); } } } let mut owned = vec![]; let mut first_index = 0; let mut last_index = 0; let mut was_escaped = false; for (i, c) in input.iter().enumerate() { if was_escaped { was_escaped = false; if *c == b'"' { if first_index == 0 { owned.extend(&input[last_index..i - 1]); last_index = i; } else { owned.extend(&input[first_index..i - 1]); first_index = i; } } continue; } if *c == b'\\' { was_escaped = true; } else if *c == b'"' { if first_index == 0 { owned.extend(&input[last_index..i]); first_index = i + 1; } else { owned.extend(&input[first_index..i]); first_index = 0; last_index = i + 1; } } } if last_index == 0 { input } else { owned.extend(&input[last_index..]); Cow::Owned(owned) } } /// `&[u8]` variant of [`normalize_cow`]. #[inline] #[must_use] pub fn normalize_bytes(input: &[u8]) -> Cow<'_, [u8]> { normalize_cow(Cow::Borrowed(input)) } /// `Vec[u8]` variant of [`normalize_cow`]. #[inline] #[must_use] pub fn normalize_vec(input: Vec<u8>) -> Cow<'static, [u8]> { normalize_cow(Cow::Owned(input)) } /// [`str`] variant of [`normalize_cow`]. #[inline] #[must_use] pub fn normalize_str(input: &str) -> Cow<'_, [u8]> { normalize_bytes(input.as_bytes()) } /// Converts string to byte slice #[cfg(test)] fn b(s: &str) -> &[u8] { s.as_bytes() } /// Any string value #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Bytes<'a> { /// bytes pub value: Cow<'a, [u8]>, } impl<'a> From<&'a [u8]> for Bytes<'a> { #[inline] fn from(s: &'a [u8]) -> Self { Self { value: Cow::Borrowed(s), } } } impl From<Vec<u8>> for Bytes<'_> { fn from(s: Vec<u8>) -> Self { Self { value: Cow::Owned(s) } } } impl<'a> From<Cow<'a, [u8]>> for Bytes<'a> { #[inline] fn from(c: Cow<'a, [u8]>) -> Self { match c { Cow::Borrowed(c) => Self::from(c), Cow::Owned(c) => Self::from(c), } } } /// pub mod path { use std::borrow::Cow; #[cfg(not(target_os = "windows"))] use pwd::Passwd; use quick_error::ResultExt; use crate::values::Path; pub mod interpolate { use quick_error::quick_error; quick_error! { #[derive(Debug)] /// The error returned by [`Path::interpolate()`]. #[allow(missing_docs)] pub enum Error { Missing { what: &'static str } { display("{} is missing", what) } Utf8Conversion(what: &'static str, err: git_features::path::Utf8Error) { display("Ill-formed UTF-8 in {}", what) context(what: &'static str, err: git_features::path::Utf8Error) -> (what, err) source(err) } UsernameConversion(err: std::str::Utf8Error) { display("Ill-formed UTF-8 in username") source(err) from() } PwdFileQuery { display("User home info missing") } UserInterpolationUnsupported { display("User interpolation is not available on this platform") } } } } impl<'a> Path<'a> { /// Interpolates this path into a file system path. /// /// If this path starts with `~/` or `~user/` or `%(prefix)/` /// - `~/` is expanded to the value of `$HOME` on unix based systems. On windows, `SHGetKnownFolderPath` is used. /// See also [dirs](https://crates.io/crates/dirs). /// - `~user/` to the specified user’s home directory, e.g `~alice` might get expanded to `/home/alice` on linux. /// The interpolation uses `getpwnam` sys call and is therefore not available on windows. See also [pwd](https://crates.io/crates/pwd). /// - `%(prefix)/` is expanded to the location where gitoxide is installed. This location is not known at compile time and therefore need to be /// optionally provided by the caller through `git_install_dir`. /// /// Any other, non-empty path value is returned unchanged and error is returned in case of an empty path value. pub fn interpolate( self, git_install_dir: Option<&std::path::Path>, ) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { if self.is_empty() { return Err(interpolate::Error::Missing { what: "path" }); } const PREFIX: &[u8] = b"%(prefix)/"; const USER_HOME: &[u8] = b"~/"; if self.starts_with(PREFIX) { let git_install_dir = git_install_dir.ok_or(interpolate::Error::Missing { what: "git install dir", })?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); let path_without_trailing_slash = git_features::path::from_byte_vec(path_without_trailing_slash).context("path past %(prefix)")?; Ok(git_install_dir.join(path_without_trailing_slash).into()) } else if self.starts_with(USER_HOME) { let home_path = dirs::home_dir().ok_or(interpolate::Error::Missing { what: "home dir" })?; let (_prefix, val) = self.split_at(USER_HOME.len()); let val = git_features::path::from_bytes(val).context("path past ~/")?; Ok(home_path.join(val).into()) } else if self.starts_with(b"~") && self.contains(&b'/') { self.interpolate_user() } else { Ok(git_features::path::from_bytes(self.value).context("unexpanded path")?) } } #[cfg(target_os = "windows")] fn interpolate_user(self) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { Err(interpolate::Error::UserInterpolationUnsupported) } #[cfg(not(target_os = "windows"))] fn interpolate_user(self) -> Result<Cow<'a, std::path::Path>, interpolate::Error> { let (_prefix, val) = self.split_at("/".len()); let i = val .iter() .position(|&e| e == b'/') .ok_or(interpolate::Error::Missing { what: "/" })?; let (username, path_with_leading_slash) = val.split_at(i); let username = std::str::from_utf8(username)?; let home = Passwd::from_name(username) .map_err(|_| interpolate::Error::PwdFileQuery)? .ok_or(interpolate::Error::Missing { what: "pwd user info" })? .dir; let path_past_user_prefix = git_features::path::from_byte_slice(&path_with_leading_slash["/".len()..]) .context("path past ~user/")?; Ok(std::path::PathBuf::from(home).join(path_past_user_prefix).into()) } } #[cfg(test)] mod interpolate_tests { use std::borrow::Cow; use crate::values::{b, path::interpolate::Error, Path}; #[test] fn no_interpolation_for_paths_without_tilde_or_prefix() { let path = &b"/foo/bar"[..]; let actual = Path::from(Cow::Borrowed(path)); assert_eq!(&*actual, path); assert!( matches!(&actual.value, Cow::Borrowed(_)), "it does not unnecessarily copy values" ); } #[test] fn empty_path_is_error() { assert!(matches!( Path::from(Cow::Borrowed(b(""))).interpolate(None), Err(Error::Missing { what: "path" }) )); } #[test] fn prefix_substitutes_git_install_dir() { for git_install_dir in &["/tmp/git", "C:\\git"] { for (val, expected) in &[ (&b"%(prefix)/foo/bar"[..], "foo/bar"), (b"%(prefix)/foo\\bar", "foo\\bar"), ] { let expected = &std::path::PathBuf::from(format!( "{}{}{}", git_install_dir, std::path::MAIN_SEPARATOR, expected )); assert_eq!( &*Path::from(Cow::Borrowed(*val)) .interpolate(Some(std::path::Path::new(git_install_dir))) .unwrap(), expected, "prefix interpolation keeps separators as they are" ); } } } #[test] fn prefix_substitution_skipped_with_dot_slash() { let path = "./%(prefix)/foo/bar"; let git_install_dir = "/tmp/git"; assert_eq!( Path::from(Cow::Borrowed(b(path))) .interpolate(Some(std::path::Path::new(git_install_dir))) .unwrap(), std::path::Path::new(path) ); } #[test] fn tilde_substitutes_current_user() { let path = &b"~/foo/bar"[..]; let expected = format!( "{}{}foo/bar", dirs::home_dir().expect("empty home").display(), std::path::MAIN_SEPARATOR ); assert_eq!( Path::from(Cow::Borrowed(path)).interpolate(None).unwrap().as_ref(), std::path::Path::new(&expected), "note that path separators are not turned into slashes as we work with `std::path::Path`" ); } #[cfg(target_os = "windows")] #[test] fn tilde_with_given_user_is_unsupported_on_windows() { assert!(matches!( Path::from(Cow::Borrowed(&b"~baz/foo/bar"[..])).interpolate(None), Err(Error::UserInterpolationUnsupported) )); } #[cfg(not(target_os = "windows"))] #[test] fn tilde_with_given_user() { let user = std::env::var("USER").unwrap(); let home = std::env::var("HOME").unwrap(); let specific_user_home = format!("~{}", user); for path_suffix in &["foo/bar", "foo\\bar", ""] { let path = format!("{}{}{}", specific_user_home, std::path::MAIN_SEPARATOR, path_suffix); let expected = format!("{}{}{}", home, std::path::MAIN_SEPARATOR, path_suffix); assert_eq!( Path::from(Cow::Borrowed(b(&path))).interpolate(None).unwrap(), std::path::Path::new(&expected), "it keeps path separators as is" ); } } } } /// Any value that can be interpreted as a file path. /// /// Git represents file paths as byte arrays, modeled here as owned or borrowed byte sequences. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub struct Path<'a> { /// The path string, un-interpolated pub value: Cow<'a, [u8]>, } impl<'a> std::ops::Deref for Path<'a> { type Target = [u8]; fn deref(&self) -> &Self::Target { self.value.as_ref() } } impl<'a> AsRef<[u8]> for Path<'a> { fn as_ref(&self) -> &[u8] { self.value.as_ref() } } impl<'a> From<Cow<'a, [u8]>> for Path<'a> { #[inline] fn from(value: Cow<'a, [u8]>) -> Self { Path { value } } } /// Any value that can be interpreted as a boolean. /// /// Note that while values can effectively be any byte string, the `git-config` /// documentation has a strict subset of values that may be interpreted as a /// boolean value, all of which are ASCII and thus UTF-8 representable. /// Consequently, variants hold [`str`]s rather than [`[u8]`]s. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[allow(missing_docs)] pub enum Boolean<'a> { True(TrueVariant<'a>), False(Cow<'a, str>), } impl Boolean<'_> { /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. #[inline] #[must_use] pub fn to_vec(&self) -> Vec<u8> { self.into() } /// Generates a byte representation of the value. This should be used when /// non-UTF-8 sequences are present or a UTF-8 representation can't be /// guaranteed. #[inline] #[must_use] pub fn as_bytes(&self) -> &[u8] { self.into() } } quick_error! { #[derive(Debug, PartialEq)] /// The error returned when creating `Boolean` from byte string. #[allow(missing_docs)] pub enum BooleanError { InvalidFormat { display("Invalid argument format") } } } impl<'a> TryFrom<&'a [u8]> for Boolean<'a> { type Error = BooleanError; fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { if let Ok(v) = TrueVariant::try_from(value) { return Ok(Self::True(v)); } if value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") || value.eq_ignore_ascii_case(b"zero") || value == b"\"\"" { return Ok(Self::False( std::str::from_utf8(value).expect("value is already validated").into(), )); } Err(BooleanError::InvalidFormat) } } impl TryFrom<Vec<u8>> for Boolean<'_> { type Error = BooleanError; fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { if value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") || value.eq_ignore_ascii_case(b"zero") || value == b"\"\"" { return Ok(Self::False(Cow::Owned( std::string::String::from_utf8(value).expect("value is already validated"),