use clap::Args;
use crate::error::CliError;
const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
#[derive(Args, Debug)]
pub struct RepairArgs {
#[arg(required = true, num_args = 1..)]
pub md1_strings: Vec<String>,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Clone)]
struct RepairDetail {
chunk_index: usize,
#[allow(dead_code)]
original_chunk: String,
corrected_chunk: String,
corrected_positions: Vec<(usize, char, char)>,
}
fn read_md1_strings(args: &[String]) -> Result<Vec<String>, CliError> {
let mut out = Vec::with_capacity(args.len());
let mut consumed_stdin = false;
for a in args {
if a == "-" && !consumed_stdin {
consumed_stdin = true;
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)
.map_err(|e| CliError::BadArg(format!("stdin read: {e}")))?;
for line in buf.lines() {
let s = line.trim();
if !s.is_empty() {
out.push(s.to_string());
}
}
} else if a == "-" {
} else {
out.push(a.clone());
}
}
if out.is_empty() {
return Err(CliError::BadArg(
"expected at least one md1 string (positional or via stdin with '-')".into(),
));
}
Ok(out)
}
pub fn run(args: RepairArgs) -> Result<u8, CliError> {
let strings = read_md1_strings(&args.md1_strings)?;
let str_refs: Vec<&str> = strings.iter().map(String::as_str).collect();
let (_descriptor, details) = match md_codec::decode_with_correction(&str_refs) {
Ok(t) => t,
Err(e) => {
eprintln!("md: repair: {e}");
return Ok(2);
}
};
let mut reports: Vec<RepairDetail> = Vec::with_capacity(strings.len());
for (idx, original) in strings.iter().enumerate() {
let mut positions: Vec<(usize, char, char)> = details
.iter()
.filter(|d| d.chunk_index == idx)
.map(|d| (d.position, d.was, d.now))
.collect();
positions.sort_by_key(|(p, _, _)| *p);
let corrected = apply_corrections(original, &positions);
reports.push(RepairDetail {
chunk_index: idx,
original_chunk: original.clone(),
corrected_chunk: corrected,
corrected_positions: positions,
});
}
let any_correction = reports.iter().any(|r| !r.corrected_positions.is_empty());
let corrected_chunks: Vec<String> =
reports.iter().map(|r| r.corrected_chunk.clone()).collect();
if args.json {
emit_json(&corrected_chunks, &reports)?;
} else {
emit_text(&corrected_chunks, &reports);
}
Ok(if any_correction { 5 } else { 0 })
}
fn apply_corrections(original: &str, positions: &[(usize, char, char)]) -> String {
let hrp_len = 3; let mut chars: Vec<char> = original.chars().collect();
for &(pos, _was, now) in positions {
let abs_idx = hrp_len + pos;
if abs_idx < chars.len() {
chars[abs_idx] = now;
}
}
chars.iter().collect()
}
fn emit_text(corrected_chunks: &[String], reports: &[RepairDetail]) {
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!("# md1 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}");
}
let _ = CODEX32_ALPHABET;
}
#[cfg(feature = "json")]
#[derive(serde::Serialize)]
struct RepairJson<'a> {
schema_version: &'static str,
kind: &'static str,
corrected_chunks: &'a [String],
repairs: Vec<RepairJsonDetail<'a>>,
}
#[cfg(feature = "json")]
#[derive(serde::Serialize)]
struct RepairJsonDetail<'a> {
chunk_index: usize,
original_chunk: &'a str,
corrected_chunk: &'a str,
corrected_positions: Vec<RepairJsonPosition>,
}
#[cfg(feature = "json")]
#[derive(serde::Serialize)]
struct RepairJsonPosition {
position: usize,
was: String,
now: String,
}
#[cfg(feature = "json")]
fn emit_json(corrected_chunks: &[String], reports: &[RepairDetail]) -> Result<(), CliError> {
let envelope = RepairJson {
schema_version: "1",
kind: "md1",
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::BadArg(format!("repair JSON serialize: {e}")))?;
println!("{body}");
Ok(())
}
#[cfg(not(feature = "json"))]
fn emit_json(_corrected_chunks: &[String], _reports: &[RepairDetail]) -> Result<(), CliError> {
Err(CliError::BadArg(
"--json requires the `json` feature (rebuild with --features json)".into(),
))
}