use std::borrow::Cow; use bstr::{ByteSlice, ByteVec}; /// The final component of the path, if it is a normal file. /// /// If the path terminates in `.`, `..`, or consists solely of a root of /// prefix, file_name will return None. pub(crate) fn file_name<'a>(path: &Cow<'a, [u8]>) -> Option> { if path.last_byte().map_or(true, |b| b == b'.') { return None; } let last_slash = path.rfind_byte(b'/').map(|i| i + 1).unwrap_or(0); Some(match *path { Cow::Borrowed(path) => Cow::Borrowed(&path[last_slash..]), Cow::Owned(ref path) => { let mut path = path.clone(); path.drain_bytes(..last_slash); Cow::Owned(path) } }) } /// Return a file extension given a path's file name. /// /// Note that this does NOT match the semantics of std::path::Path::extension. /// Namely, the extension includes the `.` and matching is otherwise more /// liberal. Specifically, the extension is: /// /// * None, if the file name given is empty; /// * None, if there is no embedded `.`; /// * Otherwise, the portion of the file name starting with the final `.`. /// /// e.g., A file name of `.rs` has an extension `.rs`. /// /// N.B. This is done to make certain glob match optimizations easier. Namely, /// a pattern like `*.rs` is obviously trying to match files with a `rs` /// extension, but it also matches files like `.rs`, which doesn't have an /// extension according to std::path::Path::extension. pub(crate) fn file_name_ext<'a>( name: &Cow<'a, [u8]>, ) -> Option> { if name.is_empty() { return None; } let last_dot_at = match name.rfind_byte(b'.') { None => return None, Some(i) => i, }; Some(match *name { Cow::Borrowed(name) => Cow::Borrowed(&name[last_dot_at..]), Cow::Owned(ref name) => { let mut name = name.clone(); name.drain_bytes(..last_dot_at); Cow::Owned(name) } }) } /// Normalizes a path to use `/` as a separator everywhere, even on platforms /// that recognize other characters as separators. #[cfg(unix)] pub(crate) fn normalize_path(path: Cow<'_, [u8]>) -> Cow<'_, [u8]> { // UNIX only uses /, so we're good. path } /// Normalizes a path to use `/` as a separator everywhere, even on platforms /// that recognize other characters as separators. #[cfg(not(unix))] pub(crate) fn normalize_path(mut path: Cow<[u8]>) -> Cow<[u8]> { use std::path::is_separator; for i in 0..path.len() { if path[i] == b'/' || !is_separator(char::from(path[i])) { continue; } path.to_mut()[i] = b'/'; } path } #[cfg(test)] mod tests { use std::borrow::Cow; use bstr::{ByteVec, B}; use super::{file_name_ext, normalize_path}; macro_rules! ext { ($name:ident, $file_name:expr, $ext:expr) => { #[test] fn $name() { let bs = Vec::from($file_name); let got = file_name_ext(&Cow::Owned(bs)); assert_eq!($ext.map(|s| Cow::Borrowed(B(s))), got); } }; } ext!(ext1, "foo.rs", Some(".rs")); ext!(ext2, ".rs", Some(".rs")); ext!(ext3, "..rs", Some(".rs")); ext!(ext4, "", None::<&str>); ext!(ext5, "foo", None::<&str>); macro_rules! normalize { ($name:ident, $path:expr, $expected:expr) => { #[test] fn $name() { let bs = Vec::from_slice($path); let got = normalize_path(Cow::Owned(bs)); assert_eq!($expected.to_vec(), got.into_owned()); } }; } normalize!(normal1, b"foo", b"foo"); normalize!(normal2, b"foo/bar", b"foo/bar"); #[cfg(unix)] normalize!(normal3, b"foo\\bar", b"foo\\bar"); #[cfg(not(unix))] normalize!(normal3, b"foo\\bar", b"foo/bar"); #[cfg(unix)] normalize!(normal4, b"foo\\bar/baz", b"foo\\bar/baz"); #[cfg(not(unix))] normalize!(normal4, b"foo\\bar/baz", b"foo/bar/baz"); }