use std::borrow::Cow;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt::{self, Debug};
static COMMAND_LIST_BEGIN: &str = "command_list_ok_begin\n";
static COMMAND_LIST_END: &str = "command_list_end\n";
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Command(Vec<String>);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CommandBuilder(Vec<CommandPart>, bool);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
enum CommandPart {
Command(String),
Argument(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandError {
pub reason: InvalidCommandReason,
pub list_at: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvalidCommandReason {
Empty,
InvalidCharacter(usize, char),
UnncessaryWhitespace,
CommandList,
}
impl Command {
pub fn build(command: impl Into<String>) -> CommandBuilder {
CommandBuilder(vec![CommandPart::Command(command.into())], false)
}
pub fn render(mut self) -> String {
if self.0.len() == 1 {
let mut c = self.0.pop().unwrap();
c.push('\n');
c
} else {
assert!(self.0.len() >= 2);
let mut out = String::with_capacity(
COMMAND_LIST_BEGIN.len()
+ self.0.iter().fold(0, |acc, c| acc + c.len() + 1)
+ COMMAND_LIST_END.len(),
);
out.push_str(COMMAND_LIST_BEGIN);
for c in self.0 {
out.push_str(&c);
out.push('\n');
}
out.push_str(COMMAND_LIST_END);
out
}
}
}
impl CommandBuilder {
pub fn argument(mut self, argument: impl Into<String>) -> Self {
self.0.push(CommandPart::Argument(argument.into()));
self
}
pub fn command(mut self, command: impl Into<String>) -> Self {
self.0.push(CommandPart::Command(command.into()));
self.1 = true;
self
}
pub fn finish(self) -> Result<Command, CommandError> {
let mut commands = Vec::new();
let mut current_command = None;
let mut command_index = 0;
let is_list = self.1;
for part in self.0 {
match part {
CommandPart::Command(mut c) => {
command_index += 1;
validate_command_part(&c).map_err(|mut e| {
if is_list {
e.list_at = Some(command_index - 1);
}
e
})?;
if is_command_list_command(&c) {
return Err(CommandError {
reason: InvalidCommandReason::CommandList,
list_at: if is_list {
Some(command_index - 1)
} else {
None
},
});
}
if let Some(command) = current_command {
commands.push(command);
}
c.make_ascii_lowercase();
current_command = Some(c);
}
CommandPart::Argument(a) => {
let a = validate_argument(&a).map_err(|mut e| {
if is_list {
e.list_at = Some(command_index - 1);
}
e
})?;
let a = escape_argument(a);
let needs_quotes = needs_quotes(&a);
let current = current_command.as_mut().unwrap();
current.reserve(1 + a.len() + if needs_quotes { 2 } else { 0 });
current.push(' ');
if needs_quotes {
current.push('"');
}
current.push_str(&a);
if needs_quotes {
current.push('"');
}
}
}
}
if let Some(c) = current_command {
commands.push(c);
}
Ok(Command(commands))
}
pub fn unwrap(self) -> Command {
self.finish().expect("Invalid command")
}
}
impl TryFrom<&str> for Command {
type Error = CommandError;
fn try_from(c: &str) -> Result<Self, Self::Error> {
let end_of_command_part = c.find(' ');
let command_part = &c[..end_of_command_part.unwrap_or_else(|| c.len())];
validate_command_part(command_part)?;
validate_no_extra_whitespace(c)?;
if let Some(i) = end_of_command_part {
if let Some(space) = c[i..].chars().position(|c| c == '\n') {
return Err(CommandError {
reason: InvalidCommandReason::InvalidCharacter(space, ' '),
list_at: None,
});
}
}
let mut done = c.to_owned();
done[..end_of_command_part.unwrap_or_else(|| c.len())].make_ascii_lowercase();
Ok(Self(vec![done]))
}
}
pub fn escape_argument(argument: &str) -> Cow<'_, str> {
let escape_count = argument.chars().filter(|c| should_escape(*c)).count();
if escape_count == 0 {
return Cow::Borrowed(argument);
}
let mut out = String::with_capacity(argument.len() + escape_count);
for c in argument.chars() {
if should_escape(c) {
out.push('\\');
}
out.push(c);
}
Cow::Owned(out)
}
fn needs_quotes(arg: &str) -> bool {
arg.chars().any(|c| c == ' ')
}
fn should_escape(c: char) -> bool {
c == '\\' || c == '"' || c == '\''
}
fn validate_no_extra_whitespace(command: &str) -> Result<(), CommandError> {
if command.chars().nth(0).unwrap().is_ascii_whitespace()
|| command.chars().last().unwrap().is_ascii_whitespace()
{
Err(CommandError {
reason: InvalidCommandReason::UnncessaryWhitespace,
list_at: None,
})
} else {
Ok(())
}
}
fn validate_command_part(command: &str) -> Result<(), CommandError> {
if command.is_empty() {
return Err(CommandError {
reason: InvalidCommandReason::Empty,
list_at: None,
});
}
validate_no_extra_whitespace(command)?;
if let Some((i, c)) = command
.char_indices()
.find(|(_, c)| !is_valid_command_char(*c))
{
Err(CommandError {
reason: InvalidCommandReason::InvalidCharacter(i, c),
list_at: None,
})
} else {
Ok(())
}
}
fn validate_argument(argument: &str) -> Result<&str, CommandError> {
validate_no_extra_whitespace(argument)?;
match argument.char_indices().find(|(_, c)| *c == '\n') {
None => Ok(argument),
Some((i, c)) => Err(CommandError {
reason: InvalidCommandReason::InvalidCharacter(i, c),
list_at: None,
}),
}
}
fn is_valid_command_char(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_command_list_command(command: &str) -> bool {
command.starts_with("command_list")
}
impl Error for CommandError {}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.reason {
InvalidCommandReason::Empty => write!(f, "Command or command list was empty"),
InvalidCommandReason::InvalidCharacter(i, c) => write!(
f,
"Command contained an invalid character: {:?} at position {}",
c, i
),
InvalidCommandReason::UnncessaryWhitespace => {
write!(f, "Command contained leading or trailing whitespace")
}
InvalidCommandReason::CommandList => {
write!(f, "Command attempted to open or close a command list")
}
}?;
if let Some(i) = self.list_at {
write!(f, " (at command list index {})", i)?;
}
Ok(())
}
}