fuzzel-pass 0.1.2

A password-store frontend for auto-typing passwords
use std::io::Write;
use std::{io::Read, process::Stdio};

use crate::error::Error;

/// Type alias to avoid repeating the usage of `crate::error::Error`
/// in return values
pub type Result<T> = core::result::Result<T, Error>;

/// Provides a way to execute simple external programs with parameters
/// and even piping input spawned  child process into it.
///
/// It should be wrapped into other structure.
///
/// See [`shell`] to learn about an easy way to implement a wrapping structure
/// around [`Shell`].
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Shell {
    /// The executable to be called
    exe: String,
}

impl Shell {
    /// Create a [`Shell`] instance holding a [`String`] representing the
    /// path to the executable to be called.
    ///
    /// [`String`]: std::string::String
    pub const fn new(exe: String) -> Self {
        Self { exe }
    }

    /// Spawns a child process calling [`Self::exe`] with `args`. Upon successful
    /// execution of the child process, will return it's stdout as a [`String`].
    ///
    /// # Errors
    ///
    /// May return a [`Error::Io`] if for any reason the child process couldn't be
    /// spawned, or it's stdout fails to be read from.
    ///
    /// Whenever the program ends properly but with a failure status, will return
    /// [`Error::ChildExitStatusFailed`].
    ///
    /// * `args` - The command line arguments to pass to the executable
    ///
    /// [`String`]: std::string::String
    /// [`Error::Io`]: crate::error::Error::Io
    /// [`Error::ChildExitStatusFailed`]: crate::error::Error::ChildExitStatusFailed
    pub fn exec(&self, args: Vec<String>) -> Result<String> {
        self.exec_with_pipe("", args)
    }

    /// Spawns a child process calling [`Self::exe`] with `args` and piping
    /// `input` on it's stdin. Upon successful execution of the child process,
    /// will return it's stdout as a [`&str`].
    ///
    /// # Errors
    ///
    /// May return a [`Error::Io`] if for any reason the child process couldn't be
    /// spawned, or it's stdin fails to be written in or it's stdout fails to be
    /// read from.
    ///
    /// Whenever the program ends properly but with a failure status, will return
    /// [`Error::ChildExitStatusFailed`].
    ///
    /// * `input` - The input of the program to pipe in the chile process
    /// * `args` - The command line arguments to pass to the executable
    ///
    /// [`&str`]: std::str
    /// [`Error::Io`]: crate::error::Error::Io
    /// [`Error::ChildExitStatusFailed`]: crate::error::Error::ChildExitStatusFailed
    pub fn exec_with_pipe(&self, input: &str, args: Vec<String>) -> Result<String> {
        let cfg = if input.is_empty() {
            Stdio::null()
        } else {
            Stdio::piped()
        };

        let program = self.exe.clone();

        let mut process = std::process::Command::new(program.as_str())
            .args(args)
            .stdin(cfg)
            .stdout(Stdio::piped())
            .spawn()
            .map_err(Error::Io)?;

        if !input.is_empty()
            && let Some(mut stdin) = process.stdin.take()
        {
            stdin.write_all(input.as_bytes()).map_err(Error::Io)?;
        }

        let output = if let Some(mut stdout) = process.stdout.take() {
            let mut buf = String::new();
            stdout.read_to_string(&mut buf).map_err(Error::Io)?;
            buf
        } else {
            String::default()
        };

        let status = process.wait().map_err(Error::Io)?;

        if !status.success() {
            return Err(Error::ChildExitStatusFailed(
                program,
                status.code().unwrap_or(1),
            ));
        }

        Ok(output)
    }
}

/// Provides an easy way to create wrapping structure around
/// [`Shell`].
///
/// Will also automatically implement [`Default`] trait
/// for the created provided with a given path tho the
/// executable to call with the spawning process.
///
/// Afterward, one should create function to call
/// [`Shell::exec`] or [`Shell::exec_with_pipe`] functions
/// on `self.inner`.
///
/// It will also add an impl for test configuration allowing
/// to easily change the inner shell with an `new` function.
///
/// [`Default`]: std::default::Default
///
/// * `StructName` - The name of the new structure
/// * `exe_path` - The path to the executable
macro_rules! shell {
    ($StructName:ident, $exe_path:expr) => {
        #[allow(unused_imports)]
        use crate::shell::Shell;

        #[doc = concat!("A wrapper of [`Shell`] around `", $exe_path, "`

[`Shell`]: crate::shell::Shell")]
        #[derive(Debug, PartialEq, Eq, Clone)]
        pub struct $StructName {
            inner: Shell,
        }

        impl<'a> Default for $StructName {
            #[doc = concat!("Returns an instance of [`", stringify!($StructName), "`]")]
            fn default() -> Self {
                Self {
                    inner: Shell::new(String::from($exe_path)),
                }
            }
        }

        #[cfg(test)]
        impl $StructName {
            pub fn new(exe: String) -> Self {
                Self {
                    inner: Shell::new(exe),
                }
            }
        }
    };
}

#[cfg(test)]
mod test {
    use super::*;
    use std::io::ErrorKind;

    #[test]
    fn test_exec_error_not_found() {
        let shell = Shell::new(String::from("test/scripts/nothing"));
        let result = shell.exec(vec![]);
        assert!(result.is_err());
        if let Err(Error::Io(err)) = result {
            assert_eq!(ErrorKind::NotFound, err.kind());
        }
    }

    #[test]
    fn test_exec_error_failure() {
        let shell = Shell::new(String::from("/usr/bin/false"));
        let result = shell.exec(vec![]);
        assert!(result.is_err());
        if let Err(err) = result {
            assert_eq!(
                Error::ChildExitStatusFailed(String::from("/usr/bin/false"), 1),
                err
            );
        }
    }

    #[test]
    fn test_exec_with_pipe() {
        let wc = Shell::new(String::from("wc"));
        let result = wc.exec_with_pipe("foo bar baz", vec!["-w".into()]);
        match result {
            Ok(output) => assert_eq!("3\n", output.as_str()),
            Err(error) => panic!("{error:?}"),
        }
    }
}

/// This module provides [`Fuzzel`], a generated wrapper around
/// `/usr/bin/fuzzel` usning [`shell`]
///
/// [`Fuzzel`]: crate::shell::fuzzel::Fuzzel
pub mod fuzzel;

/// This module provides [`Pass`], a generated wrapper around
/// `/usr/bin/pass` usning [`shell`]
///
/// [`Pass`]: crate::shell::pass::Pass
pub mod pass;

/// This module provides [`Wtype`], a generated wrapper around
/// `/usr/bin/wtype` usning [`shell`]
///
/// [`Wtype`]: crate::shell::wtype::Wtype
pub mod wtype;