use std::path::{Path, PathBuf};
use clap::Args;
use evorule_reactor::{chain_step, fact_hash, read_wal_with_hash};
use crate::error::CliError;
#[derive(Debug, Args)]
pub struct TruncatePartialLine {
pub wal_file: PathBuf,
#[arg(long)]
pub confirm: bool,
}
pub fn run(cmd: &TruncatePartialLine) -> Result<(), CliError> {
let path = &cmd.wal_file;
let data = std::fs::read(path).map_err(|e| CliError::other(format!("read: {e}")))?;
let last_nl = data
.iter()
.rposition(|&b| b == b'\n')
.ok_or_else(|| CliError::other("文件无完整行(无换行符),拒绝处理"))?;
let trailing = &data[last_nl + 1..];
if trailing.iter().all(|&b| b.is_ascii_whitespace()) || trailing.is_empty() {
println!(
"文件已以完整行结尾,无需截断(尾随空白 {} 字节属正常)",
trailing.len()
);
return Ok(());
}
let prefix = &data[..=last_nl];
println!(
"检测到半行:{} 字节不完整尾段将被截断(前缀保留 {} 字节 / {} 行)",
trailing.len(),
prefix.len(),
prefix.iter().filter(|&&b| b == b'\n').count()
);
let tmp = std::env::temp_dir().join(format!(
"evorule_truncate_prefix_{}_{}",
std::process::id(),
path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
));
std::fs::write(&tmp, prefix).map_err(|e| CliError::other(format!("temp write: {e}")))?;
let verify = (|| -> Result<(usize, String), String> {
let records = read_wal_with_hash(&tmp).map_err(|e| e.to_string())?;
let mut prev = "genesis".to_string();
for rec in &records {
let content = fact_hash(&rec.fact).map_err(|e| e.to_string())?;
if let Some(stored) = &rec.content_hash {
if stored != &content {
return Err(format!("content_hash mismatch at fact {}", rec.fact.id().0));
}
}
if let Some(stored_prev) = &rec.prev_hash {
if stored_prev != &prev {
return Err(format!("chain broken at fact {}", rec.fact.id().0));
}
}
let chain = chain_step(&prev, &content);
if let Some(stored_chain) = &rec.chain_hash {
if stored_chain != &chain {
return Err(format!("chain_hash mismatch at fact {}", rec.fact.id().0));
}
}
prev = chain;
}
Ok((records.len(), prev))
})();
let _ = std::fs::remove_file(&tmp);
match verify {
Ok((n, tail)) => println!(
"前缀链验证 PASS:{n} 条记录,链尾 {}…",
&tail[..16.min(tail.len())]
),
Err(e) => {
return Err(CliError::other(format!(
"前缀链验证 FAIL(截断无济于事,文件存在更早破坏): {e}"
)))
}
}
if !cmd.confirm {
println!(
"干跑模式(未落盘)。确认后执行:evorule truncate-partial-line {} --confirm",
path.display()
);
return Ok(());
}
let bak = backup_path(path);
std::fs::write(&bak, &data).map_err(|e| CliError::other(format!("backup write: {e}")))?;
std::fs::write(path, prefix).map_err(|e| CliError::other(format!("truncate write: {e}")))?;
println!(
"已截断:{}(备份:{},含被截断半行,供取证)",
path.display(),
bak.display()
);
Ok(())
}
fn backup_path(p: &Path) -> PathBuf {
let mut s = p.as_os_str().to_os_string();
s.push(".partial.bak");
PathBuf::from(s)
}