use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayloadDelta {
pub base_cursor: u64,
pub ops: Vec<DeltaOp>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeltaOp {
Replace { sha: String, content: String },
}
#[derive(Debug, PartialEq, Eq)]
pub enum DeltaApplyResult {
Applied { sha: String, content: String },
NeedsFullSync,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DeltaError {
#[error("base cursor mismatch")]
BaseMismatch,
}
pub fn compute_delta(base_cursor: u64, current: &str, next: &str) -> PayloadDelta {
if current == next {
PayloadDelta {
base_cursor,
ops: Vec::new(),
}
} else {
PayloadDelta {
base_cursor,
ops: vec![DeltaOp::Replace {
sha: sha256(next),
content: next.to_string(),
}],
}
}
}
pub fn apply_delta(
current_cursor: u64,
_current_sha: &str,
delta: PayloadDelta,
) -> Result<DeltaApplyResult, DeltaError> {
if delta.base_cursor != current_cursor {
return Err(DeltaError::BaseMismatch);
}
if delta.ops.is_empty() {
return Ok(DeltaApplyResult::NeedsFullSync);
}
match &delta.ops[0] {
DeltaOp::Replace { sha, content } => {
if sha256(content) != *sha {
Ok(DeltaApplyResult::NeedsFullSync)
} else {
Ok(DeltaApplyResult::Applied {
sha: sha.clone(),
content: content.clone(),
})
}
}
}
}
fn sha256(body: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(body.as_bytes());
format!("{:x}", hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn computes_replace_when_different() {
let delta = compute_delta(10, "old", "new");
assert_eq!(delta.base_cursor, 10);
assert_eq!(delta.ops.len(), 1);
match &delta.ops[0] {
DeltaOp::Replace { content, .. } => assert_eq!(content, "new"),
}
}
#[test]
fn no_ops_when_same() {
let delta = compute_delta(5, "same", "same");
assert!(delta.ops.is_empty());
}
#[test]
fn apply_delta_success() {
let next = "new tree";
let delta = compute_delta(3, "old", next);
let res = apply_delta(3, "ignored", delta).unwrap();
match res {
DeltaApplyResult::Applied { sha, content } => {
assert_eq!(content, next);
assert_eq!(sha, sha256(next));
}
_ => panic!("expected applied"),
}
}
#[test]
fn apply_delta_detects_corruption() {
let mut delta = compute_delta(2, "old", "good");
let DeltaOp::Replace { content, .. } = &mut delta.ops[0];
*content = "tampered".to_string();
let res = apply_delta(2, "ignored", delta).unwrap();
assert!(matches!(res, DeltaApplyResult::NeedsFullSync));
}
#[test]
fn base_cursor_mismatch_errors() {
let delta = compute_delta(1, "a", "b");
let err = apply_delta(2, "ignored", delta).unwrap_err();
assert_eq!(err, DeltaError::BaseMismatch);
}
}