use clap::{Parser, Subcommand, ValueEnum};
use log::{debug, error, info, warn};
use lupin::error::{LupinError, Result};
use lupin::operations;
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Debug, Clone, ValueEnum)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
#[derive(Parser, Debug)]
#[command(name = "lupin")]
#[command(version, about, long_about = None)]
#[command(arg_required_else_help = true)]
struct CliArgs {
#[arg(long, value_enum)]
log_level: Option<LogLevel>,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Embed {
src: PathBuf,
payload: PathBuf,
output: PathBuf,
},
Extract {
src: PathBuf,
output: PathBuf,
},
}
fn init_logging(log_level: Option<LogLevel>, verbose: bool, quiet: bool) {
let level = if let Some(ref level) = log_level {
match level {
LogLevel::Error => log::LevelFilter::Error,
LogLevel::Warn => log::LevelFilter::Warn,
LogLevel::Info => log::LevelFilter::Info,
LogLevel::Debug => log::LevelFilter::Debug,
}
} else if quiet {
log::LevelFilter::Error
} else if verbose {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
};
TermLogger::init(
level,
Config::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
)
.ok();
if log_level.is_some() && (verbose || quiet) {
warn!("Explicit --log-level overrides --verbose and --quiet flags");
}
}
fn format_size(size: usize) -> String {
if size < 1024 {
format!("{} B", size)
} else if size < 1024 * 1024 {
format!("{:.2} KiB", size as f64 / 1024.0)
} else if size < 1024 * 1024 * 1024 {
format!("{:.2} MiB", size as f64 / (1024.0 * 1024.0))
} else {
format!("{:.2} GiB", size as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
fn handle_embed(src: PathBuf, payload: PathBuf, output: PathBuf) -> Result<()> {
debug!("Running command: embed");
debug!(
"Source: {}, Payload: {}, Output: {}",
src.display(),
payload.display(),
output.display()
);
let source_data = fs::read(&src).map_err(|e| LupinError::SourceFileRead {
path: src,
source: e,
})?;
let payload_data = fs::read(&payload).map_err(|e| LupinError::PayloadFileRead {
path: payload,
source: e,
})?;
let (embedded_data, result) = operations::embed(&source_data, &payload_data)?;
fs::write(&output, &embedded_data).map_err(|e| LupinError::OutputFileWrite {
path: output.clone(),
source: e,
})?;
debug!("Using {} engine", result.engine);
info!(
"Embedded payload into {} source → {} output (+{:.0}%)",
format_size(result.source_size),
format_size(result.output_size),
((result.output_size as f64 / result.source_size as f64 - 1.0) * 100.0).round()
);
Ok(())
}
fn handle_extract(src: PathBuf, output: PathBuf) -> Result<()> {
debug!("Running command: extract");
debug!("Source: {}, Output: {}", src.display(), output.display());
let source_data = fs::read(&src).map_err(|e| LupinError::SourceFileRead {
path: src,
source: e,
})?;
let (payload_data, result) = operations::extract(&source_data)?;
let written_to_stdout = output.as_os_str() == "-";
if written_to_stdout {
io::stdout()
.write_all(&payload_data)
.map_err(|e| LupinError::StdoutWrite { source: e })?;
} else {
fs::write(&output, &payload_data).map_err(|e| LupinError::OutputFileWrite {
path: output,
source: e,
})?;
}
debug!("Using {} engine", result.engine);
if written_to_stdout {
debug!("Extracted {} to stdout", format_size(result.payload_size));
} else {
debug!("Extracted {} from source", format_size(result.payload_size));
}
info!("Successfully extracted payload from PDF.");
Ok(())
}
fn main() -> ExitCode {
let args = CliArgs::parse();
let mut forced_quiet = false;
if let Command::Extract { output, .. } = &args.command {
if output.as_os_str() == "-" {
forced_quiet = true; }
}
if forced_quiet {
init_logging(Some(LogLevel::Error), false, true);
} else {
init_logging(args.log_level, args.verbose, args.quiet);
}
debug!("Verbose mode enabled");
let result = match args.command {
Command::Embed {
src,
payload,
output,
} => handle_embed(src, payload, output),
Command::Extract { src, output } => handle_extract(src, output),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
error!("{}", error);
error!("{:?}", error);
ExitCode::FAILURE
}
}
}