1use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ChunkRef {
11 pub hash: String,
13 pub size: u64,
15}
16
17#[derive(Debug, Clone)]
23pub struct ChunkDelta {
24 pub added: Vec<ChunkRef>,
26 pub removed: Vec<ChunkRef>,
28 pub unchanged: usize,
30 pub efficiency: f64,
33 pub full_upload_recommended: bool,
35}
36
37pub fn compute_chunk_delta(old: &[ChunkRef], new: &[ChunkRef]) -> ChunkDelta {
50 let old_set: HashSet<&str> = old.iter().map(|c| c.hash.as_str()).collect();
51 let new_set: HashSet<&str> = new.iter().map(|c| c.hash.as_str()).collect();
52
53 let added: Vec<ChunkRef> = new.iter()
54 .filter(|c| !old_set.contains(c.hash.as_str()))
55 .cloned()
56 .collect();
57
58 let removed: Vec<ChunkRef> = old.iter()
59 .filter(|c| !new_set.contains(c.hash.as_str()))
60 .cloned()
61 .collect();
62
63 let unchanged = new.len().saturating_sub(added.len());
64
65 let efficiency = if new.is_empty() {
66 0.0
67 } else {
68 unchanged as f64 / new.len() as f64
69 };
70
71 let full_upload_recommended = if old.is_empty() {
72 false
73 } else {
74 removed.len() as f64 / old.len() as f64 > 0.80
75 };
76
77 ChunkDelta { added, removed, unchanged, efficiency, full_upload_recommended }
78}
79
80#[cfg(test)]
81mod tests {
82 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
83 use super::*;
84
85 fn chunk(hash: &str, size: u64) -> ChunkRef {
86 ChunkRef { hash: hash.to_string(), size }
87 }
88
89 #[test]
90 fn test_empty_old_empty_new() {
91 let d = compute_chunk_delta(&[], &[]);
92 assert_eq!(d.added.len(), 0);
93 assert_eq!(d.removed.len(), 0);
94 assert_eq!(d.unchanged, 0);
95 assert_eq!(d.efficiency, 0.0);
96 assert!(!d.full_upload_recommended);
97 }
98
99 #[test]
100 fn test_empty_old_five_new() {
101 let new = vec![chunk("a",100), chunk("b",100), chunk("c",100), chunk("d",100), chunk("e",100)];
102 let d = compute_chunk_delta(&[], &new);
103 assert_eq!(d.added.len(), 5);
104 assert_eq!(d.removed.len(), 0);
105 assert_eq!(d.unchanged, 0);
106 assert_eq!(d.efficiency, 0.0);
107 assert!(!d.full_upload_recommended); }
109
110 #[test]
111 fn test_five_old_empty_new() {
112 let old = vec![chunk("a",100), chunk("b",100), chunk("c",100), chunk("d",100), chunk("e",100)];
113 let d = compute_chunk_delta(&old, &[]);
114 assert_eq!(d.added.len(), 0);
115 assert_eq!(d.removed.len(), 5);
116 assert_eq!(d.unchanged, 0);
117 assert_eq!(d.efficiency, 0.0);
118 assert!(d.full_upload_recommended);
120 }
121
122 #[test]
123 fn test_identical_chunks() {
124 let chunks = vec![chunk("a",100), chunk("b",200), chunk("c",300)];
125 let d = compute_chunk_delta(&chunks, &chunks);
126 assert_eq!(d.added.len(), 0);
127 assert_eq!(d.removed.len(), 0);
128 assert_eq!(d.unchanged, 3);
129 assert!((d.efficiency - 1.0).abs() < f64::EPSILON);
130 assert!(!d.full_upload_recommended);
131 }
132
133 #[test]
134 fn test_one_chunk_changed_out_of_ten() {
135 let old: Vec<ChunkRef> = (0..10).map(|i| chunk(&format!("old_{i}"), 100)).collect();
136 let mut new_chunks = old.clone();
138 new_chunks[5] = chunk("new_5", 100);
139 let d = compute_chunk_delta(&old, &new_chunks);
140 assert_eq!(d.added.len(), 1);
141 assert_eq!(d.removed.len(), 1);
142 assert_eq!(d.unchanged, 9);
143 assert!((d.efficiency - 0.9).abs() < 1e-10);
144 assert!(!d.full_upload_recommended); }
146
147 #[test]
148 fn test_all_chunks_changed() {
149 let old: Vec<ChunkRef> = (0..10).map(|i| chunk(&format!("old_{i}"), 100)).collect();
150 let new: Vec<ChunkRef> = (0..10).map(|i| chunk(&format!("new_{i}"), 100)).collect();
151 let d = compute_chunk_delta(&old, &new);
152 assert_eq!(d.added.len(), 10);
153 assert_eq!(d.removed.len(), 10);
154 assert_eq!(d.unchanged, 0);
155 assert_eq!(d.efficiency, 0.0);
156 assert!(d.full_upload_recommended); }
158
159 #[test]
160 fn test_append_two_new_chunks() {
161 let old = vec![chunk("a",100), chunk("b",100)];
162 let new_chunks = vec![chunk("a",100), chunk("b",100), chunk("c",100), chunk("d",100)];
163 let d = compute_chunk_delta(&old, &new_chunks);
164 assert_eq!(d.added.len(), 2);
165 assert_eq!(d.removed.len(), 0);
166 assert_eq!(d.unchanged, 2);
167 assert!((d.efficiency - 0.5).abs() < f64::EPSILON);
168 assert!(!d.full_upload_recommended); }
170
171 #[test]
172 fn test_remove_three_chunks_from_tail() {
173 let old: Vec<ChunkRef> = (0..5).map(|i| chunk(&format!("c{i}"), 100)).collect();
174 let new_chunks = old[..2].to_vec(); let d = compute_chunk_delta(&old, &new_chunks);
176 assert_eq!(d.added.len(), 0);
177 assert_eq!(d.removed.len(), 3);
178 assert_eq!(d.unchanged, 2);
179 assert!(!d.full_upload_recommended);
181 }
182
183 #[test]
184 fn test_full_upload_threshold_exactly_80_percent() {
185 let old: Vec<ChunkRef> = (0..10).map(|i| chunk(&format!("old_{i}"), 100)).collect();
187 let mut new_chunks = old.clone();
188 for i in 0..8 {
189 new_chunks[i] = chunk(&format!("new_{i}"), 100);
190 }
191 let d = compute_chunk_delta(&old, &new_chunks);
192 assert!(!d.full_upload_recommended);
194
195 let mut new2 = old.clone();
197 for i in 0..9 {
198 new2[i] = chunk(&format!("newx_{i}"), 100);
199 }
200 let d2 = compute_chunk_delta(&old, &new2);
201 assert!(d2.full_upload_recommended);
202 }
203
204 #[test]
205 fn test_single_chunk_change() {
206 let old = vec![chunk("only_chunk", 512)];
207 let new_chunks = vec![chunk("new_chunk", 512)];
208 let d = compute_chunk_delta(&old, &new_chunks);
209 assert_eq!(d.added.len(), 1);
210 assert_eq!(d.removed.len(), 1);
211 assert_eq!(d.unchanged, 0);
212 assert!(d.full_upload_recommended);
214 }
215
216 #[test]
217 fn test_order_does_not_matter_for_identity() {
218 let old = vec![chunk("a",100), chunk("b",100), chunk("c",100)];
220 let new_chunks = vec![chunk("c",100), chunk("a",100), chunk("b",100)];
221 let d = compute_chunk_delta(&old, &new_chunks);
222 assert_eq!(d.added.len(), 0);
223 assert_eq!(d.removed.len(), 0);
224 assert_eq!(d.unchanged, 3);
225 assert!(!d.full_upload_recommended);
226 }
227}