use crate::cli::{RupassArgs, StdioInterface, UserInterface, parse_args};
use crate::core::app_errors::GenerationError;
use crate::core::utils::fallback_translation;
use crate::crypto::global_rng::GlobalRngStream;
use crate::password::FlowReport;
use crate::{
exit_code_for_error, generate_password_flow_with_min_score, get_global_rng, initialize_bundle,
};
use fluent::{FluentArgs, FluentBundle, FluentResource};
use std::process::ExitCode;
pub struct ReportOutput<'a> {
pub stdout: Vec<&'a str>,
pub stderr: Vec<String>,
}
pub async fn run_cli() -> ExitCode {
let args = parse_args();
run_cli_with_args(args).await
}
async fn run_cli_with_args(args: RupassArgs) -> ExitCode {
let bundle = match init_bundle_or_exit(&args) {
Ok(bundle) => bundle,
Err(code) => return code,
};
let mut ui = StdioInterface::default();
let min_score = resolve_min_score(&args);
let mut rng_stream = match init_rng_stream_or_exit() {
Ok(stream) => stream,
Err(code) => return code,
};
match run_generation_flow(&mut ui, &bundle, &args, min_score, &mut rng_stream).await {
Ok(report) => render_report_or_exit(&mut ui, &report, &args, &bundle).await,
Err(e) => handle_generation_error(&e, &args, &bundle, min_score),
}
}
fn init_bundle_or_exit(
args: &RupassArgs,
) -> std::result::Result<FluentBundle<FluentResource>, ExitCode> {
match initialize_bundle(args) {
Ok(bundle) => Ok(bundle),
Err(e) => {
eprintln!("{e}");
Err(exit_code_from_i32(exit_code_for_error(&e)))
}
}
}
fn init_rng_stream_or_exit() -> std::result::Result<GlobalRngStream, ExitCode> {
match get_global_rng() {
Ok(rng) => Ok(rng.stream()),
Err(e) => {
eprintln!("{e}");
Err(exit_code_from_i32(exit_code_for_error(&e)))
}
}
}
async fn run_generation_flow(
ui: &mut dyn UserInterface,
bundle: &FluentBundle<FluentResource>,
args: &RupassArgs,
min_score: u8,
rng_stream: &mut impl crate::crypto::global_rng::ByteStream,
) -> Result<FlowReport, GenerationError> {
generate_password_flow_with_min_score(ui, bundle, args, min_score, rng_stream).await
}
async fn render_report_or_exit(
ui: &mut dyn UserInterface,
report: &FlowReport,
args: &RupassArgs,
bundle: &FluentBundle<FluentResource>,
) -> ExitCode {
match render_report(ui, report, args, bundle).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{e}");
exit_code_from_i32(exit_code_for_error(&e))
}
}
}
fn handle_generation_error(
err: &GenerationError,
args: &RupassArgs,
bundle: &FluentBundle<FluentResource>,
min_score: u8,
) -> ExitCode {
let code = exit_code_for_error(err);
let err_msg = format_generation_error(err, args, bundle, min_score);
eprintln!("{err_msg}");
exit_code_from_i32(code)
}
pub fn build_report_output<'a>(
report: &'a FlowReport,
args: &RupassArgs,
bundle: &FluentBundle<FluentResource>,
) -> ReportOutput<'a> {
let mut stdout: Vec<&'a str> = Vec::new();
if args.quiet {
stdout.push(report.password.as_str());
} else {
if let Some(header) = report.header.as_deref() {
stdout.push(header);
}
stdout.push(report.password.as_str());
if report.show_blank_line {
stdout.push("");
}
if let Some(line) = report.strength_line.as_deref() {
stdout.push(line);
}
}
let stderr = report
.warnings
.iter()
.map(|warning| warning.to_message(bundle))
.collect();
ReportOutput { stdout, stderr }
}
async fn render_report(
ui: &mut dyn UserInterface,
report: &FlowReport,
args: &RupassArgs,
bundle: &FluentBundle<FluentResource>,
) -> crate::core::app_errors::Result<()> {
let output = build_report_output(report, args, bundle);
for line in output.stdout {
ui.print(line).await?;
}
for warning in output.stderr {
eprintln!("{warning}");
}
Ok(())
}
enum UserFacingError {
StrictTargetUnmet {
min_score: u8,
timeout_ms: u64,
quiet: bool,
},
NoCharacterSet,
InvalidCharset(String),
Other(String),
}
impl UserFacingError {
fn from_generation_error(err: &GenerationError, args: &RupassArgs, min_score: u8) -> Self {
match err {
GenerationError::StrictTargetUnmet => UserFacingError::StrictTargetUnmet {
min_score,
timeout_ms: args.timeout_ms,
quiet: args.quiet,
},
GenerationError::NoCharacterSet => UserFacingError::NoCharacterSet,
GenerationError::InvalidCharset(detail) => {
UserFacingError::InvalidCharset(detail.clone())
}
_ => UserFacingError::Other(err.to_string()),
}
}
fn to_message(&self, bundle: &FluentBundle<FluentResource>) -> String {
match self {
UserFacingError::StrictTargetUnmet {
min_score,
timeout_ms,
quiet,
} => {
let default_msg = format!(
"Error: Could not reach target score {} within {} ms.",
min_score, timeout_ms
);
if *quiet {
return default_msg;
}
let mut eargs = FluentArgs::new();
eargs.set("targetScore", *min_score as i64);
eargs.set("budgetMs", *timeout_ms as i64);
fallback_translation(
bundle,
"error_target_unmet_strict",
&default_msg,
Some(&eargs),
)
}
UserFacingError::NoCharacterSet => fallback_translation(
bundle,
"error_no_charset_selected",
"Error: No character set selected.",
None,
),
UserFacingError::InvalidCharset(detail) => {
let default_msg = format!(
"Error: The selected character set cannot produce a password of the requested length within the byte-length limit ({}).",
detail
);
let mut eargs = FluentArgs::new();
eargs.set("detail", detail.as_str());
fallback_translation(
bundle,
"error_infeasible_charset",
&default_msg,
Some(&eargs),
)
}
UserFacingError::Other(msg) => msg.clone(),
}
}
}
pub fn format_generation_error(
err: &GenerationError,
args: &RupassArgs,
bundle: &FluentBundle<FluentResource>,
min_score: u8,
) -> String {
let user_error = UserFacingError::from_generation_error(err, args, min_score);
user_error.to_message(bundle)
}
#[cfg(not(test))]
fn resolve_min_score(args: &RupassArgs) -> u8 {
args.min_score
}
#[cfg(test)]
fn resolve_min_score(args: &RupassArgs) -> u8 {
let override_raw = std::env::var("RUPASS_TEST_MIN_SCORE").ok();
resolve_min_score_from_override(args, override_raw.as_deref())
}
#[cfg(test)]
fn resolve_min_score_from_override(args: &RupassArgs, override_raw: Option<&str>) -> u8 {
match override_raw.and_then(parse_test_min_score_override) {
Some(score) => score,
None => args.min_score,
}
}
#[cfg(test)]
fn parse_test_min_score_override(raw: &str) -> Option<u8> {
let parsed = match raw.trim().parse::<u8>() {
Ok(value) => value,
Err(_) => return None,
};
if parsed <= 4 { Some(parsed) } else { None }
}
fn exit_code_from_i32(code: i32) -> ExitCode {
let code_u8 = if (0..=u8::MAX as i32).contains(&code) {
code as u8
} else {
1
};
ExitCode::from(code_u8)
}
#[cfg(test)]
#[path = "../../tests/unit/reporting_tests.rs"]
mod reporting_tests;