diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c5dc4d..2f56401 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: steps: - uses: actions/checkout@v6 + - run: cargo build --verbose - run: cargo test --verbose test-nix: diff --git a/src/dec.rs b/src/dec.rs index 10bc766..0001998 100644 --- a/src/dec.rs +++ b/src/dec.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use crate::cli::{DecArgs, FetchArgs}; use crate::file::new_async_tempfile; use crate::password::prompt_password; -use crate::io::IoBundle; +use crate::io::IoMode; use crate::{DEFINITE_BAR_STYLE, INDEFINITE_BAR_STYLE, BYTES_PER_POLL}; const SPINNER_STYLE: &str = "{spinner} deriving decryption key"; @@ -115,7 +115,7 @@ where Ok(()) } -pub async fn dec_file(args: DecArgs, io: B) -> Result<(), ()> { +pub async fn dec_file(args: DecArgs, io: IoMode) -> Result<(), ()> { let password = prompt_password(io).await.map_err(|e| { eprintln!("failed to read password interactively: {e}"); })?; @@ -135,12 +135,12 @@ pub async fn dec_file(args: DecArgs, io: B) -> Result<(), ()> { s, password, args.out_file, - B::is_interactive() && !args.silent, + io.is_interactive() && !args.silent, Some(f_in_metadata.len()) ).await } -pub async fn dec_fetch(args: FetchArgs, io: B) -> Result<(), ()> { +pub async fn dec_fetch(args: FetchArgs, io: IoMode) -> Result<(), ()> { let password = prompt_password(io).await.map_err(|e| { eprintln!("failed to read password interactively: {e}"); })?; @@ -157,7 +157,7 @@ pub async fn dec_fetch(args: FetchArgs, io: B) -> Result<(), ()> { s, password, args.out_file, - B::is_interactive() && !args.silent, + io.is_interactive() && !args.silent, enc_len ).await } diff --git a/src/enc.rs b/src/enc.rs index d702679..5efa3ef 100644 --- a/src/enc.rs +++ b/src/enc.rs @@ -5,12 +5,12 @@ use rand::rngs::SysRng; use indicatif::{ProgressBar, ProgressStyle}; use crate::cli::EncArgs; use crate::password::prompt_password; -use crate::io::IoBundle; +use crate::io::IoMode; use crate::{DEFINITE_BAR_STYLE, BYTES_PER_POLL}; const SPINNER_STYLE: &str = "{spinner} deriving encryption key"; -pub async fn enc(args: EncArgs, io: B) -> Result<(), ()> { +pub async fn enc(args: EncArgs, io: IoMode) -> Result<(), ()> { let password = prompt_password(io).await.map_err(|e| { eprintln!("failed to read password interactively: {e}"); })?; @@ -33,7 +33,7 @@ pub async fn enc(args: EncArgs, io: B) -> Result<(), ()> { return Ok(()); } - let progress = match B::is_interactive() && !args.silent { + let progress = match io.is_interactive() && !args.silent { true => ProgressBar::new(f_in_len), false => ProgressBar::hidden() }; @@ -41,7 +41,7 @@ pub async fn enc(args: EncArgs, io: B) -> Result<(), ()> { let progress_read = progress.wrap_async_read(f_in); let s = tokio_util::io::ReaderStream::with_capacity(progress_read, buf_size); let mut enc = tokio::task::spawn_blocking(move || { - let spinner = match B::is_interactive() && !args.silent { + let spinner = match io.is_interactive() && !args.silent { true => ProgressBar::new_spinner(), false => ProgressBar::hidden() }; diff --git a/src/io.rs b/src/io.rs index 9d536be..f14b8d7 100644 --- a/src/io.rs +++ b/src/io.rs @@ -1,31 +1,13 @@ -pub trait IoBundle: Send + 'static { - type IoRead: std::io::BufRead; - type IoWrite: std::io::Write; - - /// if this is `true` then all other methods are `unimplemented!()` and will panic if called - fn is_interactive() -> bool { - false - } - - fn get_bufread(&self) -> Self::IoRead; - fn get_write(&self) -> Self::IoWrite; +#[derive(Debug, Clone, Copy)] +pub enum IoMode { + Interactive, + #[cfg(test)] + TestMockedInput(&'static [u8]) } -pub struct InteractiveIo; - -impl IoBundle for InteractiveIo { - type IoRead = std::io::Empty; - type IoWrite = std::io::Sink; - - fn is_interactive() -> bool { - true - } - - fn get_bufread(&self) -> Self::IoRead { - unimplemented!() - } - - fn get_write(&self) -> Self::IoWrite { - unimplemented!() +impl IoMode { + #[inline] + pub fn is_interactive(&self) -> bool { + matches!(self, IoMode::Interactive) } } diff --git a/src/lib.rs b/src/lib.rs index cac51b3..f15291d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ fn handle_err(result: Result<(), ()>) -> std::process::ExitCode { } } -async fn run_with_io(cli: cli::Cli, io: B) -> std::process::ExitCode { +async fn run_with_io(cli: cli::Cli, io: io::IoMode) -> std::process::ExitCode { match cli.command { cli::Command::Enc(args) => handle_err(enc::enc(args, io).await), cli::Command::Dec(args) => handle_err(dec::dec_file(args, io).await), @@ -35,5 +35,5 @@ async fn run_with_io(cli: cli::Cli, io: B) -> std::process::Exi } pub async fn run(cli: cli::Cli) -> std::process::ExitCode { - run_with_io(cli, io::InteractiveIo).await + run_with_io(cli, io::IoMode::Interactive).await } diff --git a/src/password.rs b/src/password.rs index 1de6164..e1b2b54 100644 --- a/src/password.rs +++ b/src/password.rs @@ -1,15 +1,20 @@ use zeroize::Zeroizing; -use crate::io::IoBundle; +use crate::io::IoMode; -const PASSWORD_PROMPT: &str = "password: "; - -pub async fn prompt_password(io: B) -> Result>, std::io::Error> { +pub async fn prompt_password(io: IoMode) -> Result>, std::io::Error> { tokio::task::spawn_blocking(move || { - match B::is_interactive() { - true => rpassword::prompt_password(PASSWORD_PROMPT), - false => { - rpassword::prompt_password_from_bufread(&mut io.get_bufread(), &mut io.get_write(), PASSWORD_PROMPT) - } - }.map(String::into_bytes).map(Zeroizing::new) + let builder = rpassword::ConfigBuilder::new(); + + let config: rpassword::Config = match io { + IoMode::Interactive => builder.build(), + #[cfg(test)] + IoMode::TestMockedInput(mocked_password) => builder + .input_data(mocked_password) + .output_discard() + .build() + }; + + rpassword::prompt_password_with_config("password: ", config) + .map(String::into_bytes).map(Zeroizing::new) }).await.unwrap() } diff --git a/src/tests.rs b/src/tests.rs index 79b1d2d..b34e2c5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,41 +1,11 @@ use rand::{SeedableRng, TryRng}; use wiremock::{MockServer, Mock, ResponseTemplate, matchers::method}; use crate::cli::{Cli, Command, EncArgs, DecArgs, FetchArgs, ChaffArgs}; -use crate::io::IoBundle; +use crate::io::IoMode; use crate::run_with_io; const RNG_SEED: u64 = 12345678; -struct MockStdin(&'static str); - -impl IoBundle for MockStdin { - type IoRead = &'static [u8]; - type IoWrite = std::io::Sink; - - fn get_bufread(&self) -> Self::IoRead { - self.0.as_bytes() - } - - fn get_write(&self) -> Self::IoWrite { - std::io::sink() - } -} - -struct EmptyMockStdin; - -impl IoBundle for EmptyMockStdin { - type IoRead = std::io::Empty; - type IoWrite = std::io::Sink; - - fn get_bufread(&self) -> Self::IoRead { - std::io::empty() - } - - fn get_write(&self) -> Self::IoWrite { - std::io::sink() - } -} - #[tokio::test] async fn end_to_end_file() { let mut rng = rand::rngs::SmallRng::seed_from_u64(RNG_SEED); @@ -56,7 +26,7 @@ async fn end_to_end_file() { silent: true }) }, - MockStdin("hunter2\n") + IoMode::TestMockedInput(b"hunter2") ).await; assert_eq!(result, std::process::ExitCode::SUCCESS); @@ -69,7 +39,7 @@ async fn end_to_end_file() { silent: true }) }, - MockStdin("hunter2\n") + IoMode::TestMockedInput(b"hunter2") ).await; assert_eq!(result, std::process::ExitCode::SUCCESS); @@ -86,7 +56,7 @@ async fn end_to_end_file() { silent: true }) }, - MockStdin("not_hunter2\n") + IoMode::TestMockedInput(b"not_hunter2") ).await; assert_eq!(result, std::process::ExitCode::FAILURE); @@ -115,7 +85,7 @@ async fn end_to_end_fetch() { silent: true }) }, - MockStdin("hunter2\n") + IoMode::TestMockedInput(b"hunter2") ).await; assert_eq!(result, std::process::ExitCode::SUCCESS); @@ -139,7 +109,7 @@ async fn end_to_end_fetch() { silent: true }) }, - MockStdin("hunter2\n") + IoMode::TestMockedInput(b"hunter2") ).await; assert_eq!(result, std::process::ExitCode::SUCCESS); @@ -156,7 +126,7 @@ async fn end_to_end_fetch() { silent: true }) }, - MockStdin("not_hunter2\n") + IoMode::TestMockedInput(b"not_hunter2") ).await; assert_eq!(result, std::process::ExitCode::FAILURE); @@ -180,7 +150,7 @@ async fn end_to_end_chaff() { silent: true }) }, - EmptyMockStdin + IoMode::TestMockedInput(&[]) ).await; assert_eq!(result, std::process::ExitCode::SUCCESS); @@ -193,7 +163,7 @@ async fn end_to_end_chaff() { silent: true }) }, - MockStdin("hunter2\n") + IoMode::TestMockedInput(b"hunter2") ).await; assert_eq!(result, std::process::ExitCode::FAILURE);