use std::fmt;
use clap::Parser;
use console::{Style, style};
use magic_crypt::{MagicCryptTrait, new_magic_crypt};
use similar::{ChangeTag, TextDiff};
struct Line(Option<usize>);
impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}
#[derive(Parser, Debug)]
#[command(
version,
about = "Secrets CLI",
long_about = "CLI tool to encode/decode secrets files"
)]
struct Args {
#[arg(short, long)]
filepath: String,
#[arg(short, long)]
text: Option<String>,
#[arg(long)]
compare: Option<String>,
#[arg(short, long, default_value = "secret")]
password: String,
#[arg(short, long, default_value_t = false, action = clap::ArgAction::SetTrue)]
copy: bool,
#[arg(short, long, default_value_t = false, action = clap::ArgAction::SetTrue)]
overwrite: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let mcrypt = new_magic_crypt!(args.password, 256);
if let Some(compare_path) = args.compare {
let secrets_content = std::fs::read_to_string(&args.filepath)
.expect(format!("Failed to read {}", args.filepath).as_str())
.replace("\r\n", "\n");
let compare_content = std::fs::read_to_string(&compare_path)
.expect(format!("Failed to read {}", compare_path).as_str())
.replace("\r\n", "\n");
let diff = TextDiff::from_lines(&secrets_content, &compare_content);
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
println!("{:-^1$}", "-", 80);
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new().dim()),
};
print!(
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
);
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
print!("{}", s.apply_to(value).underlined().on_black());
} else {
print!("{}", s.apply_to(value));
}
}
if change.missing_newline() {
println!();
}
}
}
}
}
else if let Some(text) = args.text {
let decrypted_data = mcrypt
.decrypt_base64_to_bytes(text)
.expect("Failed to decrypt base64 data");
if std::path::Path::new(&args.filepath).exists() {
if args.overwrite {
println!("Overwriting existing {}", args.filepath);
} else {
let backup_path = format!("{}.bak", args.filepath);
std::fs::rename(&args.filepath, &backup_path)
.expect(format!("Failed to rename existing file to {}", backup_path).as_str());
println!("Created backup {}", backup_path);
}
}
std::fs::write(&args.filepath, &decrypted_data)
.expect(format!("Failed to write decrypted data to {}", args.filepath).as_str());
println!("Decoded to {}", args.filepath);
}
else {
let buffer = std::fs::read(&args.filepath)
.expect(format!("Failed to read {}", args.filepath).as_str());
let encoded = mcrypt.encrypt_bytes_to_base64(&buffer);
println!("{}", encoded);
if args.copy {
let mut clipboard = arboard::Clipboard::new().expect("Failed to open clipboard");
clipboard
.set_text(encoded.clone())
.expect("Failed to copy to clipboard");
println!("Copied to clipboard");
}
}
Ok(())
}