use rpassword::prompt_password;
use std::collections::HashMap;
use std::env;
use std::fmt::Display;
use std::fs::File;
use std::io::{stdin, BufRead, BufReader, StdinLock};
use std::num::ParseIntError;
use std::os::fd::{FromRawFd, RawFd};
use std::str::FromStr;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("invalid type {0}")]
InvalidType(String),
#[error("{0}")]
EnvVar(#[from] env::VarError),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
FdLiteral(#[from] ParseIntError),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Source {
Pass(String),
Env(std::ffi::OsString),
File(std::path::PathBuf),
Fd(RawFd),
Stdin,
Prompt(String),
}
impl FromStr for Source {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.splitn(2, ':').collect::<Vec<_>>()[..] {
[] => panic!("splitn returned nothing"),
["pass", password] => Self::Pass(password.into()),
["env", var] => Self::Env(var.into()),
["file", path] => Self::File(path.into()),
["fd", fd] => Self::Fd(fd.parse()?),
["stdin"] => Self::Stdin,
["prompt"] => Self::Prompt("Password: ".to_string()),
["prompt", prompt] => Self::Prompt(prompt.into()),
[t, ..] => return Err(Error::InvalidType(t.into())),
})
}
}
impl Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Source::*;
match self {
Pass(password) => write!(f, "pass:{password}"),
Env(var) => {
let var = var.clone().into_string().map_err(|_| std::fmt::Error)?;
write!(f, "env:{var}")
}
File(path) => {
let path = path
.clone()
.into_os_string()
.into_string()
.map_err(|_| std::fmt::Error)?;
write!(f, "file:{path}")
}
Fd(fd) => write!(f, "fd:{fd}"),
Stdin => write!(f, "stdin"),
Prompt(prompt) => write!(f, "prompt:{prompt}"),
}
}
}
#[derive(Default)]
pub struct Reader<'a> {
files: HashMap<std::path::PathBuf, BufReader<File>>,
fds: HashMap<RawFd, BufReader<File>>,
stdin: Option<StdinLock<'a>>,
}
impl Reader<'_> {
pub fn new() -> Self {
Self::default()
}
pub fn read_pass_arg(&mut self, arg: &str) -> Result<String, Error> {
self.read_source(arg.parse()?)
}
pub fn read_source(&mut self, source: Source) -> Result<String, Error> {
Ok(match source {
Source::Pass(password) => password,
Source::Env(var) => env::var(var)?,
Source::File(path) => {
let path = std::fs::canonicalize(path)?;
let f = match self.files.get_mut(&path) {
Some(f) => f,
None => {
self.files
.insert(path.clone(), BufReader::new(File::open(&path)?));
self.files.get_mut(&path).unwrap()
}
};
Self::read_from_bufreader(f)?
}
Source::Fd(fd) => {
let f = match self.fds.get_mut(&fd) {
Some(f) => f,
None => {
self.fds
.insert(fd, BufReader::new(unsafe { File::from_raw_fd(fd) }));
self.fds.get_mut(&fd).unwrap()
}
};
Self::read_from_bufreader(f)?
}
Source::Stdin => {
Self::read_from_bufreader(self.stdin.get_or_insert_with(|| stdin().lock()))?
}
Source::Prompt(prompt) => prompt_password(prompt)?,
})
}
fn read_from_bufreader(r: &mut dyn BufRead) -> Result<String, Error> {
let mut line = String::new();
r.read_line(&mut line)?;
Ok(line.trim_end_matches('\n').into())
}
}
#[cfg(test)]
mod test {
use assert_ok::assert_ok;
use clap::Parser as ClapParser;
use super::*;
#[derive(ClapParser)]
struct ClapCli {
#[arg(short)]
p: Source,
}
fn exercise_clap(arg: &str) -> Source {
assert_ok!(ClapCli::try_parse_from(vec!["test", "-p", arg].into())).p
}
#[test]
fn test_with_clap_derive() {
assert_eq!(exercise_clap("pass:omg"), Source::Pass("omg".into()));
assert_eq!(exercise_clap("env:omg"), Source::Env("omg".into()));
assert_eq!(exercise_clap("file:omg"), Source::File("omg".into()));
assert_eq!(exercise_clap("fd:3"), Source::Fd(3));
assert_eq!(exercise_clap("stdin"), Source::Stdin);
assert_eq!(exercise_clap("prompt:omg"), Source::Prompt("omg".into()));
assert_eq!(exercise_clap("prompt"), Source::Prompt("Password: ".into()));
}
}