serde-cmd 0.1.3

A small library to deserialize commands
Documentation
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#[cfg(feature = "serde")]
use serde::de::{Deserialize, Deserializer, Visitor};
use std::borrow::Cow;
use std::process::Command;
use std::str::CharIndices;

/// A command parsed into arguments that might be borrowed from the source.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cmd<S = String> {
    /// The path of the command to be executed.
    pub path: S,
    /// Optional arguments to the command.
    pub args: Vec<S>,
}

/// A variant of the command that borrows the data and does no copying
/// unless necessary.
pub type CmdBorrowed<'a> = Cmd<Cow<'a, str>>;

/// Iterator over arguments of the command.
#[derive(Clone, Debug)]
pub struct ArgIter<'a> {
    source: &'a str,
    chars: CharIndices<'a>,
}

impl<'a> ArgIter<'a> {
    /// Construct a new argument iterator from the source.
    #[inline]
    pub fn new(source: &'a str) -> Self {
        ArgIter {
            source,
            chars: source.char_indices(),
        }
    }
}

impl<'a> Iterator for ArgIter<'a> {
    type Item = Cow<'a, str>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        #[derive(Clone, Copy)]
        enum State {
            None,
            Quote(char),
        }

        let mut previous;
        let mut initial;
        let mut owned: Option<String> = None;
        loop {
            let (i, ch) = self.chars.next()?;
            if !ch.is_whitespace() {
                previous = ch;
                initial = i;
                break;
            }
        }
        let mut last = self.source.len();

        let mut state = match previous {
            '"' | '\'' => {
                initial += 1;
                State::Quote(previous)
            }
            '\\' => {
                initial += 1;
                State::None
            }
            _ => State::None,
        };
        let initial_state = state;
        let mut state_changes = 0;
        let mut found_whitespace = false;

        for (l, c) in self.chars.by_ref() {
            last = l;

            if (c == '"' || c == '\'') && previous != '\\' {
                match state {
                    State::None => {
                        state_changes += 1;
                        state = State::Quote(c)
                    }
                    State::Quote(q) if c == q => {
                        state_changes += 1;
                        state = State::None
                    }
                    _ => {}
                }
            } else if let State::None = state {
                if c.is_whitespace() {
                    found_whitespace = true;
                    break;
                }
            }

            if c != '\\' || previous == '\\' {
                if let Some(ref mut arg) = owned {
                    arg.push(c);
                }
            }

            if c == '\\' && owned.is_none() {
                owned = Some(self.source[initial..last].to_string());
            }
            previous = c;
        }

        if !found_whitespace && owned.is_none() {
            last = self.source.len();
        };

        match initial_state {
            State::Quote(_) if state_changes == 1 => {
                if let Some(ref mut arg) = owned {
                    arg.pop();
                } else {
                    last -= 1;
                }
            }
            _ => {}
        };

        owned
            .map(|s| s.into())
            .or_else(|| self.source.get(initial..last).map(|s| s.into()))
    }
}

impl<S: AsRef<str>> Cmd<S> {
    /// Turn `Cmd` into an actual command that can be executed.
    #[inline]
    pub fn make_command(&self) -> Command {
        let mut command = Command::new(self.path.as_ref());
        command.args(self.args.iter().map(|arg| arg.as_ref()));
        command
    }
}

impl<'a> From<ArgIter<'a>> for Cmd<Cow<'a, str>> {
    #[inline]
    fn from(mut iter: ArgIter<'a>) -> Self {
        let path = iter.next().unwrap_or(Cow::Borrowed(""));
        let args = iter.collect();
        Cmd { path, args }
    }
}

impl From<ArgIter<'_>> for Cmd {
    #[inline]
    fn from(iter: ArgIter) -> Self {
        let mut iter = iter.map(|s| s.to_string());
        let path = iter.next().unwrap_or_else(|| "".to_string());
        let args = iter.collect();
        Cmd { path, args }
    }
}

#[cfg(feature = "serde")]
impl<'de: 'a, 'a> Deserialize<'de> for Cmd<Cow<'a, str>> {
    #[inline]
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct CommandVisitor;

        impl<'de> Visitor<'de> for CommandVisitor {
            type Value = Cmd<Cow<'de, str>>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "a command string")
            }

            #[inline]
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> {
                Ok(ArgIter::new(v).into())
            }

            #[inline]
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
                let mut iter = ArgIter::new(v).map(|s| Cow::Owned(s.to_string()));
                let path = iter.next().unwrap_or_else(|| "".into());
                let args = iter.collect();
                Ok(Cmd { path, args })
            }
        }

        deserializer.deserialize_string(CommandVisitor)
    }
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Cmd {
    #[inline]
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct CommandVisitor;

        impl<'de> Visitor<'de> for CommandVisitor {
            type Value = Cmd;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "a command string")
            }

            #[inline]
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
                Ok(ArgIter::new(v).into())
            }
        }

        deserializer.deserialize_string(CommandVisitor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_argiter() {
        let source = "echo h \"hello\\\" world\" \"\" \"h\\\"\"";
        let args: Vec<Cow<str>> = ArgIter::new(source).collect();
        assert_eq!(args, &["echo", "h", "hello\" world", "", "h\""]);
    }

    #[test]
    fn test_deserialize() {
        #[derive(Debug, PartialEq, Eq, serde_derive::Deserialize)]
        pub struct Simple<'a> {
            #[serde(borrow)]
            owned: CmdBorrowed<'a>,
            #[serde(borrow)]
            borrowed: CmdBorrowed<'a>,
        }

        let cmd = toml::de::from_str::<Simple>(include_str!("test.toml")).unwrap();

        assert_eq!(
            cmd,
            Simple {
                owned: Cmd {
                    path: "echo".into(),
                    args: vec!["hello world".into()],
                },
                borrowed: Cmd {
                    path: "rm".into(),
                    args: vec!["-rf".into()],
                }
            }
        );
        assert!(matches!(cmd.borrowed.path, Cow::Borrowed(_)));
    }
}