#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(missing_docs)]
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use zeroize::Zeroizing;
use stenoxide_core::pipeline::{EmbedPipeline, EmbedReport};
const EXTRACTION_FAILED: &str = "Could not extract the payload.";
const PASSWORD_PROMPT: &str = "Password: ";
#[derive(Parser)]
#[command(name = "stenoxide", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Embed {
#[arg(long, value_name = "PATH")]
input: PathBuf,
#[arg(long, value_name = "PATH")]
output: PathBuf,
},
Extract {
#[arg(long, value_name = "PATH")]
input: PathBuf,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
let outcome = match &cli.command {
Command::Embed { input, output } => run_embed(input, output),
Command::Extract { input } => run_extract(input),
};
match outcome {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
}
}
}
fn read_password() -> Result<Zeroizing<Vec<u8>>, String> {
rpassword::prompt_password(PASSWORD_PROMPT)
.map(|password| Zeroizing::new(password.into_bytes()))
.map_err(|err| format!("Error: could not read the password: {err}"))
}
fn read_plaintext() -> Result<Zeroizing<Vec<u8>>, String> {
let mut plaintext = Zeroizing::new(Vec::new());
io::stdin()
.read_to_end(&mut plaintext)
.map_err(|err| format!("Error: could not read the message from standard input: {err}"))?;
Ok(plaintext)
}
fn run_embed(input: &Path, output: &Path) -> Result<(), String> {
let password = read_password()?;
let plaintext = read_plaintext()?;
if plaintext.is_empty() {
return Err("Error: the message is empty; nothing to hide.".to_string());
}
let report = EmbedPipeline::default_secure()
.embed(input, plaintext, password, output)
.map_err(|err| format!("Error: {err}"))?;
print_report(&report, output);
Ok(())
}
fn print_report(report: &EmbedReport, output: &Path) {
let (width, height) = report.image_dimensions;
println!("Stego image written to {}", output.display());
println!(" Image dimensions: {width}x{height}");
println!(" Pixels modified: {}", report.pixels_modified);
println!(" Payload embedded: {} bytes", report.payload_bytes);
println!(" Effective rate: {:.6} bpp", report.effective_bpp);
}
fn run_extract(input: &Path) -> Result<(), String> {
let password = read_password()?;
let (plaintext, _report) = EmbedPipeline::default_secure()
.extract(input, password)
.map_err(|_| EXTRACTION_FAILED.to_string())?;
io::stdout()
.write_all(plaintext.as_slice())
.and_then(|()| io::stdout().flush())
.map_err(|_| EXTRACTION_FAILED.to_string())
}