1use sha2::{Digest, Sha256};
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PayloadDelta {
7 pub base_cursor: u64,
8 pub ops: Vec<DeltaOp>,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum DeltaOp {
13 Replace { sha: String, content: String },
14}
15
16#[derive(Debug, PartialEq, Eq)]
17pub enum DeltaApplyResult {
18 Applied { sha: String, content: String },
19 NeedsFullSync,
20}
21
22#[derive(Debug, Error, PartialEq, Eq)]
23pub enum DeltaError {
24 #[error("base cursor mismatch")]
25 BaseMismatch,
26}
27
28pub fn compute_delta(base_cursor: u64, current: &str, next: &str) -> PayloadDelta {
30 if current == next {
31 PayloadDelta {
32 base_cursor,
33 ops: Vec::new(),
34 }
35 } else {
36 PayloadDelta {
37 base_cursor,
38 ops: vec![DeltaOp::Replace {
39 sha: sha256(next),
40 content: next.to_string(),
41 }],
42 }
43 }
44}
45
46pub fn apply_delta(
49 current_cursor: u64,
50 _current_sha: &str,
51 delta: PayloadDelta,
52) -> Result<DeltaApplyResult, DeltaError> {
53 if delta.base_cursor != current_cursor {
54 return Err(DeltaError::BaseMismatch);
55 }
56
57 if delta.ops.is_empty() {
58 return Ok(DeltaApplyResult::NeedsFullSync);
59 }
60
61 match &delta.ops[0] {
62 DeltaOp::Replace { sha, content } => {
63 if sha256(content) != *sha {
64 Ok(DeltaApplyResult::NeedsFullSync)
65 } else {
66 Ok(DeltaApplyResult::Applied {
67 sha: sha.clone(),
68 content: content.clone(),
69 })
70 }
71 }
72 }
73}
74
75fn sha256(body: &str) -> String {
76 let mut hasher = Sha256::new();
77 hasher.update(body.as_bytes());
78 format!("{:x}", hasher.finalize())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn computes_replace_when_different() {
87 let delta = compute_delta(10, "old", "new");
88 assert_eq!(delta.base_cursor, 10);
89 assert_eq!(delta.ops.len(), 1);
90 match &delta.ops[0] {
91 DeltaOp::Replace { content, .. } => assert_eq!(content, "new"),
92 }
93 }
94
95 #[test]
96 fn no_ops_when_same() {
97 let delta = compute_delta(5, "same", "same");
98 assert!(delta.ops.is_empty());
99 }
100
101 #[test]
102 fn apply_delta_success() {
103 let next = "new tree";
104 let delta = compute_delta(3, "old", next);
105 let res = apply_delta(3, "ignored", delta).unwrap();
106 match res {
107 DeltaApplyResult::Applied { sha, content } => {
108 assert_eq!(content, next);
109 assert_eq!(sha, sha256(next));
110 }
111 _ => panic!("expected applied"),
112 }
113 }
114
115 #[test]
116 fn apply_delta_detects_corruption() {
117 let mut delta = compute_delta(2, "old", "good");
118 let DeltaOp::Replace { content, .. } = &mut delta.ops[0];
119 *content = "tampered".to_string();
120 let res = apply_delta(2, "ignored", delta).unwrap();
121 assert!(matches!(res, DeltaApplyResult::NeedsFullSync));
122 }
123
124 #[test]
125 fn base_cursor_mismatch_errors() {
126 let delta = compute_delta(1, "a", "b");
127 let err = apply_delta(2, "ignored", delta).unwrap_err();
128 assert_eq!(err, DeltaError::BaseMismatch);
129 }
130}