evorule_cli/commands/
truncate_partial_line.rs1use std::path::{Path, PathBuf};
16
17use clap::Args;
18use evorule_reactor::{chain_step, fact_hash, read_wal_with_hash};
19
20use crate::error::CliError;
21
22#[derive(Debug, Args)]
23pub struct TruncatePartialLine {
24 pub wal_file: PathBuf,
26 #[arg(long)]
28 pub confirm: bool,
29}
30
31pub fn run(cmd: &TruncatePartialLine) -> Result<(), CliError> {
32 let path = &cmd.wal_file;
33 let data = std::fs::read(path).map_err(|e| CliError::other(format!("read: {e}")))?;
34
35 let last_nl = data
36 .iter()
37 .rposition(|&b| b == b'\n')
38 .ok_or_else(|| CliError::other("文件无完整行(无换行符),拒绝处理"))?;
39 let trailing = &data[last_nl + 1..];
40 if trailing.iter().all(|&b| b.is_ascii_whitespace()) || trailing.is_empty() {
41 println!(
42 "文件已以完整行结尾,无需截断(尾随空白 {} 字节属正常)",
43 trailing.len()
44 );
45 return Ok(());
46 }
47 let prefix = &data[..=last_nl];
48 println!(
49 "检测到半行:{} 字节不完整尾段将被截断(前缀保留 {} 字节 / {} 行)",
50 trailing.len(),
51 prefix.len(),
52 prefix.iter().filter(|&&b| b == b'\n').count()
53 );
54
55 let tmp = std::env::temp_dir().join(format!(
57 "evorule_truncate_prefix_{}_{}",
58 std::process::id(),
59 path.file_name()
60 .map(|n| n.to_string_lossy().to_string())
61 .unwrap_or_default()
62 ));
63 std::fs::write(&tmp, prefix).map_err(|e| CliError::other(format!("temp write: {e}")))?;
64 let verify = (|| -> Result<(usize, String), String> {
65 let records = read_wal_with_hash(&tmp).map_err(|e| e.to_string())?;
66 let mut prev = "genesis".to_string();
67 for rec in &records {
68 let content = fact_hash(&rec.fact).map_err(|e| e.to_string())?;
69 if let Some(stored) = &rec.content_hash {
70 if stored != &content {
71 return Err(format!("content_hash mismatch at fact {}", rec.fact.id().0));
72 }
73 }
74 if let Some(stored_prev) = &rec.prev_hash {
75 if stored_prev != &prev {
76 return Err(format!("chain broken at fact {}", rec.fact.id().0));
77 }
78 }
79 let chain = chain_step(&prev, &content);
80 if let Some(stored_chain) = &rec.chain_hash {
81 if stored_chain != &chain {
82 return Err(format!("chain_hash mismatch at fact {}", rec.fact.id().0));
83 }
84 }
85 prev = chain;
86 }
87 Ok((records.len(), prev))
88 })();
89 let _ = std::fs::remove_file(&tmp);
90 match verify {
91 Ok((n, tail)) => println!(
92 "前缀链验证 PASS:{n} 条记录,链尾 {}…",
93 &tail[..16.min(tail.len())]
94 ),
95 Err(e) => {
96 return Err(CliError::other(format!(
97 "前缀链验证 FAIL(截断无济于事,文件存在更早破坏): {e}"
98 )))
99 }
100 }
101
102 if !cmd.confirm {
103 println!(
104 "干跑模式(未落盘)。确认后执行:evorule truncate-partial-line {} --confirm",
105 path.display()
106 );
107 return Ok(());
108 }
109
110 let bak = backup_path(path);
112 std::fs::write(&bak, &data).map_err(|e| CliError::other(format!("backup write: {e}")))?;
113 std::fs::write(path, prefix).map_err(|e| CliError::other(format!("truncate write: {e}")))?;
114 println!(
115 "已截断:{}(备份:{},含被截断半行,供取证)",
116 path.display(),
117 bak.display()
118 );
119 Ok(())
120}
121
122fn backup_path(p: &Path) -> PathBuf {
123 let mut s = p.as_os_str().to_os_string();
124 s.push(".partial.bak");
125 PathBuf::from(s)
126}