Skip to main content

ipfrs_storage/
block_fragment_store.rs

1//! Block Fragment Store
2//!
3//! Stores and reassembles fragmented blocks, supporting parallel fragment reception
4//! and erasure recovery. Fragments are identified by their block CID and index.
5//! Once all fragments for a block are received, they are assembled into the complete
6//! block by concatenating fragments in index order after verifying checksums.
7
8use std::collections::HashMap;
9
10// ---------------------------------------------------------------------------
11// FNV-1a helpers
12// ---------------------------------------------------------------------------
13
14/// FNV-1a 32-bit checksum (lower 32 bits of the 64-bit digest).
15pub fn bfs_fnv1a_32(data: &[u8]) -> u32 {
16    let mut h: u64 = 14_695_981_039_346_656_037;
17    for &b in data {
18        h ^= b as u64;
19        h = h.wrapping_mul(1_099_511_628_211);
20    }
21    h as u32
22}
23
24// ---------------------------------------------------------------------------
25// Core types
26// ---------------------------------------------------------------------------
27
28/// Uniquely identifies one fragment within a block.
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct FragmentId {
31    /// The CID of the block this fragment belongs to.
32    pub block_cid: String,
33    /// Zero-based index of this fragment within the block.
34    pub index: u32,
35}
36
37/// A single fragment carrying a slice of a block's data.
38#[derive(Debug, Clone)]
39pub struct BfsFragment {
40    /// Identity of this fragment.
41    pub id: FragmentId,
42    /// Raw bytes for this fragment.
43    pub data: Vec<u8>,
44    /// Total number of fragments required to reassemble the block.
45    pub total_fragments: u32,
46    /// FNV-1a-32 checksum of `data`.
47    pub checksum: u32,
48}
49
50impl BfsFragment {
51    /// Construct a new fragment, computing its checksum automatically.
52    pub fn new(
53        block_cid: impl Into<String>,
54        index: u32,
55        data: Vec<u8>,
56        total_fragments: u32,
57    ) -> Self {
58        let checksum = bfs_fnv1a_32(&data);
59        BfsFragment {
60            id: FragmentId {
61                block_cid: block_cid.into(),
62                index,
63            },
64            data,
65            total_fragments,
66            checksum,
67        }
68    }
69}
70
71/// Current state of a fragment set.
72#[derive(Debug, Clone, PartialEq)]
73pub enum FragmentSetState {
74    /// Not all fragments have been received yet.
75    Incomplete {
76        /// Number of fragments received so far.
77        received: u32,
78        /// Total fragments expected.
79        total: u32,
80    },
81    /// All fragments have been received and the block has been assembled.
82    Complete,
83    /// All fragments received but some checksums failed.
84    Corrupted {
85        /// Indices of fragments whose checksums do not match.
86        bad_indices: Vec<u32>,
87    },
88}
89
90/// A collection of fragments for one block.
91#[derive(Debug, Clone)]
92pub struct FragmentSet {
93    /// CID of the block being assembled.
94    pub block_cid: String,
95    /// Expected total fragment count.
96    pub total_fragments: u32,
97    /// Received fragments keyed by index.
98    pub fragments: HashMap<u32, BfsFragment>,
99    /// Timestamp (ms) when the first fragment arrived.
100    pub created_at: u64,
101    /// Timestamp (ms) of the most recent fragment arrival.
102    pub last_updated: u64,
103}
104
105impl FragmentSet {
106    fn new(block_cid: String, total_fragments: u32, now: u64) -> Self {
107        FragmentSet {
108            block_cid,
109            total_fragments,
110            fragments: HashMap::new(),
111            created_at: now,
112            last_updated: now,
113        }
114    }
115
116    /// Whether all fragments are present.
117    fn is_complete(&self) -> bool {
118        self.fragments.len() as u32 == self.total_fragments
119    }
120
121    /// Current state (does not perform assembly).
122    fn state(&self) -> FragmentSetState {
123        if self.is_complete() {
124            // Verify checksums to decide Complete vs Corrupted.
125            let bad: Vec<u32> = self
126                .fragments
127                .iter()
128                .filter(|(_, f)| bfs_fnv1a_32(&f.data) != f.checksum)
129                .map(|(idx, _)| *idx)
130                .collect();
131            if bad.is_empty() {
132                FragmentSetState::Complete
133            } else {
134                let mut sorted = bad;
135                sorted.sort_unstable();
136                FragmentSetState::Corrupted {
137                    bad_indices: sorted,
138                }
139            }
140        } else {
141            FragmentSetState::Incomplete {
142                received: self.fragments.len() as u32,
143                total: self.total_fragments,
144            }
145        }
146    }
147}
148
149/// A fully assembled block produced from its constituent fragments.
150#[derive(Debug, Clone)]
151pub struct AssembledBlock {
152    /// CID of this block.
153    pub cid: String,
154    /// Complete block data (fragments concatenated in index order).
155    pub data: Vec<u8>,
156    /// Number of fragments that were assembled.
157    pub fragment_count: u32,
158    /// Timestamp (ms) when this block was assembled.
159    pub assembled_at: u64,
160}
161
162// ---------------------------------------------------------------------------
163// Errors
164// ---------------------------------------------------------------------------
165
166/// Errors produced by [`BlockFragmentStore`] operations.
167#[derive(Debug, Clone, PartialEq)]
168pub enum FragmentError {
169    /// A fragment with this (block_cid, index) already exists in the store.
170    DuplicateFragment {
171        /// Block CID.
172        block_cid: String,
173        /// Fragment index.
174        index: u32,
175    },
176    /// The fragment index is out of range for the declared total.
177    IndexOutOfRange {
178        /// Fragment index received.
179        index: u32,
180        /// Total fragments declared.
181        total: u32,
182    },
183    /// No fragment set found for the given block CID.
184    BlockNotFound(String),
185    /// Assembly requested but not all fragments have been received.
186    AssemblyIncomplete {
187        /// Fragments received so far.
188        received: u32,
189        /// Total fragments expected.
190        total: u32,
191    },
192    /// A fragment failed its checksum during assembly.
193    ChecksumMismatch {
194        /// Index of the offending fragment.
195        index: u32,
196    },
197    /// The pending-set or assembled-block cap has been reached.
198    StoreFull,
199}
200
201impl std::fmt::Display for FragmentError {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            FragmentError::DuplicateFragment { block_cid, index } => {
205                write!(f, "duplicate fragment {index} for block {block_cid}")
206            }
207            FragmentError::IndexOutOfRange { index, total } => {
208                write!(f, "fragment index {index} out of range (total={total})")
209            }
210            FragmentError::BlockNotFound(cid) => {
211                write!(f, "block not found: {cid}")
212            }
213            FragmentError::AssemblyIncomplete { received, total } => {
214                write!(f, "assembly incomplete: {received}/{total} fragments")
215            }
216            FragmentError::ChecksumMismatch { index } => {
217                write!(f, "checksum mismatch for fragment {index}")
218            }
219            FragmentError::StoreFull => write!(f, "store is full"),
220        }
221    }
222}
223
224impl std::error::Error for FragmentError {}
225
226// ---------------------------------------------------------------------------
227// Stats
228// ---------------------------------------------------------------------------
229
230/// Aggregate statistics about the store.
231#[derive(Debug, Clone)]
232pub struct FragmentStats {
233    /// Number of in-progress (pending) fragment sets.
234    pub pending_sets: usize,
235    /// Number of successfully assembled blocks in cache.
236    pub assembled_blocks: usize,
237    /// Total number of individual fragments currently stored (pending only).
238    pub total_fragments_stored: usize,
239    /// Average completion ratio across all pending sets (0.0–1.0).
240    pub avg_completion_rate: f64,
241}
242
243// ---------------------------------------------------------------------------
244// Store
245// ---------------------------------------------------------------------------
246
247/// Production-grade store for fragmented blocks.
248///
249/// Handles concurrent fragment arrival, checksum verification, automatic assembly
250/// on completion, LRU-style eviction of the oldest pending sets when the cap is
251/// exceeded, and a bounded assembled-block cache.
252#[derive(Debug)]
253pub struct BlockFragmentStore {
254    /// Pending (incomplete) fragment sets keyed by block CID.
255    sets: HashMap<String, FragmentSet>,
256    /// Assembled (complete) blocks keyed by block CID.
257    assembled: HashMap<String, AssembledBlock>,
258    /// Maximum number of pending fragment sets before oldest is evicted.
259    max_pending: usize,
260    /// Maximum number of assembled blocks to cache.
261    max_assembled: usize,
262}
263
264impl BlockFragmentStore {
265    // -----------------------------------------------------------------------
266    // Construction
267    // -----------------------------------------------------------------------
268
269    /// Create a new store with the given capacity limits.
270    ///
271    /// * `max_pending`  – maximum number of in-progress fragment sets.
272    /// * `max_assembled` – maximum number of assembled blocks kept in cache.
273    pub fn new(max_pending: usize, max_assembled: usize) -> Self {
274        BlockFragmentStore {
275            sets: HashMap::new(),
276            assembled: HashMap::new(),
277            max_pending,
278            max_assembled,
279        }
280    }
281
282    // -----------------------------------------------------------------------
283    // Fragment storage
284    // -----------------------------------------------------------------------
285
286    /// Store a fragment.
287    ///
288    /// If the fragment completes its set, assembly is attempted automatically
289    /// and the pending set is removed.  If `max_pending` would be exceeded when
290    /// creating a new set, the oldest pending set (by `created_at`) is dropped.
291    ///
292    /// Returns the current [`FragmentSetState`] after the operation.
293    pub fn store_fragment(
294        &mut self,
295        fragment: BfsFragment,
296        now: u64,
297    ) -> Result<FragmentSetState, FragmentError> {
298        let block_cid = fragment.id.block_cid.clone();
299        let index = fragment.id.index;
300        let total = fragment.total_fragments;
301
302        // Validate index range.
303        if total == 0 || index >= total {
304            return Err(FragmentError::IndexOutOfRange { index, total });
305        }
306
307        // If the block is already assembled, reject the fragment.
308        if self.assembled.contains_key(&block_cid) {
309            // Treat as duplicate: the block is already complete.
310            return Ok(FragmentSetState::Complete);
311        }
312
313        // Get or create the fragment set.
314        if !self.sets.contains_key(&block_cid) {
315            // Evict oldest pending set if we are at capacity.
316            if self.sets.len() >= self.max_pending {
317                self.evict_oldest_pending();
318                // If we still can't make room (e.g. max_pending == 0), error.
319                if self.sets.len() >= self.max_pending {
320                    return Err(FragmentError::StoreFull);
321                }
322            }
323            self.sets.insert(
324                block_cid.clone(),
325                FragmentSet::new(block_cid.clone(), total, now),
326            );
327        }
328
329        let set = self
330            .sets
331            .get_mut(&block_cid)
332            .ok_or_else(|| FragmentError::BlockNotFound(block_cid.clone()))?;
333
334        // Check for duplicate.
335        if set.fragments.contains_key(&index) {
336            return Err(FragmentError::DuplicateFragment { block_cid, index });
337        }
338
339        set.fragments.insert(index, fragment);
340        set.last_updated = now;
341
342        // Check if complete.
343        if set.is_complete() {
344            // Attempt assembly; this also validates checksums.
345            match self.assemble_internal(&block_cid, now) {
346                Ok(block) => {
347                    // Evict oldest assembled if at capacity.
348                    if self.assembled.len() >= self.max_assembled {
349                        self.evict_oldest_assembled();
350                    }
351                    self.assembled.insert(block_cid.clone(), block);
352                    self.sets.remove(&block_cid);
353                    Ok(FragmentSetState::Complete)
354                }
355                Err(FragmentError::ChecksumMismatch { index: bad_idx }) => {
356                    // Leave the set in place for potential repair.
357                    Ok(FragmentSetState::Corrupted {
358                        bad_indices: vec![bad_idx],
359                    })
360                }
361                Err(e) => Err(e),
362            }
363        } else {
364            let state = self
365                .sets
366                .get(&block_cid)
367                .map(|s| s.state())
368                .unwrap_or(FragmentSetState::Incomplete { received: 0, total });
369            Ok(state)
370        }
371    }
372
373    // -----------------------------------------------------------------------
374    // State inspection
375    // -----------------------------------------------------------------------
376
377    /// Return the current state of a fragment set.
378    ///
379    /// Returns `None` if neither a pending set nor an assembled block exists for
380    /// the given CID.
381    pub fn get_state(&self, block_cid: &str) -> Option<FragmentSetState> {
382        if self.assembled.contains_key(block_cid) {
383            return Some(FragmentSetState::Complete);
384        }
385        self.sets.get(block_cid).map(|s| s.state())
386    }
387
388    /// Return the indices of fragments that have not yet been received, in
389    /// ascending order.  Returns an empty vec if the block is assembled or
390    /// unknown.
391    pub fn missing_indices(&self, block_cid: &str) -> Vec<u32> {
392        if self.assembled.contains_key(block_cid) {
393            return Vec::new();
394        }
395        let set = match self.sets.get(block_cid) {
396            Some(s) => s,
397            None => return Vec::new(),
398        };
399        (0..set.total_fragments)
400            .filter(|i| !set.fragments.contains_key(i))
401            .collect()
402    }
403
404    // -----------------------------------------------------------------------
405    // Assembly
406    // -----------------------------------------------------------------------
407
408    /// Assemble a block from its fragments.
409    ///
410    /// All fragments must be present and pass checksum verification.  On success,
411    /// the assembled block is stored in the cache and the pending set is removed.
412    pub fn assemble(&mut self, block_cid: &str, now: u64) -> Result<AssembledBlock, FragmentError> {
413        if let Some(block) = self.assembled.get(block_cid) {
414            return Ok(block.clone());
415        }
416
417        let assembled = self.assemble_internal(block_cid, now)?;
418
419        if self.assembled.len() >= self.max_assembled {
420            self.evict_oldest_assembled();
421        }
422        let cid = block_cid.to_owned();
423        self.assembled.insert(cid.clone(), assembled.clone());
424        self.sets.remove(&cid);
425        Ok(assembled)
426    }
427
428    /// Internal assembly logic (does not mutate `self.assembled` or `self.sets`).
429    fn assemble_internal(
430        &self,
431        block_cid: &str,
432        now: u64,
433    ) -> Result<AssembledBlock, FragmentError> {
434        let set = self
435            .sets
436            .get(block_cid)
437            .ok_or_else(|| FragmentError::BlockNotFound(block_cid.to_owned()))?;
438
439        if !set.is_complete() {
440            return Err(FragmentError::AssemblyIncomplete {
441                received: set.fragments.len() as u32,
442                total: set.total_fragments,
443            });
444        }
445
446        // Sort indices and verify.
447        let mut indices: Vec<u32> = (0..set.total_fragments).collect();
448        indices.sort_unstable();
449
450        let mut data: Vec<u8> = Vec::new();
451        for idx in &indices {
452            let fragment = set
453                .fragments
454                .get(idx)
455                .ok_or_else(|| FragmentError::BlockNotFound(block_cid.to_owned()))?;
456            if bfs_fnv1a_32(&fragment.data) != fragment.checksum {
457                return Err(FragmentError::ChecksumMismatch { index: *idx });
458            }
459            data.extend_from_slice(&fragment.data);
460        }
461
462        Ok(AssembledBlock {
463            cid: block_cid.to_owned(),
464            data,
465            fragment_count: set.total_fragments,
466            assembled_at: now,
467        })
468    }
469
470    // -----------------------------------------------------------------------
471    // Assembled block retrieval
472    // -----------------------------------------------------------------------
473
474    /// Return a reference to the data of an assembled block, or `None` if not
475    /// present in the cache.
476    pub fn get_assembled(&self, block_cid: &str) -> Option<&[u8]> {
477        self.assembled.get(block_cid).map(|b| b.data.as_slice())
478    }
479
480    /// Return a reference to the full [`AssembledBlock`] struct, if cached.
481    pub fn get_assembled_block(&self, block_cid: &str) -> Option<&AssembledBlock> {
482        self.assembled.get(block_cid)
483    }
484
485    // -----------------------------------------------------------------------
486    // Verification helpers
487    // -----------------------------------------------------------------------
488
489    /// Return `true` if the fragment's checksum matches the recomputed value.
490    pub fn verify_fragment(fragment: &BfsFragment) -> bool {
491        bfs_fnv1a_32(&fragment.data) == fragment.checksum
492    }
493
494    /// Return the indices of fragments in a set whose checksums do not match.
495    pub fn verify_set(set: &FragmentSet) -> Vec<u32> {
496        let mut bad: Vec<u32> = set
497            .fragments
498            .iter()
499            .filter(|(_, f)| bfs_fnv1a_32(&f.data) != f.checksum)
500            .map(|(idx, _)| *idx)
501            .collect();
502        bad.sort_unstable();
503        bad
504    }
505
506    // -----------------------------------------------------------------------
507    // Eviction
508    // -----------------------------------------------------------------------
509
510    /// Remove all pending (incomplete) sets whose `created_at` is older than
511    /// `max_age_ms` milliseconds before `now`.  Returns the number of sets
512    /// removed.
513    pub fn evict_stale_pending(&mut self, max_age_ms: u64, now: u64) -> usize {
514        let cutoff = now.saturating_sub(max_age_ms);
515        let before = self.sets.len();
516        self.sets.retain(|_, set| set.created_at >= cutoff);
517        before - self.sets.len()
518    }
519
520    /// Remove an assembled block from the cache by CID.  Returns `true` if the
521    /// block was present and removed.
522    pub fn evict_assembled(&mut self, cid: &str) -> bool {
523        self.assembled.remove(cid).is_some()
524    }
525
526    /// Evict the oldest pending set (by `created_at` timestamp).
527    fn evict_oldest_pending(&mut self) {
528        let oldest = self
529            .sets
530            .iter()
531            .min_by_key(|(_, s)| s.created_at)
532            .map(|(k, _)| k.clone());
533        if let Some(key) = oldest {
534            self.sets.remove(&key);
535        }
536    }
537
538    /// Evict the oldest assembled block (by `assembled_at` timestamp).
539    fn evict_oldest_assembled(&mut self) {
540        let oldest = self
541            .assembled
542            .iter()
543            .min_by_key(|(_, b)| b.assembled_at)
544            .map(|(k, _)| k.clone());
545        if let Some(key) = oldest {
546            self.assembled.remove(&key);
547        }
548    }
549
550    // -----------------------------------------------------------------------
551    // Counts
552    // -----------------------------------------------------------------------
553
554    /// Number of pending (incomplete) fragment sets.
555    pub fn pending_count(&self) -> usize {
556        self.sets.len()
557    }
558
559    /// Number of assembled blocks in the cache.
560    pub fn assembled_count(&self) -> usize {
561        self.assembled.len()
562    }
563
564    // -----------------------------------------------------------------------
565    // Statistics
566    // -----------------------------------------------------------------------
567
568    /// Aggregate statistics about the store.
569    pub fn stats(&self) -> FragmentStats {
570        let pending_sets = self.sets.len();
571        let assembled_blocks = self.assembled.len();
572
573        let total_fragments_stored: usize = self.sets.values().map(|s| s.fragments.len()).sum();
574
575        let avg_completion_rate = if pending_sets == 0 {
576            0.0
577        } else {
578            let sum: f64 = self
579                .sets
580                .values()
581                .map(|s| {
582                    if s.total_fragments == 0 {
583                        0.0
584                    } else {
585                        s.fragments.len() as f64 / s.total_fragments as f64
586                    }
587                })
588                .sum();
589            sum / pending_sets as f64
590        };
591
592        FragmentStats {
593            pending_sets,
594            assembled_blocks,
595            total_fragments_stored,
596            avg_completion_rate,
597        }
598    }
599
600    // -----------------------------------------------------------------------
601    // Advanced / diagnostic helpers
602    // -----------------------------------------------------------------------
603
604    /// Return all pending fragment set CIDs.
605    pub fn pending_cids(&self) -> Vec<&str> {
606        self.sets.keys().map(String::as_str).collect()
607    }
608
609    /// Return all assembled block CIDs.
610    pub fn assembled_cids(&self) -> Vec<&str> {
611        self.assembled.keys().map(String::as_str).collect()
612    }
613
614    /// Clear all pending sets without assembling them.  Returns the number of
615    /// sets dropped.
616    pub fn clear_pending(&mut self) -> usize {
617        let count = self.sets.len();
618        self.sets.clear();
619        count
620    }
621
622    /// Clear all assembled blocks from the cache.  Returns the number of blocks
623    /// dropped.
624    pub fn clear_assembled(&mut self) -> usize {
625        let count = self.assembled.len();
626        self.assembled.clear();
627        count
628    }
629
630    /// Return how many more pending sets can be accepted before the cap is hit.
631    pub fn pending_capacity_remaining(&self) -> usize {
632        self.max_pending.saturating_sub(self.sets.len())
633    }
634}
635
636// ===========================================================================
637// Tests
638// ===========================================================================
639
640#[cfg(test)]
641mod tests {
642    use crate::block_fragment_store::{
643        bfs_fnv1a_32, BfsFragment, BlockFragmentStore, FragmentError, FragmentId, FragmentSet,
644        FragmentSetState,
645    };
646
647    // -----------------------------------------------------------------------
648    // Helpers
649    // -----------------------------------------------------------------------
650
651    fn make_fragment(cid: &str, index: u32, data: &[u8], total: u32) -> BfsFragment {
652        BfsFragment::new(cid, index, data.to_vec(), total)
653    }
654
655    fn make_fragment_tampered(cid: &str, index: u32, data: &[u8], total: u32) -> BfsFragment {
656        let mut f = make_fragment(cid, index, data, total);
657        f.checksum = f.checksum.wrapping_add(1); // corrupt checksum
658        f
659    }
660
661    fn store_with_defaults() -> BlockFragmentStore {
662        BlockFragmentStore::new(16, 16)
663    }
664
665    // -----------------------------------------------------------------------
666    // 1. FNV-1a checksum correctness
667    // -----------------------------------------------------------------------
668
669    #[test]
670    fn test_fnv1a_empty() {
671        // FNV-1a of empty slice should equal the FNV offset basis cast to u32.
672        let h = bfs_fnv1a_32(&[]);
673        let expected = 14_695_981_039_346_656_037_u64 as u32;
674        assert_eq!(h, expected);
675    }
676
677    #[test]
678    fn test_fnv1a_known_value() {
679        // "abc" -> known FNV-1a-64 value 0xe71fa2190541574b, lower 32 bits = 0x0541574b
680        let h = bfs_fnv1a_32(b"abc");
681        assert_eq!(h, 0x0541_574b_u32);
682    }
683
684    #[test]
685    fn test_fnv1a_different_inputs_differ() {
686        assert_ne!(bfs_fnv1a_32(b"hello"), bfs_fnv1a_32(b"world"));
687    }
688
689    // -----------------------------------------------------------------------
690    // 2. Fragment construction
691    // -----------------------------------------------------------------------
692
693    #[test]
694    fn test_fragment_new_computes_checksum() {
695        let data = b"test data";
696        let f = BfsFragment::new("cid1", 0, data.to_vec(), 3);
697        assert_eq!(f.checksum, bfs_fnv1a_32(data));
698    }
699
700    #[test]
701    fn test_fragment_id_equality() {
702        let a = FragmentId {
703            block_cid: "x".into(),
704            index: 1,
705        };
706        let b = FragmentId {
707            block_cid: "x".into(),
708            index: 1,
709        };
710        assert_eq!(a, b);
711    }
712
713    // -----------------------------------------------------------------------
714    // 3. Basic store_fragment / happy path
715    // -----------------------------------------------------------------------
716
717    #[test]
718    fn test_store_single_fragment_incomplete() {
719        let mut store = store_with_defaults();
720        let f = make_fragment("cid1", 0, b"part0", 3);
721        let state = store.store_fragment(f, 1000).expect("store ok");
722        assert_eq!(
723            state,
724            FragmentSetState::Incomplete {
725                received: 1,
726                total: 3
727            }
728        );
729    }
730
731    #[test]
732    fn test_store_all_fragments_completes() {
733        let mut store = store_with_defaults();
734        store
735            .store_fragment(make_fragment("cid1", 0, b"aa", 2), 100)
736            .expect("ok");
737        let state = store
738            .store_fragment(make_fragment("cid1", 1, b"bb", 2), 200)
739            .expect("ok");
740        assert_eq!(state, FragmentSetState::Complete);
741    }
742
743    #[test]
744    fn test_assembled_block_data_correct() {
745        let mut store = store_with_defaults();
746        store
747            .store_fragment(make_fragment("cid1", 0, b"hello", 2), 1)
748            .expect("ok");
749        store
750            .store_fragment(make_fragment("cid1", 1, b"world", 2), 2)
751            .expect("ok");
752        let data = store.get_assembled("cid1").expect("assembled");
753        assert_eq!(data, b"helloworld");
754    }
755
756    #[test]
757    fn test_fragments_ordered_by_index() {
758        let mut store = store_with_defaults();
759        // Insert out of order
760        store
761            .store_fragment(make_fragment("cid2", 2, b"C", 3), 1)
762            .expect("ok");
763        store
764            .store_fragment(make_fragment("cid2", 0, b"A", 3), 2)
765            .expect("ok");
766        store
767            .store_fragment(make_fragment("cid2", 1, b"B", 3), 3)
768            .expect("ok");
769        let data = store.get_assembled("cid2").expect("assembled");
770        assert_eq!(data, b"ABC");
771    }
772
773    // -----------------------------------------------------------------------
774    // 4. Error cases
775    // -----------------------------------------------------------------------
776
777    #[test]
778    fn test_duplicate_fragment_error() {
779        let mut store = store_with_defaults();
780        store
781            .store_fragment(make_fragment("cid1", 0, b"a", 2), 1)
782            .expect("ok");
783        let err = store
784            .store_fragment(make_fragment("cid1", 0, b"a", 2), 2)
785            .expect_err("dup");
786        assert_eq!(
787            err,
788            FragmentError::DuplicateFragment {
789                block_cid: "cid1".into(),
790                index: 0
791            }
792        );
793    }
794
795    #[test]
796    fn test_index_out_of_range_error() {
797        let mut store = store_with_defaults();
798        let f = make_fragment("cid1", 5, b"x", 3);
799        let err = store.store_fragment(f, 1).expect_err("oob");
800        assert_eq!(err, FragmentError::IndexOutOfRange { index: 5, total: 3 });
801    }
802
803    #[test]
804    fn test_total_fragments_zero_error() {
805        let mut store = store_with_defaults();
806        let f = BfsFragment {
807            id: FragmentId {
808                block_cid: "cid1".into(),
809                index: 0,
810            },
811            data: vec![1],
812            total_fragments: 0,
813            checksum: 0,
814        };
815        let err = store.store_fragment(f, 1).expect_err("zero total");
816        assert_eq!(err, FragmentError::IndexOutOfRange { index: 0, total: 0 });
817    }
818
819    #[test]
820    fn test_block_not_found_for_state() {
821        let store = store_with_defaults();
822        assert!(store.get_state("nonexistent").is_none());
823    }
824
825    #[test]
826    fn test_assemble_not_found_error() {
827        let mut store = store_with_defaults();
828        let err = store.assemble("ghost", 0).expect_err("not found");
829        assert_eq!(err, FragmentError::BlockNotFound("ghost".into()));
830    }
831
832    #[test]
833    fn test_assemble_incomplete_error() {
834        let mut store = store_with_defaults();
835        store
836            .store_fragment(make_fragment("cid1", 0, b"a", 3), 1)
837            .expect("ok");
838        let err = store.assemble("cid1", 2).expect_err("incomplete");
839        assert_eq!(
840            err,
841            FragmentError::AssemblyIncomplete {
842                received: 1,
843                total: 3
844            }
845        );
846    }
847
848    // -----------------------------------------------------------------------
849    // 5. Checksum / corruption
850    // -----------------------------------------------------------------------
851
852    #[test]
853    fn test_verify_fragment_valid() {
854        let f = make_fragment("cid", 0, b"data", 1);
855        assert!(BlockFragmentStore::verify_fragment(&f));
856    }
857
858    #[test]
859    fn test_verify_fragment_invalid() {
860        let f = make_fragment_tampered("cid", 0, b"data", 1);
861        assert!(!BlockFragmentStore::verify_fragment(&f));
862    }
863
864    #[test]
865    fn test_verify_set_all_valid() {
866        let mut set = FragmentSet::new("cid".into(), 2, 0);
867        set.fragments.insert(0, make_fragment("cid", 0, b"a", 2));
868        set.fragments.insert(1, make_fragment("cid", 1, b"b", 2));
869        assert!(BlockFragmentStore::verify_set(&set).is_empty());
870    }
871
872    #[test]
873    fn test_verify_set_one_bad() {
874        let mut set = FragmentSet::new("cid".into(), 2, 0);
875        set.fragments.insert(0, make_fragment("cid", 0, b"a", 2));
876        set.fragments
877            .insert(1, make_fragment_tampered("cid", 1, b"b", 2));
878        let bad = BlockFragmentStore::verify_set(&set);
879        assert_eq!(bad, vec![1u32]);
880    }
881
882    #[test]
883    fn test_corrupted_state_on_bad_checksum() {
884        let mut store = store_with_defaults();
885        store
886            .store_fragment(make_fragment("cid1", 0, b"good", 2), 1)
887            .expect("ok");
888        let state = store
889            .store_fragment(make_fragment_tampered("cid1", 1, b"bad", 2), 2)
890            .expect("stored despite tamper");
891        // Depending on implementation the state should reflect corruption.
892        match state {
893            FragmentSetState::Corrupted { bad_indices } => {
894                assert!(bad_indices.contains(&1));
895            }
896            // If the set remains pending it's also acceptable since checksum
897            // will be caught at assembly time.
898            FragmentSetState::Incomplete { .. } => {}
899            FragmentSetState::Complete => panic!("should not be complete with bad checksum"),
900        }
901    }
902
903    #[test]
904    fn test_assemble_detects_checksum_mismatch() {
905        let mut store = BlockFragmentStore::new(4, 4);
906        // Manually insert a tampered fragment by first storing valid then
907        // using a low-level approach: store both, manipulate the set directly.
908        // Instead, build a set with a tampered fragment via store_fragment
909        // for the valid one, then call assemble on an incomplete set (to get the
910        // pending entry), then add a tampered one.
911        store
912            .store_fragment(make_fragment("cid1", 0, b"ok", 2), 1)
913            .expect("ok");
914        // Insert tampered fragment directly:
915        let tampered = make_fragment_tampered("cid1", 1, b"bad", 2);
916        // store_fragment will call assemble internally; we expect a Corrupted or
917        // a ChecksumMismatch error propagated.
918        let result = store.store_fragment(tampered, 2);
919        match result {
920            Ok(FragmentSetState::Corrupted { .. }) => {}  // expected
921            Ok(FragmentSetState::Incomplete { .. }) => {} // tolerated
922            Ok(FragmentSetState::Complete) => panic!("should not complete with bad checksum"),
923            Err(FragmentError::ChecksumMismatch { .. }) => {} // also acceptable
924            Err(e) => panic!("unexpected error: {e}"),
925        }
926    }
927
928    // -----------------------------------------------------------------------
929    // 6. Missing indices
930    // -----------------------------------------------------------------------
931
932    #[test]
933    fn test_missing_indices_all_missing() {
934        let mut store = store_with_defaults();
935        store
936            .store_fragment(make_fragment("cid1", 0, b"a", 4), 1)
937            .expect("ok");
938        let missing = store.missing_indices("cid1");
939        assert_eq!(missing, vec![1u32, 2, 3]);
940    }
941
942    #[test]
943    fn test_missing_indices_none_when_assembled() {
944        let mut store = store_with_defaults();
945        store
946            .store_fragment(make_fragment("cid1", 0, b"a", 1), 1)
947            .expect("ok");
948        let missing = store.missing_indices("cid1");
949        assert!(missing.is_empty());
950    }
951
952    #[test]
953    fn test_missing_indices_unknown_cid() {
954        let store = store_with_defaults();
955        assert!(store.missing_indices("ghost").is_empty());
956    }
957
958    // -----------------------------------------------------------------------
959    // 7. get_state
960    // -----------------------------------------------------------------------
961
962    #[test]
963    fn test_get_state_after_assembly() {
964        let mut store = store_with_defaults();
965        store
966            .store_fragment(make_fragment("cid1", 0, b"x", 1), 1)
967            .expect("ok");
968        assert_eq!(store.get_state("cid1"), Some(FragmentSetState::Complete));
969    }
970
971    #[test]
972    fn test_get_state_incomplete() {
973        let mut store = store_with_defaults();
974        store
975            .store_fragment(make_fragment("cid1", 0, b"x", 3), 1)
976            .expect("ok");
977        let state = store.get_state("cid1").expect("exists");
978        assert_eq!(
979            state,
980            FragmentSetState::Incomplete {
981                received: 1,
982                total: 3
983            }
984        );
985    }
986
987    // -----------------------------------------------------------------------
988    // 8. Eviction – stale pending
989    // -----------------------------------------------------------------------
990
991    #[test]
992    fn test_evict_stale_pending_removes_old() {
993        let mut store = store_with_defaults();
994        store
995            .store_fragment(make_fragment("old_cid", 0, b"a", 2), 0)
996            .expect("ok");
997        store
998            .store_fragment(make_fragment("new_cid", 0, b"b", 2), 5000)
999            .expect("ok");
1000        // Evict sets older than 2000ms, simulating now=6000
1001        let removed = store.evict_stale_pending(2000, 6000);
1002        assert_eq!(removed, 1);
1003        assert!(store.get_state("old_cid").is_none());
1004        assert!(store.get_state("new_cid").is_some());
1005    }
1006
1007    #[test]
1008    fn test_evict_stale_pending_no_removal_when_fresh() {
1009        let mut store = store_with_defaults();
1010        store
1011            .store_fragment(make_fragment("cid1", 0, b"a", 2), 5000)
1012            .expect("ok");
1013        let removed = store.evict_stale_pending(10000, 6000);
1014        assert_eq!(removed, 0);
1015    }
1016
1017    // -----------------------------------------------------------------------
1018    // 9. Eviction – assembled cache
1019    // -----------------------------------------------------------------------
1020
1021    #[test]
1022    fn test_evict_assembled_removes_block() {
1023        let mut store = store_with_defaults();
1024        store
1025            .store_fragment(make_fragment("cid1", 0, b"x", 1), 1)
1026            .expect("ok");
1027        assert!(store.get_assembled("cid1").is_some());
1028        let removed = store.evict_assembled("cid1");
1029        assert!(removed);
1030        assert!(store.get_assembled("cid1").is_none());
1031    }
1032
1033    #[test]
1034    fn test_evict_assembled_missing_returns_false() {
1035        let mut store = store_with_defaults();
1036        assert!(!store.evict_assembled("ghost"));
1037    }
1038
1039    // -----------------------------------------------------------------------
1040    // 10. Capacity / pending count
1041    // -----------------------------------------------------------------------
1042
1043    #[test]
1044    fn test_pending_count_increments() {
1045        let mut store = store_with_defaults();
1046        store
1047            .store_fragment(make_fragment("a", 0, b"x", 2), 1)
1048            .expect("ok");
1049        store
1050            .store_fragment(make_fragment("b", 0, b"y", 2), 2)
1051            .expect("ok");
1052        assert_eq!(store.pending_count(), 2);
1053    }
1054
1055    #[test]
1056    fn test_assembled_count_increments() {
1057        let mut store = store_with_defaults();
1058        store
1059            .store_fragment(make_fragment("a", 0, b"x", 1), 1)
1060            .expect("ok");
1061        store
1062            .store_fragment(make_fragment("b", 0, b"y", 1), 2)
1063            .expect("ok");
1064        assert_eq!(store.assembled_count(), 2);
1065    }
1066
1067    #[test]
1068    fn test_max_pending_evicts_oldest() {
1069        let mut store = BlockFragmentStore::new(2, 16);
1070        store
1071            .store_fragment(make_fragment("a", 0, b"x", 2), 1)
1072            .expect("ok");
1073        store
1074            .store_fragment(make_fragment("b", 0, b"y", 2), 2)
1075            .expect("ok");
1076        // Adding a third should evict the oldest ("a", created_at=1).
1077        store
1078            .store_fragment(make_fragment("c", 0, b"z", 2), 3)
1079            .expect("ok");
1080        assert_eq!(store.pending_count(), 2);
1081        assert!(store.get_state("a").is_none(), "oldest should be evicted");
1082    }
1083
1084    #[test]
1085    fn test_store_full_error_when_max_pending_zero() {
1086        let mut store = BlockFragmentStore::new(0, 16);
1087        let err = store
1088            .store_fragment(make_fragment("cid1", 0, b"x", 2), 1)
1089            .expect_err("store full");
1090        assert_eq!(err, FragmentError::StoreFull);
1091    }
1092
1093    // -----------------------------------------------------------------------
1094    // 11. Stats
1095    // -----------------------------------------------------------------------
1096
1097    #[test]
1098    fn test_stats_empty_store() {
1099        let store = store_with_defaults();
1100        let s = store.stats();
1101        assert_eq!(s.pending_sets, 0);
1102        assert_eq!(s.assembled_blocks, 0);
1103        assert_eq!(s.total_fragments_stored, 0);
1104        assert_eq!(s.avg_completion_rate, 0.0);
1105    }
1106
1107    #[test]
1108    fn test_stats_with_pending() {
1109        let mut store = store_with_defaults();
1110        store
1111            .store_fragment(make_fragment("cid1", 0, b"a", 4), 1)
1112            .expect("ok");
1113        store
1114            .store_fragment(make_fragment("cid1", 1, b"b", 4), 2)
1115            .expect("ok");
1116        let s = store.stats();
1117        assert_eq!(s.pending_sets, 1);
1118        assert_eq!(s.total_fragments_stored, 2);
1119        let expected_rate = 2.0 / 4.0;
1120        assert!((s.avg_completion_rate - expected_rate).abs() < 1e-9);
1121    }
1122
1123    #[test]
1124    fn test_stats_assembled_counted() {
1125        let mut store = store_with_defaults();
1126        store
1127            .store_fragment(make_fragment("cid1", 0, b"z", 1), 1)
1128            .expect("ok");
1129        let s = store.stats();
1130        assert_eq!(s.assembled_blocks, 1);
1131        assert_eq!(s.pending_sets, 0);
1132    }
1133
1134    // -----------------------------------------------------------------------
1135    // 12. Multiple blocks
1136    // -----------------------------------------------------------------------
1137
1138    #[test]
1139    fn test_multiple_blocks_independent() {
1140        let mut store = store_with_defaults();
1141        for i in 0u32..5 {
1142            let cid = format!("block-{i}");
1143            for j in 0u32..3 {
1144                let data = format!("cid{i}-frag{j}");
1145                store
1146                    .store_fragment(
1147                        make_fragment(&cid, j, data.as_bytes(), 3),
1148                        (i * 10 + j) as u64,
1149                    )
1150                    .expect("ok");
1151            }
1152        }
1153        assert_eq!(store.assembled_count(), 5);
1154        assert_eq!(store.pending_count(), 0);
1155    }
1156
1157    // -----------------------------------------------------------------------
1158    // 13. Explicit assemble()
1159    // -----------------------------------------------------------------------
1160
1161    #[test]
1162    fn test_explicit_assemble_after_all_received() {
1163        let mut store = store_with_defaults();
1164        store
1165            .store_fragment(make_fragment("cid1", 0, b"X", 2), 1)
1166            .expect("ok");
1167        store
1168            .store_fragment(make_fragment("cid1", 1, b"Y", 2), 2)
1169            .expect("ok");
1170        // Block should already be assembled automatically; explicit assemble returns cached.
1171        let block = store.assemble("cid1", 99).expect("assembled");
1172        assert_eq!(block.data, b"XY");
1173    }
1174
1175    #[test]
1176    fn test_explicit_assemble_stores_to_cache() {
1177        let mut store = store_with_defaults();
1178        store
1179            .store_fragment(make_fragment("cid1", 0, b"P", 2), 1)
1180            .expect("ok");
1181        store
1182            .store_fragment(make_fragment("cid1", 1, b"Q", 2), 2)
1183            .expect("ok");
1184        let block = store.assemble("cid1", 10).expect("ok");
1185        assert_eq!(block.fragment_count, 2);
1186        // get_assembled should now return data.
1187        assert_eq!(store.get_assembled("cid1"), Some(b"PQ".as_slice()));
1188    }
1189
1190    // -----------------------------------------------------------------------
1191    // 14. Clear helpers
1192    // -----------------------------------------------------------------------
1193
1194    #[test]
1195    fn test_clear_pending() {
1196        let mut store = store_with_defaults();
1197        store
1198            .store_fragment(make_fragment("a", 0, b"x", 2), 1)
1199            .expect("ok");
1200        store
1201            .store_fragment(make_fragment("b", 0, b"y", 2), 2)
1202            .expect("ok");
1203        let dropped = store.clear_pending();
1204        assert_eq!(dropped, 2);
1205        assert_eq!(store.pending_count(), 0);
1206    }
1207
1208    #[test]
1209    fn test_clear_assembled() {
1210        let mut store = store_with_defaults();
1211        store
1212            .store_fragment(make_fragment("a", 0, b"x", 1), 1)
1213            .expect("ok");
1214        store
1215            .store_fragment(make_fragment("b", 0, b"y", 1), 2)
1216            .expect("ok");
1217        let dropped = store.clear_assembled();
1218        assert_eq!(dropped, 2);
1219        assert_eq!(store.assembled_count(), 0);
1220    }
1221
1222    // -----------------------------------------------------------------------
1223    // 15. Capacity remaining
1224    // -----------------------------------------------------------------------
1225
1226    #[test]
1227    fn test_pending_capacity_remaining() {
1228        let mut store = BlockFragmentStore::new(5, 16);
1229        assert_eq!(store.pending_capacity_remaining(), 5);
1230        store
1231            .store_fragment(make_fragment("a", 0, b"x", 2), 1)
1232            .expect("ok");
1233        assert_eq!(store.pending_capacity_remaining(), 4);
1234    }
1235
1236    // -----------------------------------------------------------------------
1237    // 16. AssembledBlock fields
1238    // -----------------------------------------------------------------------
1239
1240    #[test]
1241    fn test_assembled_block_fragment_count() {
1242        let mut store = store_with_defaults();
1243        for i in 0u32..4 {
1244            store
1245                .store_fragment(make_fragment("c", i, &[i as u8], 4), i as u64)
1246                .expect("ok");
1247        }
1248        let block = store.get_assembled_block("c").expect("assembled");
1249        assert_eq!(block.fragment_count, 4);
1250    }
1251
1252    #[test]
1253    fn test_assembled_block_cid_matches() {
1254        let mut store = store_with_defaults();
1255        store
1256            .store_fragment(make_fragment("myCid", 0, b"z", 1), 1)
1257            .expect("ok");
1258        let block = store.get_assembled_block("myCid").expect("assembled");
1259        assert_eq!(block.cid, "myCid");
1260    }
1261
1262    // -----------------------------------------------------------------------
1263    // 17. Pending CIDs / Assembled CIDs listing
1264    // -----------------------------------------------------------------------
1265
1266    #[test]
1267    fn test_pending_cids_listed() {
1268        let mut store = store_with_defaults();
1269        store
1270            .store_fragment(make_fragment("p1", 0, b"a", 2), 1)
1271            .expect("ok");
1272        store
1273            .store_fragment(make_fragment("p2", 0, b"b", 2), 2)
1274            .expect("ok");
1275        let mut cids = store.pending_cids();
1276        cids.sort();
1277        assert_eq!(cids, vec!["p1", "p2"]);
1278    }
1279
1280    #[test]
1281    fn test_assembled_cids_listed() {
1282        let mut store = store_with_defaults();
1283        store
1284            .store_fragment(make_fragment("done1", 0, b"x", 1), 1)
1285            .expect("ok");
1286        store
1287            .store_fragment(make_fragment("done2", 0, b"y", 1), 2)
1288            .expect("ok");
1289        let mut cids = store.assembled_cids();
1290        cids.sort();
1291        assert_eq!(cids, vec!["done1", "done2"]);
1292    }
1293
1294    // -----------------------------------------------------------------------
1295    // 18. Single-fragment blocks
1296    // -----------------------------------------------------------------------
1297
1298    #[test]
1299    fn test_single_fragment_block() {
1300        let mut store = store_with_defaults();
1301        let data = b"singletons are valid";
1302        store
1303            .store_fragment(make_fragment("single", 0, data, 1), 1)
1304            .expect("ok");
1305        assert_eq!(store.get_assembled("single"), Some(data.as_slice()));
1306    }
1307
1308    // -----------------------------------------------------------------------
1309    // 19. Large fragment count
1310    // -----------------------------------------------------------------------
1311
1312    #[test]
1313    fn test_large_fragment_count() {
1314        let mut store = BlockFragmentStore::new(4, 4);
1315        let total = 256u32;
1316        for i in 0..total {
1317            let data = vec![i as u8; 64];
1318            store
1319                .store_fragment(make_fragment("big", i, &data, total), i as u64)
1320                .expect("ok");
1321        }
1322        let assembled = store.get_assembled("big").expect("assembled");
1323        assert_eq!(assembled.len(), 256 * 64);
1324        // Verify first bytes of each 64-byte chunk.
1325        for i in 0..total as usize {
1326            assert_eq!(assembled[i * 64], i as u8);
1327        }
1328    }
1329
1330    // -----------------------------------------------------------------------
1331    // 20. Idempotent assemble call
1332    // -----------------------------------------------------------------------
1333
1334    #[test]
1335    fn test_assemble_idempotent() {
1336        let mut store = store_with_defaults();
1337        store
1338            .store_fragment(make_fragment("idem", 0, b"A", 1), 1)
1339            .expect("ok");
1340        let b1 = store.assemble("idem", 2).expect("first");
1341        let b2 = store.assemble("idem", 3).expect("second");
1342        assert_eq!(b1.data, b2.data);
1343    }
1344
1345    // -----------------------------------------------------------------------
1346    // 21. already-assembled block treated as complete in store_fragment
1347    // -----------------------------------------------------------------------
1348
1349    #[test]
1350    fn test_store_fragment_for_assembled_block_returns_complete() {
1351        let mut store = store_with_defaults();
1352        store
1353            .store_fragment(make_fragment("done", 0, b"X", 1), 1)
1354            .expect("ok");
1355        // Now try to store another fragment for the same (already assembled) block.
1356        let state = store
1357            .store_fragment(make_fragment("done", 0, b"Y", 1), 2)
1358            .expect("ok");
1359        assert_eq!(state, FragmentSetState::Complete);
1360    }
1361
1362    // -----------------------------------------------------------------------
1363    // 22. Stats avg_completion_rate multiple sets
1364    // -----------------------------------------------------------------------
1365
1366    #[test]
1367    fn test_avg_completion_rate_two_sets() {
1368        let mut store = store_with_defaults();
1369        // Set 1: 1/4 complete
1370        store
1371            .store_fragment(make_fragment("s1", 0, b"a", 4), 1)
1372            .expect("ok");
1373        // Set 2: 3/4 complete
1374        store
1375            .store_fragment(make_fragment("s2", 0, b"a", 4), 2)
1376            .expect("ok");
1377        store
1378            .store_fragment(make_fragment("s2", 1, b"b", 4), 3)
1379            .expect("ok");
1380        store
1381            .store_fragment(make_fragment("s2", 2, b"c", 4), 4)
1382            .expect("ok");
1383        let s = store.stats();
1384        let expected = (0.25 + 0.75) / 2.0;
1385        assert!((s.avg_completion_rate - expected).abs() < 1e-9);
1386    }
1387
1388    // -----------------------------------------------------------------------
1389    // 23. FragmentError Display
1390    // -----------------------------------------------------------------------
1391
1392    #[test]
1393    fn test_error_display_duplicate() {
1394        let e = FragmentError::DuplicateFragment {
1395            block_cid: "c".into(),
1396            index: 2,
1397        };
1398        let s = e.to_string();
1399        assert!(s.contains("duplicate"), "msg: {s}");
1400    }
1401
1402    #[test]
1403    fn test_error_display_store_full() {
1404        let e = FragmentError::StoreFull;
1405        let s = e.to_string();
1406        assert!(s.contains("full"), "msg: {s}");
1407    }
1408
1409    // -----------------------------------------------------------------------
1410    // 24. max_assembled eviction
1411    // -----------------------------------------------------------------------
1412
1413    #[test]
1414    fn test_max_assembled_evicts_oldest() {
1415        let mut store = BlockFragmentStore::new(16, 2);
1416        // Assemble three 1-fragment blocks.
1417        store
1418            .store_fragment(make_fragment("a", 0, b"A", 1), 1)
1419            .expect("ok");
1420        store
1421            .store_fragment(make_fragment("b", 0, b"B", 1), 2)
1422            .expect("ok");
1423        // Both should be in cache.
1424        assert_eq!(store.assembled_count(), 2);
1425        // Adding a third should evict the oldest (assembled_at=1, which is "a").
1426        store
1427            .store_fragment(make_fragment("c", 0, b"C", 1), 3)
1428            .expect("ok");
1429        assert_eq!(store.assembled_count(), 2);
1430        assert!(
1431            store.get_assembled("a").is_none(),
1432            "oldest assembled evicted"
1433        );
1434        assert!(store.get_assembled("b").is_some());
1435        assert!(store.get_assembled("c").is_some());
1436    }
1437
1438    // -----------------------------------------------------------------------
1439    // 25. Verify set with multiple bad indices sorted
1440    // -----------------------------------------------------------------------
1441
1442    #[test]
1443    fn test_verify_set_multiple_bad_sorted() {
1444        let mut set = FragmentSet::new("cid".into(), 4, 0);
1445        set.fragments.insert(0, make_fragment("cid", 0, b"ok", 4));
1446        set.fragments
1447            .insert(1, make_fragment_tampered("cid", 1, b"bad1", 4));
1448        set.fragments.insert(2, make_fragment("cid", 2, b"ok2", 4));
1449        set.fragments
1450            .insert(3, make_fragment_tampered("cid", 3, b"bad3", 4));
1451        let bad = BlockFragmentStore::verify_set(&set);
1452        assert_eq!(bad, vec![1u32, 3]);
1453    }
1454}