diff --git a/src/exec/input.rs b/src/exec/input.rs new file mode 100644 index 0000000..4d05b8f --- /dev/null +++ b/src/exec/input.rs @@ -0,0 +1,250 @@ +use std::path::MAIN_SEPARATOR; +use std::borrow::Cow; + +#[cfg(windows)] +const ESCAPE: char = '^'; + +#[cfg(not(windows))] +const ESCAPE: char = '\\'; + +/// A builder for efficiently generating input strings. +/// +/// After choosing your required specs, the `get()` method will escape special characters found +/// in the input. Allocations will only occur if special characters are found that need to be +/// escaped. +pub struct Input<'a> { + data: &'a str, +} + +impl<'a> Input<'a> { + /// Creates a new `Input` structure, which provides access to command-building + /// primitives, such as `basename()` and `dirname()`. + pub fn new(data: &'a str) -> Input<'a> { + Input { data } + } + + /// Removes the parent component of the path + pub fn basename(&'a mut self) -> &'a mut Self { + let mut index = 0; + for (id, character) in self.data.char_indices() { + if character == MAIN_SEPARATOR { + index = id; + } + } + + if index != 0 { + self.data = &self.data[index + 1..] + } + + self + } + + /// Removes the extension from the path + pub fn remove_extension(&'a mut self) -> &'a mut Self { + let mut dir_index = 0; + let mut ext_index = 0; + + for (id, character) in self.data.char_indices() { + if character == MAIN_SEPARATOR { + dir_index = id; + } + if character == '.' { + ext_index = id; + } + } + + // Account for hidden files and directories + if ext_index != 0 && dir_index + 2 <= ext_index { + self.data = &self.data[0..ext_index]; + } + + self + } + + /// Removes the basename from the path. + pub fn dirname(&'a mut self) -> &'a mut Self { + let mut index = 0; + for (id, character) in self.data.char_indices() { + if character == MAIN_SEPARATOR { + index = id; + } + } + + self.data = if index == 0 { + "." + } else { + &self.data[0..index] + }; + + self + } + + pub fn get(&'a self) -> Cow<'a, str> { + fn char_is_quotable(x: char) -> bool { + [ + ' ', + '(', + ')', + '[', + ']', + '&', + '$', + '@', + '{', + '}', + '<', + '>', + '|', + ';', + '"', + '\'', + '#', + '*', + '%', + '?', + '`', + ].contains(&x) + }; + + // If a quotable character is found, we will use that position for allocating. + let pos = match self.data.find(char_is_quotable) { + Some(pos) => pos, + // Otherwise, we will return the contents of `data` without allocating. + None => return Cow::Borrowed(self.data), + }; + + // When building the input string, we will start by adding the characters that + // we've already verified to be free of special characters. + let mut owned = String::with_capacity(self.data.len()); + owned.push_str(&self.data[..pos]); + owned.push(ESCAPE); + + // This slice contains the data that is left to be scanned for special characters. + // If multiple characters are found, this slice will be sliced and updated multiple times. + let mut slice = &self.data[pos..]; + + // Repeatedly search for special characters until all special characters have been found, + // appending and inserting the escape character each time, as well as updating our + // starting position. + while let Some(pos) = slice[1..].find(char_is_quotable) { + owned.push_str(&slice[..pos + 1]); + owned.push(ESCAPE); + slice = &slice[pos + 1..]; + } + + // Finally, we return our newly-allocated input string. + owned.push_str(slice); + Cow::Owned(owned) + } +} + +#[cfg(test)] +mod tests { + use super::{MAIN_SEPARATOR, Input}; + + fn correct(input: &str) -> String { + let mut sep = String::new(); + sep.push(MAIN_SEPARATOR); + input.replace('/', &sep) + } + + #[test] + fn path_remove_ext_simple() { + assert_eq!(&Input::new("foo.txt").remove_extension().get(), "foo"); + } + + #[test] + fn path_remove_ext_dir() { + assert_eq!( + &Input::new(&correct("dir/foo.txt")).remove_extension().get(), + &correct("dir/foo") + ); + } + + #[test] + fn path_hidden() { + assert_eq!(&Input::new(".foo").remove_extension().get(), ".foo") + } + + #[test] + fn path_remove_ext_utf8() { + assert_eq!(&Input::new("💖.txt").remove_extension().get(), "💖"); + } + + #[test] + fn path_remove_ext_empty() { + assert_eq!(&Input::new("").remove_extension().get(), ""); + } + + #[test] + fn path_basename_simple() { + assert_eq!(&Input::new("foo.txt").basename().get(), "foo.txt"); + } + + #[test] + fn path_basename_dir() { + assert_eq!( + &Input::new(&correct("dir/foo.txt")).basename().get(), + "foo.txt" + ); + } + + #[test] + fn path_basename_empty() { + assert_eq!(&Input::new("").basename().get(), ""); + } + + #[test] + fn path_basename_utf8() { + assert_eq!( + &Input::new(&correct("💖/foo.txt")).basename().get(), + "foo.txt" + ); + assert_eq!( + &Input::new(&correct("dir/💖.txt")).basename().get(), + "💖.txt" + ); + } + + #[test] + fn path_dirname_simple() { + assert_eq!(&Input::new("foo.txt").dirname().get(), "."); + } + + #[test] + fn path_dirname_dir() { + assert_eq!(&Input::new(&correct("dir/foo.txt")).dirname().get(), "dir"); + } + + #[test] + fn path_dirname_utf8() { + assert_eq!( + &Input::new(&correct("💖/foo.txt")).dirname().get(), + "💖" + ); + assert_eq!(&Input::new(&correct("dir/💖.txt")).dirname().get(), "dir"); + } + + #[test] + fn path_dirname_empty() { + assert_eq!(&Input::new("").dirname().get(), "."); + } + + #[cfg(windows)] + #[test] + fn path_special_chars() { + assert_eq!( + &Input::new("A Directory\\And A File").get(), + "A^ Directory\\And^ A^ File" + ); + } + + #[cfg(not(windows))] + #[test] + fn path_special_chars() { + assert_eq!( + &Input::new("A Directory/And A File").get(), + "A\\ Directory/And\\ A\\ File" + ); + } +} diff --git a/src/exec/mod.rs b/src/exec/mod.rs index cb86fbe..d91bb06 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -2,12 +2,13 @@ mod ticket; mod token; mod job; -mod paths; +mod input; use std::path::Path; use std::sync::{Arc, Mutex}; -use self::paths::{basename, dirname, remove_extension}; +// use self::paths::{basename, dirname, remove_extension}; +use self::input::Input; use self::ticket::CommandTicket; use self::token::Token; pub use self::job::job; @@ -109,18 +110,18 @@ impl TokenizedCommand { input: &Path, out_perm: Arc>, ) -> CommandTicket<'a> { - let input = input.strip_prefix(".").unwrap_or(input); - + use self::Token::*; + let input = input.strip_prefix(".").unwrap_or(input).to_string_lossy(); for token in &self.tokens { match *token { - Token::Basename => *command += basename(&input.to_string_lossy()), - Token::BasenameNoExt => { - *command += remove_extension(basename(&input.to_string_lossy())) + Basename => *command += &Input::new(&input).basename().get(), + BasenameNoExt => { + *command += &Input::new(&input).basename().remove_extension().get() } - Token::NoExt => *command += remove_extension(&input.to_string_lossy()), - Token::Parent => *command += dirname(&input.to_string_lossy()), - Token::Placeholder => *command += &input.to_string_lossy(), - Token::Text(ref string) => *command += string, + NoExt => *command += &Input::new(&input).remove_extension().get(), + Parent => *command += &Input::new(&input).dirname().get(), + Placeholder => *command += &Input::new(&input).get(), + Text(ref string) => *command += string, } } diff --git a/src/exec/paths.rs b/src/exec/paths.rs deleted file mode 100644 index 6cf2f0f..0000000 --- a/src/exec/paths.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::path::MAIN_SEPARATOR; - -pub fn basename(input: &str) -> &str { - let mut index = 0; - for (id, character) in input.char_indices() { - if character == MAIN_SEPARATOR { - index = id; - } - } - if index == 0 { - input - } else { - &input[index + 1..] - } -} - -/// Removes the extension of a given input -pub fn remove_extension(input: &str) -> &str { - let mut dir_index = 0; - let mut ext_index = 0; - - for (id, character) in input.char_indices() { - if character == MAIN_SEPARATOR { - dir_index = id; - } - if character == '.' { - ext_index = id; - } - } - - // Account for hidden files and directories - if ext_index == 0 || dir_index + 2 > ext_index { - input - } else { - &input[0..ext_index] - } -} - -pub fn dirname(input: &str) -> &str { - let mut index = 0; - for (id, character) in input.char_indices() { - if character == MAIN_SEPARATOR { - index = id; - } - } - if index == 0 { "." } else { &input[0..index] } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn correct(input: &str) -> String { - let mut sep = String::new(); - sep.push(MAIN_SEPARATOR); - input.replace('/', &sep) - } - - #[test] - fn path_remove_ext_simple() { - assert_eq!(remove_extension("foo.txt"), "foo"); - } - - #[test] - fn path_remove_ext_dir() { - assert_eq!( - remove_extension(&correct("dir/foo.txt")), - &correct("dir/foo") - ); - } - - #[test] - fn path_hidden() { - assert_eq!(remove_extension(".foo"), ".foo") - } - - #[test] - fn path_remove_ext_utf8() { - assert_eq!(remove_extension("💖.txt"), "💖"); - } - - #[test] - fn path_remove_ext_empty() { - assert_eq!(remove_extension(""), ""); - } - - #[test] - fn path_basename_simple() { - assert_eq!(basename("foo.txt"), "foo.txt"); - } - - #[test] - fn path_basename_dir() { - assert_eq!(basename(&correct("dir/foo.txt")), "foo.txt"); - } - - #[test] - fn path_basename_empty() { - assert_eq!(basename(""), ""); - } - - #[test] - fn path_basename_utf8() { - assert_eq!(basename(&correct("💖/foo.txt")), "foo.txt"); - assert_eq!(basename(&correct("dir/💖.txt")), "💖.txt"); - } - - #[test] - fn path_dirname_simple() { - assert_eq!(dirname("foo.txt"), "."); - } - - #[test] - fn path_dirname_dir() { - assert_eq!(dirname(&correct("dir/foo.txt")), "dir"); - } - - #[test] - fn path_dirname_utf8() { - assert_eq!(dirname(&correct("💖/foo.txt")), "💖"); - assert_eq!(dirname(&correct("dir/💖.txt")), "dir"); - } - - #[test] - fn path_dirname_empty() { - assert_eq!(dirname(""), "."); - } -}