Skip to main content

claw_patch/
binary.rs

1use claw_core::types::PatchOp;
2
3use crate::codec::Codec;
4use crate::PatchError;
5
6/// Codec that treats every binary change as a whole-object replacement.
7pub struct BinaryCodec;
8
9impl Codec for BinaryCodec {
10    fn id(&self) -> &str {
11        "binary"
12    }
13
14    fn diff(&self, old: &[u8], new: &[u8]) -> Result<Vec<PatchOp>, PatchError> {
15        if old == new {
16            return Ok(vec![]);
17        }
18        Ok(vec![PatchOp {
19            address: "B0".to_string(),
20            op_type: "replace".to_string(),
21            old_data: Some(old.to_vec()),
22            new_data: Some(new.to_vec()),
23            context_hash: None,
24        }])
25    }
26
27    fn apply(&self, base: &[u8], ops: &[PatchOp]) -> Result<Vec<u8>, PatchError> {
28        if ops.is_empty() {
29            return Ok(base.to_vec());
30        }
31        // Return new_data from the last replace op
32        for op in ops.iter().rev() {
33            if let Some(ref new_data) = op.new_data {
34                return Ok(new_data.clone());
35            }
36        }
37        Err(PatchError::ApplyFailed("no replace op found".into()))
38    }
39
40    fn invert(&self, ops: &[PatchOp]) -> Result<Vec<PatchOp>, PatchError> {
41        Ok(ops
42            .iter()
43            .map(|op| PatchOp {
44                address: op.address.clone(),
45                op_type: op.op_type.clone(),
46                old_data: op.new_data.clone(),
47                new_data: op.old_data.clone(),
48                context_hash: None,
49            })
50            .collect())
51    }
52
53    fn commute(
54        &self,
55        _left: &[PatchOp],
56        _right: &[PatchOp],
57    ) -> Result<(Vec<PatchOp>, Vec<PatchOp>), PatchError> {
58        Err(PatchError::CommuteFailed)
59    }
60
61    fn merge3(&self, _base: &[u8], _left: &[u8], _right: &[u8]) -> Result<Vec<u8>, PatchError> {
62        Err(PatchError::Merge3Failed(
63            "binary files cannot be auto-merged".into(),
64        ))
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn binary_diff_identical() {
74        let codec = BinaryCodec;
75        let data = b"hello";
76        let ops = codec.diff(data, data).unwrap();
77        assert!(ops.is_empty());
78    }
79
80    #[test]
81    fn binary_diff_and_apply() {
82        let codec = BinaryCodec;
83        let old = b"old data";
84        let new = b"new data";
85        let ops = codec.diff(old, new).unwrap();
86        assert_eq!(ops.len(), 1);
87        let result = codec.apply(old, &ops).unwrap();
88        assert_eq!(result, new);
89    }
90
91    #[test]
92    fn binary_invert() {
93        let codec = BinaryCodec;
94        let old = b"old";
95        let new = b"new";
96        let ops = codec.diff(old, new).unwrap();
97        let inv = codec.invert(&ops).unwrap();
98        let result = codec.apply(new, &inv).unwrap();
99        assert_eq!(result, old);
100    }
101
102    #[test]
103    fn binary_commute_fails() {
104        let codec = BinaryCodec;
105        let ops = vec![PatchOp {
106            address: "B0".to_string(),
107            op_type: "replace".to_string(),
108            old_data: Some(vec![1]),
109            new_data: Some(vec![2]),
110            context_hash: None,
111        }];
112        assert!(codec.commute(&ops, &ops).is_err());
113    }
114
115    #[test]
116    fn binary_merge3_fails() {
117        let codec = BinaryCodec;
118        assert!(codec.merge3(b"base", b"left", b"right").is_err());
119    }
120}