use std::io::Write;
use clap::Args;
use ms_codec::CorrectionDetail;
use serde::Serialize;
use crate::error::{CliError, Result};
use crate::parse::read_input;
#[derive(Args, Debug)]
pub struct RepairArgs {
#[arg(long, value_name = "MS1")]
pub ms1: String,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Clone)]
struct RepairDetail {
chunk_index: usize,
original_chunk: String,
corrected_chunk: String,
corrected_positions: Vec<(usize, char, char)>,
}
pub fn run(args: RepairArgs) -> Result<u8> {
let original = read_input(Some(args.ms1.as_str()))?;
let (_tag, _payload, corrections) = ms_codec::decode_with_correction(&original)?;
let (corrected_chunk, corrected_positions) =
reconstruct_corrected(&original, &corrections);
let report = RepairDetail {
chunk_index: 0,
original_chunk: original.clone(),
corrected_chunk: corrected_chunk.clone(),
corrected_positions,
};
let corrected_chunks = vec![corrected_chunk];
let reports = vec![report];
if args.json {
emit_json(&corrected_chunks, &reports)?;
} else {
emit_text(&corrected_chunks, &reports)?;
}
let _ = writeln!(
std::io::stderr(),
"warning: secret material on stdout — consider redirecting (e.g., '> file.txt' or '| age -e ...')"
);
let any_correction = reports.iter().any(|r| !r.corrected_positions.is_empty());
Ok(if any_correction { 5 } else { 0 })
}
fn reconstruct_corrected(
original: &str,
corrections: &[CorrectionDetail],
) -> (String, Vec<(usize, char, char)>) {
let sep_pos = original
.rfind('1')
.expect("ms1 input passed BCH decode; must contain bech32 separator '1'");
let (prefix, rest) = original.split_at(sep_pos);
let mut data_chars: Vec<char> = rest[1..].chars().map(|c| c.to_ascii_lowercase()).collect();
let mut corrected_positions: Vec<(usize, char, char)> =
Vec::with_capacity(corrections.len());
for c in corrections {
if c.position < data_chars.len() {
data_chars[c.position] = c.now;
}
corrected_positions.push((c.position, c.was, c.now));
}
let mut out = String::with_capacity(prefix.len() + 1 + data_chars.len());
out.push_str(&prefix.to_ascii_lowercase());
out.push('1');
for c in &data_chars {
out.push(*c);
}
(out, corrected_positions)
}
fn emit_text(corrected_chunks: &[String], reports: &[RepairDetail]) -> Result<()> {
let any_correction = reports.iter().any(|r| !r.corrected_positions.is_empty());
if any_correction {
println!("# Repair report");
for r in reports {
if r.corrected_positions.is_empty() {
continue;
}
let n = r.corrected_positions.len();
let plural = if n == 1 { "correction" } else { "corrections" };
let mut line = format!("# ms1 chunk {}: {} {} at ", r.chunk_index, n, plural);
for (i, (pos, was, now)) in r.corrected_positions.iter().enumerate() {
if i > 0 {
line.push_str(", ");
}
line.push_str(&format!("position {pos}: '{was}' -> '{now}'"));
}
println!("{line}");
}
}
for chunk in corrected_chunks {
println!("{chunk}");
}
Ok(())
}
#[derive(Serialize)]
struct RepairJson<'a> {
schema_version: &'static str,
kind: &'static str,
corrected_chunks: &'a [String],
repairs: Vec<RepairJsonDetail<'a>>,
}
#[derive(Serialize)]
struct RepairJsonDetail<'a> {
chunk_index: usize,
original_chunk: &'a str,
corrected_chunk: &'a str,
corrected_positions: Vec<RepairJsonPosition>,
}
#[derive(Serialize)]
struct RepairJsonPosition {
position: usize,
was: String,
now: String,
}
fn emit_json(corrected_chunks: &[String], reports: &[RepairDetail]) -> Result<()> {
let envelope = RepairJson {
schema_version: "1",
kind: "ms1",
corrected_chunks,
repairs: reports
.iter()
.filter(|r| !r.corrected_positions.is_empty())
.map(|r| RepairJsonDetail {
chunk_index: r.chunk_index,
original_chunk: &r.original_chunk,
corrected_chunk: &r.corrected_chunk,
corrected_positions: r
.corrected_positions
.iter()
.map(|(p, w, n)| RepairJsonPosition {
position: *p,
was: w.to_string(),
now: n.to_string(),
})
.collect(),
})
.collect(),
};
let body = serde_json::to_string(&envelope)
.map_err(|e| CliError::BadInput(format!("repair JSON serialize: {e}")))?;
println!("{body}");
Ok(())
}