Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:

steps:
- uses: actions/checkout@v6
- run: cargo build --verbose
- run: cargo test --verbose

test-nix:
Expand Down
10 changes: 5 additions & 5 deletions src/dec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -115,7 +115,7 @@ where
Ok(())
}

pub async fn dec_file<B: IoBundle>(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}");
})?;
Expand All @@ -135,12 +135,12 @@ pub async fn dec_file<B: IoBundle>(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<B: IoBundle>(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}");
})?;
Expand All @@ -157,7 +157,7 @@ pub async fn dec_fetch<B: IoBundle>(args: FetchArgs, io: B) -> Result<(), ()> {
s,
password,
args.out_file,
B::is_interactive() && !args.silent,
io.is_interactive() && !args.silent,
enc_len
).await
}
8 changes: 4 additions & 4 deletions src/enc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<B: IoBundle>(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}");
})?;
Expand All @@ -33,15 +33,15 @@ pub async fn enc<B: IoBundle>(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()
};
let buf_size = f_in.max_buf_size();
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()
};
Expand Down
36 changes: 9 additions & 27 deletions src/io.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn handle_err(result: Result<(), ()>) -> std::process::ExitCode {
}
}

async fn run_with_io<B: io::IoBundle>(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),
Expand All @@ -35,5 +35,5 @@ async fn run_with_io<B: io::IoBundle>(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
}
25 changes: 15 additions & 10 deletions src/password.rs
Original file line number Diff line number Diff line change
@@ -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<B: IoBundle>(io: B) -> Result<Zeroizing<Vec<u8>>, std::io::Error> {
pub async fn prompt_password(io: IoMode) -> Result<Zeroizing<Vec<u8>>, 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()
}
48 changes: 9 additions & 39 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -180,7 +150,7 @@ async fn end_to_end_chaff() {
silent: true
})
},
EmptyMockStdin
IoMode::TestMockedInput(&[])
).await;

assert_eq!(result, std::process::ExitCode::SUCCESS);
Expand All @@ -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);
Expand Down