use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use redactor::{RedactionService, RedactionTarget, SecureRedactionStrategy};
#[derive(Parser)]
#[command(name = "redactor")]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, value_name = "FILE")]
input: Option<PathBuf>,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(short, long, value_name = "PATTERN")]
pattern: Vec<String>,
#[arg(long)]
phones: bool,
#[arg(long)]
verizon: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Extract {
#[arg(short, long, value_name = "FILE")]
input: PathBuf,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
},
}
struct RedactionHandler {
service: RedactionService,
verbose: bool,
}
impl RedactionHandler {
fn new(verbose: bool) -> Self {
let strategy = SecureRedactionStrategy::new();
Self {
service: RedactionService::new(Box::new(strategy)),
verbose,
}
}
fn redact(&self, input: &Path, output: &Path, targets: Vec<RedactionTarget>) -> Result<()> {
if !input.exists() {
anyhow::bail!("Input file does not exist: {}", input.display());
}
if targets.is_empty() {
anyhow::bail!("No redaction targets specified. Use --pattern, --phones, or --verizon.");
}
if self.verbose {
println!("Input: {}", input.display());
println!("Output: {}", output.display());
println!("Targets: {} redaction target(s)", targets.len());
}
let result = self
.service
.redact(input, output, &targets)
.with_context(|| "Redaction failed")?;
if self.verbose {
println!("\nRedaction Summary:");
println!(" Pages processed: {}", result.pages_processed);
println!(" Pages modified: {}", result.pages_modified);
println!(" Instances redacted: {}", result.instances_redacted);
println!(
" Secure: {}",
if result.secure {
"Yes"
} else {
"No (visual only)"
}
);
}
if result.instances_redacted > 0 {
println!(
"✓ Successfully redacted {} instance(s) → {}",
result.instances_redacted,
output.display()
);
} else {
println!("⚠ No instances found to redact");
}
Ok(())
}
fn extract(&self, input: &Path, output: Option<&Path>) -> Result<()> {
if !input.exists() {
anyhow::bail!("Input file does not exist: {}", input.display());
}
let text = self
.service
.extract_text(input)
.with_context(|| "Text extraction failed")?;
if let Some(output_path) = output {
std::fs::write(output_path, &text)
.with_context(|| format!("Failed to write to {}", output_path.display()))?;
println!(
"✓ Extracted {} characters → {}",
text.len(),
output_path.display()
);
} else {
println!("{}", text);
}
Ok(())
}
}
fn build_targets(patterns: &[String], phones: bool, verizon: bool) -> Vec<RedactionTarget> {
let mut targets = Vec::new();
if verizon {
targets.push(RedactionTarget::VerizonAccount);
targets.push(RedactionTarget::PhoneNumbers);
targets.push(RedactionTarget::VerizonCallDetails);
}
if phones && !verizon {
targets.push(RedactionTarget::PhoneNumbers);
}
targets.extend(patterns.iter().map(|p| RedactionTarget::Literal(p.clone())));
targets
}
fn main() -> Result<()> {
let cli = Cli::parse();
let handler = RedactionHandler::new(cli.verbose);
match &cli.command {
Some(Commands::Extract { input, output }) => {
handler.extract(input, output.as_deref())?;
}
None => {
let input = cli
.input
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--input is required"))?;
let output = cli
.output
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--output is required"))?;
let targets = build_targets(&cli.pattern, cli.phones, cli.verizon);
handler.redact(input, output, targets)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_target_building() {
let targets = build_targets(&[], false, true);
assert_eq!(targets.len(), 3);
let targets = build_targets(&[String::from("test")], false, false);
assert_eq!(targets.len(), 1);
assert!(matches!(targets[0], RedactionTarget::Literal(_)));
let targets = build_targets(&[], true, false);
assert_eq!(targets.len(), 1);
assert!(matches!(targets[0], RedactionTarget::PhoneNumbers));
}
}