1use std::fs::{File, OpenOptions};
43use std::io::{BufRead, BufReader, Write};
44use std::path::{Path, PathBuf};
45
46use serde::{Deserialize, Serialize};
47
48use crate::delegation::Delegation;
49use crate::error::CoreError;
50use crate::gate::DenyReason;
51use crate::intent::SpendIntent;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum WalDecision {
57 Allow,
58 Deny,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(tag = "kind", rename_all = "snake_case")]
64pub enum WalRecord {
65 RegisterDelegation { ts: u64, delegation: Delegation },
67 Revoke { ts: u64, delegation_id: String },
69 Decide {
71 ts: u64,
72 decision: WalDecision,
73 delegation_id: String,
74 intent: SpendIntent,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 reason: Option<DenyReason>,
78 budget_after_cents: u64,
80 },
81}
82
83impl WalRecord {
84 pub fn ts(&self) -> u64 {
86 match self {
87 WalRecord::RegisterDelegation { ts, .. }
88 | WalRecord::Revoke { ts, .. }
89 | WalRecord::Decide { ts, .. } => *ts,
90 }
91 }
92
93 pub fn kind(&self) -> &'static str {
95 match self {
96 WalRecord::RegisterDelegation { .. } => "register_delegation",
97 WalRecord::Revoke { .. } => "revoke",
98 WalRecord::Decide { .. } => "decide",
99 }
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108pub struct WalLine {
109 pub seq: u64,
111 pub prev: u64,
113 pub rec: WalRecord,
115}
116
117pub(crate) fn chain_value(prev: u64, seq: u64, rec_json: &str) -> u64 {
123 let mut bytes = Vec::with_capacity(16 + rec_json.len());
124 bytes.extend_from_slice(&prev.to_le_bytes());
125 bytes.extend_from_slice(&seq.to_le_bytes());
126 bytes.extend_from_slice(rec_json.as_bytes());
127 fnv1a_64(&bytes)
128}
129
130pub(crate) fn fnv1a_64(bytes: &[u8]) -> u64 {
132 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
133 const PRIME: u64 = 0x0000_0100_0000_01b3;
134 let mut hash = OFFSET_BASIS;
135 for byte in bytes {
136 hash ^= u64::from(*byte);
137 hash = hash.wrapping_mul(PRIME);
138 }
139 hash
140}
141
142#[derive(Debug)]
145pub struct Wal {
146 file: File,
147 path: PathBuf,
148 lines: u64,
149 chain: u64,
151 _lock: WalLock,
153}
154
155pub fn single_writer_lock_path(wal_path: impl AsRef<Path>) -> PathBuf {
160 let wal_path = wal_path.as_ref();
161 let mut name = wal_path
162 .file_name()
163 .map(|n| n.to_os_string())
164 .unwrap_or_default();
165 name.push(".lock");
166 wal_path.with_file_name(name)
167}
168
169#[derive(Debug)]
191pub struct WalLock {
192 path: PathBuf,
193}
194
195impl WalLock {
196 pub fn acquire(wal_path: impl AsRef<Path>) -> Result<Self, CoreError> {
198 let wal_path = wal_path.as_ref();
199 let lock_path = single_writer_lock_path(wal_path);
200 match OpenOptions::new()
201 .write(true)
202 .create_new(true)
203 .open(&lock_path)
204 {
205 Ok(mut file) => {
206 let written = writeln!(file, "pid={}", std::process::id())
207 .and_then(|()| writeln!(file, "wal={}", wal_path.display()))
208 .and_then(|()| file.flush());
209 if let Err(e) = written {
210 let _ = std::fs::remove_file(&lock_path);
212 return Err(CoreError::WalIo(format!(
213 "写单写者锁 {lock_path:?} 失败(fail-closed): {e}"
214 )));
215 }
216 Ok(Self { path: lock_path })
217 }
218 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
219 let holder = std::fs::read_to_string(&lock_path).unwrap_or_default();
220 let holder = holder.trim();
221 let holder_note = if holder.is_empty() {
222 "锁文件为空(持锁方刚创建,极可能是并发启动竞争)".to_string()
223 } else {
224 format!("持锁信息: {holder}")
225 };
226 Err(CoreError::WalLocked {
227 path: lock_path.display().to_string(),
228 message: format!(
229 "同一份审计日志已有另一个 Wanning 进程在写({holder_note});\
230 确认没有别的闸在跑后,删除该锁文件即可恢复\
231 (默认 WAL 在 target/ 下,cargo clean 亦可)"
232 ),
233 })
234 }
235 Err(e) => Err(CoreError::WalIo(format!(
236 "创建单写者锁 {lock_path:?} 失败: {e}"
237 ))),
238 }
239 }
240}
241
242impl Drop for WalLock {
243 fn drop(&mut self) {
244 let _ = std::fs::remove_file(&self.path);
246 }
247}
248
249impl Wal {
250 pub fn open(path: impl AsRef<Path>) -> Result<Self, CoreError> {
258 let path = path.as_ref().to_path_buf();
259 crate::paths::ensure_wal_parent(&path)?;
260 let _lock = WalLock::acquire(&path)?;
261 let (existing_lines, chain) = if path.exists() {
263 let verified = read_verified(&path)?;
264 (verified.records.len() as u64, verified.tail)
265 } else {
266 (0, 0)
267 };
268 let file = OpenOptions::new()
269 .create(true)
270 .append(true)
271 .read(false)
272 .open(&path)
273 .map_err(|e| CoreError::WalIo(format!("打开 WAL {path:?} 失败: {e}")))?;
274 Ok(Self {
275 file,
276 path,
277 lines: existing_lines,
278 chain,
279 _lock,
280 })
281 }
282
283 pub fn path(&self) -> &Path {
284 &self.path
285 }
286
287 pub fn line_count(&self) -> u64 {
289 self.lines
290 }
291
292 pub fn chain_tail(&self) -> u64 {
294 self.chain
295 }
296
297 pub fn append(&mut self, record: &WalRecord) -> Result<u64, CoreError> {
302 let seq = self.lines + 1;
303 let rec_json = serde_json::to_string(record)
304 .map_err(|e| CoreError::WalIo(format!("WAL 记录序列化失败: {e}")))?;
305 let mut line = serde_json::to_string(&WalLine {
306 seq,
307 prev: self.chain,
308 rec: record.clone(),
309 })
310 .map_err(|e| CoreError::WalIo(format!("WAL 记录序列化失败: {e}")))?;
311 line.push('\n');
312 let path = self.path.clone();
313 self.file
314 .write_all(line.as_bytes())
315 .and_then(|()| self.file.flush())
316 .map_err(|e| CoreError::WalIo(format!("写 WAL {path:?} 失败: {e}")))?;
317 self.lines = seq;
318 self.chain = chain_value(self.chain, seq, &rec_json);
319 Ok(self.lines)
320 }
321}
322
323pub fn raw_lines(path: impl AsRef<Path>) -> Result<Vec<String>, CoreError> {
325 let path = path.as_ref();
326 let file =
327 File::open(path).map_err(|e| CoreError::WalIo(format!("读 WAL {path:?} 失败: {e}")))?;
328 BufReader::new(file)
329 .lines()
330 .collect::<Result<Vec<_>, _>>()
331 .map_err(|e| CoreError::WalIo(format!("读 WAL {path:?} 失败: {e}")))
332}
333
334#[derive(Clone, Debug, PartialEq, Eq)]
336pub struct VerifiedLog {
337 pub records: Vec<(u64, WalRecord)>,
339 pub links: Vec<WalChainLink>,
343 pub tail: u64,
346}
347
348#[derive(Clone, Copy, Debug, PartialEq, Eq)]
350pub struct WalChainLink {
351 pub seq: u64,
353 pub prev: u64,
355 pub value: u64,
357}
358
359pub fn read_verified(path: impl AsRef<Path>) -> Result<VerifiedLog, CoreError> {
370 let mut records = Vec::new();
371 let mut links = Vec::new();
372 let mut chain = 0u64;
373 for (idx, line) in raw_lines(path)?.into_iter().enumerate() {
374 let line_no = idx as u64 + 1;
375 if line.trim().is_empty() {
376 return Err(CoreError::WalBadLine {
377 line: line_no,
378 message: "空行(WAL 不允许空行)".to_string(),
379 });
380 }
381 let parsed: WalLine = match serde_json::from_str(&line) {
382 Ok(parsed) => parsed,
383 Err(e) => return Err(parse_failure(line_no, &line, e)),
384 };
385 if parsed.seq != line_no {
386 return Err(CoreError::WalChainBroken {
387 line: line_no,
388 message: format!(
389 "seq={} 与物理行号 {line_no} 不一致——删行/重排/复制的痕迹",
390 parsed.seq
391 ),
392 });
393 }
394 if parsed.prev != chain {
395 return Err(CoreError::WalChainBroken {
396 line: line_no,
397 message: format!(
398 "prev={} 与按前文重算的链值 {chain} 不符——本行或之前的行被改过,\
399 且后续整条链未重算",
400 parsed.prev
401 ),
402 });
403 }
404 let rec_json = serde_json::to_string(&parsed.rec).map_err(|e| CoreError::WalBadLine {
405 line: line_no,
406 message: format!("记录重序列化失败: {e}"),
407 })?;
408 chain = chain_value(chain, line_no, &rec_json);
409 records.push((line_no, parsed.rec));
410 links.push(WalChainLink {
411 seq: line_no,
412 prev: parsed.prev,
413 value: chain,
414 });
415 }
416 Ok(VerifiedLog {
417 records,
418 links,
419 tail: chain,
420 })
421}
422
423fn parse_failure(line_no: u64, line: &str, error: serde_json::Error) -> CoreError {
426 let legacy_hint = if serde_json::from_str::<WalRecord>(line).is_ok() {
427 ";该行是 W-21 引入完整性链之前的旧格式(裸记录,无 seq/prev 完整性链)。\
428 新旧格式不互通:旧文件原样保留、绝不迁移改写;确认旧日志已留档后,\
429 可将其改名/移走,让闸从一份新日志重新开始"
430 } else {
431 ""
432 };
433 CoreError::WalBadLine {
434 line: line_no,
435 message: format!("JSON 解析失败: {error}{legacy_hint}"),
436 }
437}
438
439pub fn read_records(path: impl AsRef<Path>) -> Result<Vec<(u64, WalRecord)>, CoreError> {
441 Ok(read_verified(path)?.records)
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn tmp_path(tag: &str) -> PathBuf {
449 use std::sync::atomic::{AtomicU64, Ordering};
450 static SEQ: AtomicU64 = AtomicU64::new(0);
451 let dir = std::env::temp_dir().join("wanning-wal-tests");
452 std::fs::create_dir_all(&dir).expect("建临时目录");
453 let nanos = std::time::SystemTime::now()
455 .duration_since(std::time::UNIX_EPOCH)
456 .map(|d| d.as_nanos())
457 .unwrap_or(0);
458 dir.join(format!(
459 "{tag}-{}-{}-{nanos}.jsonl",
460 std::process::id(),
461 SEQ.fetch_add(1, Ordering::SeqCst)
462 ))
463 }
464
465 fn sample_record(ts: u64) -> WalRecord {
466 WalRecord::Decide {
467 ts,
468 decision: WalDecision::Allow,
469 delegation_id: "d1".into(),
470 intent: SpendIntent::new("d1", 1, 500, "jd:shop-1", "grocery", "测试"),
471 reason: None,
472 budget_after_cents: 500,
473 }
474 }
475
476 #[test]
477 fn append_is_one_json_per_line_and_counts_lines() {
478 let path = tmp_path("append");
479 let mut wal = Wal::open(&path).expect("打开");
480 assert_eq!(wal.line_count(), 0);
481 assert_eq!(wal.chain_tail(), 0, "空日志链尾 = 创世值 0");
482 assert_eq!(wal.append(&sample_record(1)).expect("写"), 1);
483 assert_eq!(wal.append(&sample_record(2)).expect("写"), 2);
484 drop(wal);
485
486 let lines = raw_lines(&path).expect("读");
487 assert_eq!(lines.len(), 2);
488 assert!(!lines[0].ends_with('\n'), "行内不含换行");
489 let line: WalLine = serde_json::from_str(&lines[0]).expect("逐行可解析");
490 assert_eq!(line.seq, 1, "首行 seq = 物理行号");
491 assert_eq!(line.prev, 0, "首行 prev = 创世值 0");
492 assert_eq!(line.rec.ts(), 1);
493 assert_eq!(line.rec.kind(), "decide");
494 let second: WalLine = serde_json::from_str(&lines[1]).expect("逐行可解析");
495 assert_eq!(second.seq, 2);
496 assert_ne!(second.prev, 0, "第二行 prev 必须是第一行的链值");
497 }
498
499 #[test]
500 fn chain_tail_matches_independent_recompute_and_survives_reopen() {
501 let path = tmp_path("chain-tail");
503 let mut wal = Wal::open(&path).expect("打开");
504 for ts in 1..=3 {
505 wal.append(&sample_record(ts)).expect("写");
506 }
507 let live_tail = wal.chain_tail();
508 drop(wal);
509
510 let verified = read_verified(&path).expect("读回验链");
511 assert_eq!(verified.records.len(), 3);
512 assert_eq!(verified.tail, live_tail, "读侧重算链尾 == 写侧链尾");
513 assert_ne!(live_tail, 0, "三条记录后链尾非 0");
514
515 let mut wal = Wal::open(&path).expect("重开(历史完整)");
517 assert_eq!(wal.line_count(), 3);
518 assert_eq!(wal.chain_tail(), live_tail, "重开后链尾从历史接续");
519 wal.append(&sample_record(4)).expect("续写");
520 let verified = read_verified(&path).expect("续写后读回验链");
521 assert_eq!(verified.records.len(), 4);
522 assert_eq!(verified.tail, wal.chain_tail());
523 }
524
525 #[test]
526 fn read_verified_reports_per_line_chain_links() {
527 let path = tmp_path("links");
530 let mut wal = Wal::open(&path).expect("打开");
531 for ts in 1..=4 {
532 wal.append(&sample_record(ts)).expect("写");
533 }
534 drop(wal);
535
536 let verified = read_verified(&path).expect("读回验链");
537 assert_eq!(
538 verified.links.len(),
539 verified.records.len(),
540 "逐行链与记录一一对应"
541 );
542 for (idx, link) in verified.links.iter().enumerate() {
543 assert_eq!(link.seq, idx as u64 + 1, "link.seq = 物理行号");
544 if idx == 0 {
545 assert_eq!(link.prev, 0, "首行 prev = 创世值 0");
546 } else {
547 assert_eq!(
548 link.prev,
549 verified.links[idx - 1].value,
550 "本行 prev = 前行链值"
551 );
552 }
553 }
554 assert_eq!(
555 verified.links.last().map(|link| link.value),
556 Some(verified.tail),
557 "尾行链值 = 链尾"
558 );
559 }
560
561 #[test]
562 fn empty_wal_has_no_chain_links() {
563 let path = tmp_path("empty-links");
564 std::fs::write(&path, "").expect("写空文件");
565 let verified = read_verified(&path).expect("空文件是合法状态");
566 assert!(verified.links.is_empty(), "空日志无链节");
567 }
568
569 #[test]
570 fn empty_wal_verifies_to_genesis_chain() {
571 let path = tmp_path("empty-chain");
572 std::fs::write(&path, "").expect("写空文件");
573 let verified = read_verified(&path).expect("空文件是合法状态");
574 assert!(verified.records.is_empty());
575 assert_eq!(verified.tail, 0, "空日志链尾 = 创世值 0");
576 }
577
578 #[test]
579 fn open_is_append_only_never_truncates() {
580 let path = tmp_path("append-only");
581 {
582 let mut wal = Wal::open(&path).expect("打开");
583 wal.append(&sample_record(1)).expect("写");
584 }
585 {
586 let mut wal = Wal::open(&path).expect("重开不得截断");
587 assert_eq!(wal.line_count(), 1, "重开必须看到历史行");
588 wal.append(&sample_record(2)).expect("追加");
589 }
590 assert_eq!(raw_lines(&path).expect("读").len(), 2, "历史行必须保留");
591 }
592
593 #[test]
594 fn decide_record_roundtrip_shape() {
595 let deny = WalRecord::Decide {
597 ts: 7,
598 decision: WalDecision::Deny,
599 delegation_id: "d1".into(),
600 intent: SpendIntent::new("d1", 2, 9000, "jd:shop-1", "x", ""),
601 reason: Some(DenyReason::OverBudget),
602 budget_after_cents: 500,
603 };
604 let json = serde_json::to_string(&deny).unwrap();
605 assert!(json.contains("\"kind\":\"decide\""));
606 assert!(json.contains("\"decision\":\"deny\""));
607 assert!(json.contains("\"reason\":\"over_budget\""));
608 let back: WalRecord = serde_json::from_str(&json).unwrap();
609 assert_eq!(back, deny);
610
611 let allow_json = serde_json::to_string(&sample_record(1)).unwrap();
612 assert!(!allow_json.contains("reason"), "Allow 不应带 reason 字段");
613 }
614
615 #[test]
616 fn read_records_fails_closed_on_half_line() {
617 let path = tmp_path("corrupt");
618 std::fs::write(&path, "{\"kind\":\"revoke\",\"ts\":1,\"deleg\n").expect("写坏行");
619 let err = read_records(&path).unwrap_err();
620 assert!(
621 matches!(err, CoreError::WalBadLine { line: 1, .. }),
622 "半行 JSON 必须 fail-closed 报错: {err:?}"
623 );
624 }
625
626 #[test]
627 fn read_records_fails_closed_on_blank_line() {
628 let path = tmp_path("blank");
629 std::fs::write(&path, "\n").expect("写空行");
630 let err = read_records(&path).unwrap_err();
631 assert!(
632 matches!(err, CoreError::WalBadLine { line: 1, .. }),
633 "{err:?}"
634 );
635 }
636
637 #[test]
638 fn read_records_fails_closed_on_unknown_shape() {
639 let path = tmp_path("unknown");
640 std::fs::write(&path, "{\"kind\":\"mystery\",\"ts\":1}\n").expect("写");
641 let err = read_records(&path).unwrap_err();
642 assert!(
643 matches!(err, CoreError::WalBadLine { line: 1, .. }),
644 "{err:?}"
645 );
646 }
647
648 #[test]
649 fn read_records_reports_failing_line_number() {
650 let path = tmp_path("line3");
651 let mut wal = Wal::open(&path).expect("打开");
652 wal.append(&sample_record(1)).expect("写");
653 wal.append(&sample_record(2)).expect("写");
654 drop(wal);
655 let mut content = raw_lines(&path).expect("读").join("\n");
656 content.push_str("\n{\"kind\":\"decide\",\"ts\":3\n");
657 std::fs::write(&path, content).expect("追加坏行");
658
659 match read_records(&path) {
660 Err(CoreError::WalBadLine { line, .. }) => assert_eq!(line, 3, "报错必须指到坏行"),
661 other => panic!("应报 WalBadLine,实际 {other:?}"),
662 }
663 }
664}