1use std::collections::HashMap;
5
6pub fn fnv1a(bytes: &[u8]) -> u64 {
12 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
13 const PRIME: u64 = 1_099_511_628_211;
14
15 let mut hash = OFFSET_BASIS;
16 for &b in bytes {
17 hash ^= u64::from(b);
18 hash = hash.wrapping_mul(PRIME);
19 }
20 hash
21}
22
23#[derive(Debug, Clone, PartialEq)]
29pub struct ManifestEntry {
30 pub cid: String,
32 pub size_bytes: u64,
34 pub content_type: String,
36 pub added_at_secs: u64,
38 pub pinned: bool,
40 pub checksum: u64,
42}
43
44impl ManifestEntry {
45 pub fn new(
47 cid: impl Into<String>,
48 size_bytes: u64,
49 content_type: impl Into<String>,
50 added_at_secs: u64,
51 pinned: bool,
52 ) -> Self {
53 let cid = cid.into();
54 let checksum = fnv1a(cid.as_bytes());
55 Self {
56 cid,
57 size_bytes,
58 content_type: content_type.into(),
59 added_at_secs,
60 pinned,
61 checksum,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
72pub struct ManifestStats {
73 pub total_entries: usize,
75 pub total_size_bytes: u64,
77 pub pinned_count: usize,
79 pub content_type_counts: HashMap<String, usize>,
81}
82
83#[derive(Debug, Clone, PartialEq)]
89pub enum ManifestFilter {
90 All,
92 PinnedOnly,
94 ContentType(String),
96 AddedAfter(u64),
98 SizeAbove(u64),
100}
101
102impl ManifestFilter {
103 fn matches(&self, entry: &ManifestEntry) -> bool {
104 match self {
105 ManifestFilter::All => true,
106 ManifestFilter::PinnedOnly => entry.pinned,
107 ManifestFilter::ContentType(ct) => &entry.content_type == ct,
108 ManifestFilter::AddedAfter(threshold) => entry.added_at_secs > *threshold,
109 ManifestFilter::SizeAbove(threshold) => entry.size_bytes > *threshold,
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
123pub struct StorageBlockManifest {
124 pub entries: HashMap<String, ManifestEntry>,
126}
127
128impl StorageBlockManifest {
129 pub fn new() -> Self {
131 Self {
132 entries: HashMap::new(),
133 }
134 }
135
136 pub fn add_entry(&mut self, entry: ManifestEntry) {
138 self.entries.insert(entry.cid.clone(), entry);
139 }
140
141 pub fn remove_entry(&mut self, cid: &str) -> bool {
145 self.entries.remove(cid).is_some()
146 }
147
148 pub fn get_entry(&self, cid: &str) -> Option<&ManifestEntry> {
150 self.entries.get(cid)
151 }
152
153 pub fn pin(&mut self, cid: &str) -> bool {
157 match self.entries.get_mut(cid) {
158 Some(entry) => {
159 entry.pinned = true;
160 true
161 }
162 None => false,
163 }
164 }
165
166 pub fn unpin(&mut self, cid: &str) -> bool {
170 match self.entries.get_mut(cid) {
171 Some(entry) => {
172 entry.pinned = false;
173 true
174 }
175 None => false,
176 }
177 }
178
179 pub fn filter(&self, filter: &ManifestFilter) -> Vec<&ManifestEntry> {
181 let mut matched: Vec<&ManifestEntry> = self
182 .entries
183 .values()
184 .filter(|e| filter.matches(e))
185 .collect();
186 matched.sort_by_key(|e| e.added_at_secs);
187 matched
188 }
189
190 pub fn stats(&self) -> ManifestStats {
192 let mut total_size_bytes: u64 = 0;
193 let mut pinned_count: usize = 0;
194 let mut content_type_counts: HashMap<String, usize> = HashMap::new();
195
196 for entry in self.entries.values() {
197 total_size_bytes = total_size_bytes.saturating_add(entry.size_bytes);
198 if entry.pinned {
199 pinned_count += 1;
200 }
201 *content_type_counts
202 .entry(entry.content_type.clone())
203 .or_insert(0) += 1;
204 }
205
206 ManifestStats {
207 total_entries: self.entries.len(),
208 total_size_bytes,
209 pinned_count,
210 content_type_counts,
211 }
212 }
213
214 pub fn merge(&mut self, other: &StorageBlockManifest) {
220 for (cid, other_entry) in &other.entries {
221 match self.entries.get_mut(cid) {
222 Some(self_entry) => {
223 if other_entry.pinned {
224 self_entry.pinned = true;
225 }
226 }
227 None => {
228 self.entries.insert(cid.clone(), other_entry.clone());
229 }
230 }
231 }
232 }
233
234 pub fn export_cids(&self) -> Vec<String> {
236 let mut cids: Vec<String> = self.entries.keys().cloned().collect();
237 cids.sort();
238 cids
239 }
240
241 pub fn verify_checksums(&self) -> Vec<String> {
244 let mut mismatches: Vec<String> = self
245 .entries
246 .values()
247 .filter(|e| fnv1a(e.cid.as_bytes()) != e.checksum)
248 .map(|e| e.cid.clone())
249 .collect();
250 mismatches.sort();
251 mismatches
252 }
253}
254
255impl Default for StorageBlockManifest {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 fn make_entry(
270 cid: &str,
271 size_bytes: u64,
272 content_type: &str,
273 added_at_secs: u64,
274 pinned: bool,
275 ) -> ManifestEntry {
276 ManifestEntry::new(cid, size_bytes, content_type, added_at_secs, pinned)
277 }
278
279 #[test]
284 fn test_new_starts_empty() {
285 let manifest = StorageBlockManifest::new();
286 assert!(manifest.entries.is_empty());
287 }
288
289 #[test]
294 fn test_add_entry_stores_correctly() {
295 let mut m = StorageBlockManifest::new();
296 let entry = make_entry("cid1", 100, "raw", 1000, false);
297 m.add_entry(entry.clone());
298 let got = m.get_entry("cid1").expect("entry should exist");
299 assert_eq!(got, &entry);
300 }
301
302 #[test]
303 fn test_add_entry_overwrites_existing() {
304 let mut m = StorageBlockManifest::new();
305 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
306 m.add_entry(make_entry("cid1", 200, "dag-cbor", 2000, true));
307 let got = m.get_entry("cid1").expect("entry should exist");
308 assert_eq!(got.size_bytes, 200);
309 assert_eq!(got.content_type, "dag-cbor");
310 }
311
312 #[test]
313 fn test_get_entry_none_for_missing() {
314 let m = StorageBlockManifest::new();
315 assert!(m.get_entry("nonexistent").is_none());
316 }
317
318 #[test]
323 fn test_remove_entry_returns_true_when_found() {
324 let mut m = StorageBlockManifest::new();
325 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
326 assert!(m.remove_entry("cid1"));
327 assert!(m.get_entry("cid1").is_none());
328 }
329
330 #[test]
331 fn test_remove_entry_returns_false_when_not_found() {
332 let mut m = StorageBlockManifest::new();
333 assert!(!m.remove_entry("ghost"));
334 }
335
336 #[test]
341 fn test_pin_returns_false_when_missing() {
342 let mut m = StorageBlockManifest::new();
343 assert!(!m.pin("ghost"));
344 }
345
346 #[test]
347 fn test_unpin_returns_false_when_missing() {
348 let mut m = StorageBlockManifest::new();
349 assert!(!m.unpin("ghost"));
350 }
351
352 #[test]
353 fn test_pin_sets_pinned_true() {
354 let mut m = StorageBlockManifest::new();
355 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
356 assert!(m.pin("cid1"));
357 assert!(m.get_entry("cid1").unwrap().pinned);
358 }
359
360 #[test]
361 fn test_unpin_sets_pinned_false() {
362 let mut m = StorageBlockManifest::new();
363 m.add_entry(make_entry("cid1", 100, "raw", 1000, true));
364 assert!(m.unpin("cid1"));
365 assert!(!m.get_entry("cid1").unwrap().pinned);
366 }
367
368 #[test]
369 fn test_pin_returns_true_when_found() {
370 let mut m = StorageBlockManifest::new();
371 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
372 assert!(m.pin("cid1"));
373 }
374
375 #[test]
376 fn test_unpin_returns_true_when_found() {
377 let mut m = StorageBlockManifest::new();
378 m.add_entry(make_entry("cid1", 100, "raw", 1000, true));
379 assert!(m.unpin("cid1"));
380 }
381
382 #[test]
387 fn test_filter_all_returns_all() {
388 let mut m = StorageBlockManifest::new();
389 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
390 m.add_entry(make_entry("cid2", 200, "dag-cbor", 2000, true));
391 assert_eq!(m.filter(&ManifestFilter::All).len(), 2);
392 }
393
394 #[test]
395 fn test_filter_pinned_only() {
396 let mut m = StorageBlockManifest::new();
397 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
398 m.add_entry(make_entry("cid2", 200, "dag-cbor", 2000, true));
399 let result = m.filter(&ManifestFilter::PinnedOnly);
400 assert_eq!(result.len(), 1);
401 assert_eq!(result[0].cid, "cid2");
402 }
403
404 #[test]
405 fn test_filter_content_type() {
406 let mut m = StorageBlockManifest::new();
407 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
408 m.add_entry(make_entry("cid2", 200, "dag-cbor", 2000, false));
409 m.add_entry(make_entry("cid3", 300, "raw", 3000, false));
410 let result = m.filter(&ManifestFilter::ContentType("raw".to_string()));
411 assert_eq!(result.len(), 2);
412 }
413
414 #[test]
415 fn test_filter_added_after() {
416 let mut m = StorageBlockManifest::new();
417 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
418 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
419 m.add_entry(make_entry("cid3", 300, "raw", 3000, false));
420 let result = m.filter(&ManifestFilter::AddedAfter(1500));
421 assert_eq!(result.len(), 2);
422 for e in &result {
423 assert!(e.added_at_secs > 1500);
424 }
425 }
426
427 #[test]
428 fn test_filter_size_above() {
429 let mut m = StorageBlockManifest::new();
430 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
431 m.add_entry(make_entry("cid2", 500, "raw", 2000, false));
432 m.add_entry(make_entry("cid3", 1000, "raw", 3000, false));
433 let result = m.filter(&ManifestFilter::SizeAbove(400));
434 assert_eq!(result.len(), 2);
435 for e in &result {
436 assert!(e.size_bytes > 400);
437 }
438 }
439
440 #[test]
441 fn test_filter_sorted_by_added_at_secs_asc() {
442 let mut m = StorageBlockManifest::new();
443 m.add_entry(make_entry("cid3", 300, "raw", 3000, false));
445 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
446 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
447 let result = m.filter(&ManifestFilter::All);
448 let timestamps: Vec<u64> = result.iter().map(|e| e.added_at_secs).collect();
449 assert_eq!(timestamps, vec![1000, 2000, 3000]);
450 }
451
452 #[test]
457 fn test_stats_total_entries() {
458 let mut m = StorageBlockManifest::new();
459 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
460 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
461 assert_eq!(m.stats().total_entries, 2);
462 }
463
464 #[test]
465 fn test_stats_total_size_bytes() {
466 let mut m = StorageBlockManifest::new();
467 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
468 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
469 assert_eq!(m.stats().total_size_bytes, 300);
470 }
471
472 #[test]
473 fn test_stats_pinned_count() {
474 let mut m = StorageBlockManifest::new();
475 m.add_entry(make_entry("cid1", 100, "raw", 1000, true));
476 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
477 m.add_entry(make_entry("cid3", 300, "raw", 3000, true));
478 assert_eq!(m.stats().pinned_count, 2);
479 }
480
481 #[test]
482 fn test_stats_content_type_counts() {
483 let mut m = StorageBlockManifest::new();
484 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
485 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
486 m.add_entry(make_entry("cid3", 300, "dag-cbor", 3000, false));
487 let stats = m.stats();
488 assert_eq!(*stats.content_type_counts.get("raw").unwrap_or(&0), 2);
489 assert_eq!(*stats.content_type_counts.get("dag-cbor").unwrap_or(&0), 1);
490 }
491
492 #[test]
497 fn test_merge_adds_missing_entries() {
498 let mut m1 = StorageBlockManifest::new();
499 m1.add_entry(make_entry("cid1", 100, "raw", 1000, false));
500
501 let mut m2 = StorageBlockManifest::new();
502 m2.add_entry(make_entry("cid2", 200, "raw", 2000, false));
503
504 m1.merge(&m2);
505 assert!(m1.get_entry("cid2").is_some());
506 assert_eq!(m1.entries.len(), 2);
507 }
508
509 #[test]
510 fn test_merge_propagates_pin_from_other() {
511 let mut m1 = StorageBlockManifest::new();
512 m1.add_entry(make_entry("cid1", 100, "raw", 1000, false));
513
514 let mut m2 = StorageBlockManifest::new();
515 m2.add_entry(make_entry("cid1", 100, "raw", 1000, true));
516
517 m1.merge(&m2);
518 assert!(m1.get_entry("cid1").unwrap().pinned);
519 }
520
521 #[test]
522 fn test_merge_does_not_unpin_existing() {
523 let mut m1 = StorageBlockManifest::new();
524 m1.add_entry(make_entry("cid1", 100, "raw", 1000, true));
525
526 let mut m2 = StorageBlockManifest::new();
527 m2.add_entry(make_entry("cid1", 100, "raw", 1000, false));
528
529 m1.merge(&m2);
530 assert!(m1.get_entry("cid1").unwrap().pinned);
532 }
533
534 #[test]
539 fn test_export_cids_sorted() {
540 let mut m = StorageBlockManifest::new();
541 m.add_entry(make_entry("cidZ", 100, "raw", 1000, false));
542 m.add_entry(make_entry("cidA", 200, "raw", 2000, false));
543 m.add_entry(make_entry("cidM", 300, "raw", 3000, false));
544 let cids = m.export_cids();
545 assert_eq!(cids, vec!["cidA", "cidM", "cidZ"]);
546 }
547
548 #[test]
553 fn test_verify_checksums_correct_entries_pass() {
554 let mut m = StorageBlockManifest::new();
555 m.add_entry(make_entry("cid1", 100, "raw", 1000, false));
556 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
557 assert!(m.verify_checksums().is_empty());
558 }
559
560 #[test]
561 fn test_verify_checksums_detects_mismatch() {
562 let mut m = StorageBlockManifest::new();
563 let mut bad = make_entry("cid1", 100, "raw", 1000, false);
564 bad.checksum = bad.checksum.wrapping_add(1); m.add_entry(bad);
566 m.add_entry(make_entry("cid2", 200, "raw", 2000, false));
567 let mismatches = m.verify_checksums();
568 assert_eq!(mismatches, vec!["cid1"]);
569 }
570
571 #[test]
576 fn test_fnv1a_empty_input() {
577 assert_eq!(fnv1a(b""), 14_695_981_039_346_656_037);
579 }
580
581 #[test]
582 fn test_fnv1a_deterministic() {
583 let h1 = fnv1a(b"hello");
584 let h2 = fnv1a(b"hello");
585 assert_eq!(h1, h2);
586 }
587
588 #[test]
589 fn test_fnv1a_different_inputs() {
590 assert_ne!(fnv1a(b"foo"), fnv1a(b"bar"));
591 }
592}