1use std::collections::VecDeque;
2
3use citadel_core::types::{PageId, PageType};
4use citadel_core::{Result, MERKLE_HASH_SIZE};
5
6pub type MerkleHash = [u8; MERKLE_HASH_SIZE];
8
9#[derive(Debug, Clone)]
11pub struct PageDigest {
12 pub page_id: PageId,
13 pub page_type: PageType,
14 pub merkle_hash: MerkleHash,
15 pub children: Vec<PageId>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct DiffEntry {
22 pub key: Vec<u8>,
23 pub value: Vec<u8>,
24 pub val_type: u8,
25}
26
27#[derive(Debug, Clone)]
29pub struct DiffResult {
30 pub entries: Vec<DiffEntry>,
32 pub pages_compared: u64,
34 pub subtrees_skipped: u64,
36}
37
38impl DiffResult {
39 pub fn is_empty(&self) -> bool {
40 self.entries.is_empty()
41 }
42
43 pub fn len(&self) -> usize {
44 self.entries.len()
45 }
46}
47
48pub trait TreeReader {
53 fn root_info(&self) -> Result<(PageId, MerkleHash)>;
55
56 fn page_digest(&self, page_id: PageId) -> Result<PageDigest>;
58
59 fn leaf_entries(&self, page_id: PageId) -> Result<Vec<DiffEntry>>;
61
62 fn subtree_entries(&self, page_id: PageId) -> Result<Vec<DiffEntry>> {
64 let digest = self.page_digest(page_id)?;
65 match digest.page_type {
66 PageType::Leaf => self.leaf_entries(page_id),
67 PageType::Branch => {
68 let mut entries = Vec::new();
69 for child in &digest.children {
70 entries.extend(self.subtree_entries(*child)?);
71 }
72 Ok(entries)
73 }
74 _ => Ok(Vec::new()),
75 }
76 }
77}
78
79pub fn merkle_diff(source: &dyn TreeReader, target: &dyn TreeReader) -> Result<DiffResult> {
85 let (src_root, src_root_hash) = source.root_info()?;
86 let (tgt_root, tgt_root_hash) = target.root_info()?;
87
88 let mut result = DiffResult {
89 entries: Vec::new(),
90 pages_compared: 0,
91 subtrees_skipped: 0,
92 };
93
94 if src_root_hash == tgt_root_hash {
96 return Ok(result);
97 }
98
99 let mut queue: VecDeque<(PageId, PageId)> = VecDeque::new();
100 queue.push_back((src_root, tgt_root));
101
102 while let Some((src_pid, tgt_pid)) = queue.pop_front() {
103 let src_digest = source.page_digest(src_pid)?;
104 let tgt_digest = target.page_digest(tgt_pid)?;
105 result.pages_compared += 1;
106
107 if src_digest.merkle_hash == tgt_digest.merkle_hash {
108 result.subtrees_skipped += 1;
109 continue;
110 }
111
112 match (src_digest.page_type, tgt_digest.page_type) {
113 (PageType::Leaf, PageType::Leaf) => {
114 result.entries.extend(source.leaf_entries(src_pid)?);
115 }
116 (PageType::Branch, PageType::Branch)
117 if src_digest.children.len() == tgt_digest.children.len() =>
118 {
119 for (sc, tc) in src_digest.children.iter().zip(&tgt_digest.children) {
120 queue.push_back((*sc, *tc));
121 }
122 }
123 _ => {
124 result.entries.extend(source.subtree_entries(src_pid)?);
125 }
126 }
127 }
128
129 Ok(result)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn diff_entry_equality() {
138 let a = DiffEntry {
139 key: b"key1".to_vec(),
140 value: b"val1".to_vec(),
141 val_type: 0,
142 };
143 let b = DiffEntry {
144 key: b"key1".to_vec(),
145 value: b"val1".to_vec(),
146 val_type: 0,
147 };
148 let c = DiffEntry {
149 key: b"key1".to_vec(),
150 value: b"val2".to_vec(),
151 val_type: 0,
152 };
153 assert_eq!(a, b);
154 assert_ne!(a, c);
155 }
156
157 #[test]
158 fn diff_result_empty() {
159 let r = DiffResult {
160 entries: vec![],
161 pages_compared: 0,
162 subtrees_skipped: 0,
163 };
164 assert!(r.is_empty());
165 assert_eq!(r.len(), 0);
166 }
167
168 #[test]
169 fn diff_result_non_empty() {
170 let r = DiffResult {
171 entries: vec![DiffEntry {
172 key: b"k".to_vec(),
173 value: b"v".to_vec(),
174 val_type: 0,
175 }],
176 pages_compared: 1,
177 subtrees_skipped: 0,
178 };
179 assert!(!r.is_empty());
180 assert_eq!(r.len(), 1);
181 }
182
183 #[test]
184 fn page_digest_leaf_has_no_children() {
185 let d = PageDigest {
186 page_id: PageId(0),
187 page_type: PageType::Leaf,
188 merkle_hash: [0u8; MERKLE_HASH_SIZE],
189 children: vec![],
190 };
191 assert!(d.children.is_empty());
192 }
193
194 #[test]
195 fn page_digest_branch_has_children() {
196 let d = PageDigest {
197 page_id: PageId(0),
198 page_type: PageType::Branch,
199 merkle_hash: [1u8; MERKLE_HASH_SIZE],
200 children: vec![PageId(1), PageId(2), PageId(3)],
201 };
202 assert_eq!(d.children.len(), 3);
203 }
204}