use std::ffi::OsStr;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Result};
use pinentry::PassphraseInput;
use secrecy::SecretString;
pub fn parse_and_split_args(argv: &str) -> Result<(String, Vec<String>)> {
let args = match shell_words::split(argv) {
Ok(args) => args,
Err(_) => return Err(anyhow!("failed to split command-line arguments: {}", argv)),
};
let (command, args) = args
.split_first()
.map(|t| (t.0.to_owned(), t.1.to_owned()))
.ok_or_else(|| anyhow!("missing one or more arguments in command"))?;
Ok((command, args))
}
pub fn run_with_output(command: &str, args: &[&str]) -> Result<String> {
let output = Command::new(command)
.args(args)
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
.map_err(|_| anyhow!("failed to execute command: {}", command))?;
if output.stdout.is_empty() {
return Err(anyhow!("expected output from {}, but none given", command));
}
let mut output = String::from_utf8(output.stdout)?;
if output.ends_with('\n') {
output.pop();
}
Ok(output)
}
pub fn get_password<S: AsRef<OsStr>>(
prompt: Option<&'static str>,
pinentry: S,
) -> Result<SecretString> {
let prompt = prompt.unwrap_or("Password: ");
if let Some(mut input) = PassphraseInput::with_binary(pinentry) {
input
.with_description("Enter your master kbs2 password")
.with_prompt(prompt)
.required("A non-empty password is required")
.interact()
.map_err(|e| anyhow!("pinentry failed: {}", e.to_string()))
} else {
log::debug!("no pinentry binary, falling back on rpassword");
rpassword::prompt_password(prompt)
.map(SecretString::new)
.map_err(|e| anyhow!("password prompt failed: {}", e.to_string()))
}
}
pub fn current_timestamp() -> u64 {
#[allow(clippy::expect_used)]
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("impossible: system time is before the UNIX epoch")
.as_secs()
}
pub fn warn(msg: &str) {
eprintln!("Warn: {}", msg);
}
pub fn read_guarded<P: AsRef<Path>>(path: P, limit: u64) -> Result<Vec<u8>> {
let mut file = File::open(&path)?;
let meta = file.metadata()?;
if meta.len() > limit {
return Err(anyhow!("requested file is suspiciously large, refusing"));
}
let mut buf = Vec::with_capacity(meta.len() as usize);
file.read_to_end(&mut buf)?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
#[test]
fn test_parse_and_split_args() {
{
let (cmd, args) = parse_and_split_args("just-a-command").unwrap();
assert_eq!(cmd, "just-a-command");
assert_eq!(args, Vec::<String>::new());
}
{
let (cmd, args) =
parse_and_split_args("foo -a -ab --c -d=e --f=g bar baz quux").unwrap();
assert_eq!(cmd, "foo");
assert_eq!(
args,
vec!["-a", "-ab", "--c", "-d=e", "--f=g", "bar", "baz", "quux"]
);
}
{
let (cmd, args) = parse_and_split_args("foo 'one arg' \"another arg\" ''").unwrap();
assert_eq!(cmd, "foo");
assert_eq!(args, vec!["one arg", "another arg", ""]);
}
{
let err = parse_and_split_args("some 'bad {syntax").unwrap_err();
assert_eq!(
err.to_string(),
"failed to split command-line arguments: some 'bad {syntax"
);
}
{
let err = parse_and_split_args("").unwrap_err();
assert_eq!(err.to_string(), "missing one or more arguments in command");
}
}
#[test]
fn test_run_with_output() {
{
let output = run_with_output("echo", &["-n", "foo"]).unwrap();
assert_eq!(output, "foo");
}
{
let output = run_with_output("echo", &["foo"]).unwrap();
assert_eq!(output, "foo");
}
{
let err = run_with_output("this-command-should-not-exist", &[]).unwrap_err();
assert_eq!(
err.to_string(),
"failed to execute command: this-command-should-not-exist"
);
}
{
let err = run_with_output("true", &[]).unwrap_err();
assert_eq!(err.to_string(), "expected output from true, but none given");
}
}
#[test]
fn test_current_timestamp() {
{
let ts = current_timestamp();
assert!(ts != 0);
}
{
let ts1 = current_timestamp();
let ts2 = current_timestamp();
assert!(ts2 >= ts1);
}
}
#[test]
fn test_read_guarded() {
{
let mut small = NamedTempFile::new().unwrap();
small.write(b"test").unwrap();
small.flush().unwrap();
let contents = read_guarded(small.path(), 1024);
assert!(contents.is_ok());
assert_eq!(contents.unwrap().as_slice(), b"test");
}
{
let mut toobig = NamedTempFile::new().unwrap();
toobig.write(b"slightlytoobig").unwrap();
toobig.flush().unwrap();
assert!(read_guarded(toobig.path(), 10).is_err());
}
}
}