Skip to main content

ipfrs_storage/
utils.rs

1//! Utility functions for testing, benchmarking, and batch operations
2
3use bytes::Bytes;
4use ipfrs_core::{Block, Cid, Result};
5use sha2::{Digest, Sha256};
6use std::collections::HashMap;
7
8/// Compute CID from raw data for testing/benchmarking
9///
10/// This creates a CIDv1 with SHA-256 hash and raw codec
11#[inline]
12pub fn compute_cid(data: &[u8]) -> Cid {
13    let hash = Sha256::digest(data);
14
15    // Create CIDv1 with SHA-256 multihash (0x12) and raw codec (0x55)
16    let mut multihash = Vec::with_capacity(34);
17    multihash.push(0x12); // SHA-256
18    multihash.push(32); // hash length
19    multihash.extend_from_slice(&hash);
20
21    // For testing, we'll use the multihash as the CID bytes directly
22    // In production, this would use the proper CID encoding
23    Cid::try_from(multihash).unwrap_or_else(|_| {
24        // Fallback: create a simple CID from the hash
25        let cid_bytes = format!("bafkreei{}", hex::encode(&hash[..16]));
26        Cid::try_from(cid_bytes.as_bytes().to_vec())
27            .expect("fallback CID from hex-encoded hash is always valid")
28    })
29}
30
31/// Create a block from raw data for testing
32#[inline]
33pub fn create_block(data: Vec<u8>) -> Result<Block> {
34    let cid = compute_cid(&data);
35    Ok(Block::from_parts(cid, Bytes::from(data)))
36}
37
38/// Generate random block data for testing
39pub fn generate_random_block(size: usize, seed: u64) -> Vec<u8> {
40    let mut rng = fastrand::Rng::with_seed(seed);
41    let mut data = vec![0u8; size];
42
43    for chunk in data.chunks_mut(8) {
44        let val = rng.u64(..);
45        let bytes = val.to_le_bytes();
46        let len = chunk.len().min(8);
47        chunk[..len].copy_from_slice(&bytes[..len]);
48    }
49
50    data
51}
52
53/// Generate compressible data for testing compression
54pub fn generate_compressible_data(size: usize) -> Vec<u8> {
55    let mut data = vec![0u8; size];
56    let pattern = b"IPFS is a distributed file system. ";
57
58    for (i, byte) in data.iter_mut().enumerate() {
59        *byte = pattern[i % pattern.len()];
60    }
61
62    data
63}
64
65/// Generate incompressible data for testing
66pub fn generate_incompressible_data(size: usize, seed: u64) -> Vec<u8> {
67    generate_random_block(size, seed)
68}
69
70/// Create multiple blocks from raw data efficiently
71///
72/// This is optimized for batch operations, creating all blocks in parallel
73/// Returns a HashMap mapping CIDs to Blocks for easy lookup
74pub fn create_blocks_batch(data_vec: Vec<Vec<u8>>) -> Result<HashMap<Cid, Block>> {
75    let mut blocks = HashMap::with_capacity(data_vec.len());
76
77    for data in data_vec {
78        let block = create_block(data)?;
79        blocks.insert(*block.cid(), block);
80    }
81
82    Ok(blocks)
83}
84
85/// Generate multiple random blocks with sequential seeds
86///
87/// Returns a Vec of (Block, seed) tuples for reproducibility
88pub fn generate_random_blocks(count: usize, size: usize, start_seed: u64) -> Result<Vec<Block>> {
89    let mut blocks = Vec::with_capacity(count);
90
91    for i in 0..count {
92        let seed = start_seed.wrapping_add(i as u64);
93        let data = generate_random_block(size, seed);
94        let block = create_block(data)?;
95        blocks.push(block);
96    }
97
98    Ok(blocks)
99}
100
101/// Generate multiple compressible blocks for compression testing
102///
103/// Each block has a different pattern to test compression efficiency
104pub fn generate_compressible_blocks(count: usize, size: usize) -> Result<Vec<Block>> {
105    let patterns = [
106        b"IPFS is a distributed file system. ".to_vec(),
107        b"Content addressing with CIDs. ".to_vec(),
108        b"Merkle DAGs for data structures. ".to_vec(),
109        b"Peer-to-peer networking protocol. ".to_vec(),
110        b"Immutable data storage layer. ".to_vec(),
111    ];
112
113    let mut blocks = Vec::with_capacity(count);
114
115    for i in 0..count {
116        let pattern = &patterns[i % patterns.len()];
117        let mut data = vec![0u8; size];
118
119        for (j, byte) in data.iter_mut().enumerate() {
120            *byte = pattern[j % pattern.len()];
121        }
122
123        let block = create_block(data)?;
124        blocks.push(block);
125    }
126
127    Ok(blocks)
128}
129
130/// Create a test dataset with mixed block sizes
131///
132/// Returns blocks with sizes: small (1KB), medium (64KB), large (1MB)
133/// Useful for testing size-dependent optimizations
134pub fn generate_mixed_size_blocks(small: usize, medium: usize, large: usize) -> Result<Vec<Block>> {
135    let mut blocks = Vec::with_capacity(small + medium + large);
136    let mut seed = 0u64;
137
138    // Small blocks (1KB)
139    for _ in 0..small {
140        let data = generate_random_block(1024, seed);
141        blocks.push(create_block(data)?);
142        seed = seed.wrapping_add(1);
143    }
144
145    // Medium blocks (64KB)
146    for _ in 0..medium {
147        let data = generate_random_block(64 * 1024, seed);
148        blocks.push(create_block(data)?);
149        seed = seed.wrapping_add(1);
150    }
151
152    // Large blocks (1MB)
153    for _ in 0..large {
154        let data = generate_random_block(1024 * 1024, seed);
155        blocks.push(create_block(data)?);
156        seed = seed.wrapping_add(1);
157    }
158
159    Ok(blocks)
160}
161
162/// Create blocks with controlled deduplication characteristics
163///
164/// - `unique`: Number of unique blocks
165/// - `duplicate_factor`: How many times each block is duplicated
166pub fn generate_dedup_dataset(unique: usize, duplicate_factor: usize) -> Result<Vec<Block>> {
167    let mut blocks = Vec::new();
168
169    // Generate unique blocks
170    let unique_blocks = generate_random_blocks(unique, 4096, 42)?;
171
172    // Duplicate them
173    for _ in 0..duplicate_factor {
174        blocks.extend(unique_blocks.iter().cloned());
175    }
176
177    Ok(blocks)
178}
179
180/// Extract CIDs from a collection of blocks
181pub fn extract_cids(blocks: &[Block]) -> Vec<Cid> {
182    blocks.iter().map(|b| *b.cid()).collect()
183}
184
185/// Compute total size of a block collection
186pub fn compute_total_size(blocks: &[Block]) -> usize {
187    blocks.iter().map(|b| b.data().len()).sum()
188}
189
190/// Group blocks by size ranges for analysis
191///
192/// Returns (small, medium, large) where:
193/// - small: < 16KB
194/// - medium: 16KB - 256KB
195/// - large: > 256KB
196pub fn group_blocks_by_size(blocks: &[Block]) -> (Vec<Block>, Vec<Block>, Vec<Block>) {
197    let mut small = Vec::new();
198    let mut medium = Vec::new();
199    let mut large = Vec::new();
200
201    for block in blocks {
202        let size = block.data().len();
203        if size < 16 * 1024 {
204            small.push(block.clone());
205        } else if size < 256 * 1024 {
206            medium.push(block.clone());
207        } else {
208            large.push(block.clone());
209        }
210    }
211
212    (small, medium, large)
213}
214
215/// Validate block integrity by recomputing CID
216///
217/// Returns true if the block's CID matches the computed CID from its data
218pub fn validate_block_integrity(block: &Block) -> bool {
219    let computed_cid = compute_cid(block.data());
220    computed_cid == *block.cid()
221}
222
223/// Batch validate block integrity for multiple blocks
224///
225/// Returns a vector of (CID, is_valid) tuples
226pub fn validate_blocks_batch(blocks: &[Block]) -> Vec<(Cid, bool)> {
227    blocks
228        .iter()
229        .map(|block| (*block.cid(), validate_block_integrity(block)))
230        .collect()
231}
232
233/// Compute statistics for a collection of blocks
234#[derive(Debug, Clone)]
235pub struct BlockStatistics {
236    /// Total number of blocks
237    pub count: usize,
238    /// Total size in bytes
239    pub total_size: usize,
240    /// Average block size
241    pub avg_size: f64,
242    /// Minimum block size
243    pub min_size: usize,
244    /// Maximum block size
245    pub max_size: usize,
246    /// Median block size (approximate)
247    pub median_size: usize,
248}
249
250impl BlockStatistics {
251    /// Compute statistics from a block collection
252    pub fn from_blocks(blocks: &[Block]) -> Self {
253        if blocks.is_empty() {
254            return Self {
255                count: 0,
256                total_size: 0,
257                avg_size: 0.0,
258                min_size: 0,
259                max_size: 0,
260                median_size: 0,
261            };
262        }
263
264        let mut sizes: Vec<usize> = blocks.iter().map(|b| b.data().len()).collect();
265        sizes.sort_unstable();
266
267        let count = blocks.len();
268        let total_size: usize = sizes.iter().sum();
269        let avg_size = total_size as f64 / count as f64;
270        let min_size = sizes[0];
271        let max_size = sizes[count - 1];
272        let median_size = sizes[count / 2];
273
274        Self {
275            count,
276            total_size,
277            avg_size,
278            min_size,
279            max_size,
280            median_size,
281        }
282    }
283
284    /// Estimate memory overhead (CID + metadata)
285    pub fn estimated_memory_overhead(&self) -> usize {
286        // Rough estimate: 64 bytes per block for CID and metadata
287        self.count * 64
288    }
289
290    /// Total memory footprint (data + overhead)
291    pub fn total_memory_footprint(&self) -> usize {
292        self.total_size + self.estimated_memory_overhead()
293    }
294}
295
296/// Filter blocks by size range
297pub fn filter_blocks_by_size(blocks: &[Block], min_size: usize, max_size: usize) -> Vec<Block> {
298    blocks
299        .iter()
300        .filter(|block| {
301            let size = block.data().len();
302            size >= min_size && size <= max_size
303        })
304        .cloned()
305        .collect()
306}
307
308/// Sort blocks by size (ascending)
309pub fn sort_blocks_by_size_asc(blocks: &mut [Block]) {
310    blocks.sort_by_key(|b| b.data().len());
311}
312
313/// Sort blocks by size (descending)
314pub fn sort_blocks_by_size_desc(blocks: &mut [Block]) {
315    blocks.sort_by_key(|b| std::cmp::Reverse(b.data().len()));
316}
317
318/// Find duplicate blocks (same CID)
319///
320/// Returns a HashMap mapping CIDs to their occurrence count
321pub fn find_duplicates(blocks: &[Block]) -> HashMap<Cid, usize> {
322    let mut counts = HashMap::new();
323    for block in blocks {
324        *counts.entry(*block.cid()).or_insert(0) += 1;
325    }
326    counts.retain(|_, count| *count > 1);
327    counts
328}
329
330/// Deduplicate blocks by CID (keep first occurrence)
331pub fn deduplicate_blocks(blocks: &[Block]) -> Vec<Block> {
332    let mut seen = std::collections::HashSet::new();
333    let mut result = Vec::new();
334
335    for block in blocks {
336        if seen.insert(*block.cid()) {
337            result.push(block.clone());
338        }
339    }
340
341    result
342}
343
344/// Estimate compression ratio for a block
345///
346/// Returns a value between 0.0 and 1.0, where lower values indicate better compression
347pub fn estimate_compression_ratio(data: &[u8]) -> f64 {
348    if data.is_empty() {
349        return 1.0;
350    }
351
352    // Simple entropy-based estimation
353    let mut counts = [0u64; 256];
354    for &byte in data {
355        counts[byte as usize] += 1;
356    }
357
358    let len = data.len() as f64;
359    let mut entropy = 0.0;
360
361    for &count in &counts {
362        if count > 0 {
363            let p = count as f64 / len;
364            entropy -= p * p.log2();
365        }
366    }
367
368    // Normalize entropy to 0-1 range (8 bits max entropy)
369    (entropy / 8.0).min(1.0)
370}
371
372/// Sample a subset of blocks for testing
373///
374/// Returns up to `count` blocks, evenly distributed across the input
375pub fn sample_blocks(blocks: &[Block], count: usize) -> Vec<Block> {
376    if blocks.len() <= count {
377        return blocks.to_vec();
378    }
379
380    let step = blocks.len() / count;
381    blocks.iter().step_by(step).take(count).cloned().collect()
382}
383
384/// Generate blocks with specific patterns for testing
385///
386/// Patterns: "sequential", "random", "compressible", "sparse"
387pub fn generate_pattern_blocks(count: usize, size: usize, pattern: &str) -> Result<Vec<Block>> {
388    match pattern {
389        "sequential" => {
390            let mut blocks = Vec::new();
391            for i in 0..count {
392                let mut data = vec![0u8; size];
393                for (j, byte) in data.iter_mut().enumerate() {
394                    *byte = ((i + j) % 256) as u8;
395                }
396                blocks.push(create_block(data)?);
397            }
398            Ok(blocks)
399        }
400        "random" => generate_random_blocks(count, size, 42),
401        "compressible" => generate_compressible_blocks(count, size),
402        "sparse" => {
403            let mut blocks = Vec::new();
404            for _ in 0..count {
405                let mut data = vec![0u8; size];
406                // Set only 10% of bytes to non-zero
407                let mut rng = fastrand::Rng::new();
408                for _ in 0..size / 10 {
409                    let idx = rng.usize(..size);
410                    data[idx] = rng.u8(1..);
411                }
412                blocks.push(create_block(data)?);
413            }
414            Ok(blocks)
415        }
416        _ => generate_random_blocks(count, size, 42), // Default to random
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_compute_cid() {
426        let data = b"hello world";
427        let cid1 = compute_cid(data);
428        let cid2 = compute_cid(data);
429
430        // Same data should produce same CID
431        assert_eq!(cid1, cid2);
432    }
433
434    #[test]
435    fn test_create_block() {
436        let data = b"hello world".to_vec();
437        let block = create_block(data.clone()).unwrap();
438        assert_eq!(block.data(), &data);
439    }
440
441    #[test]
442    fn test_generate_random_block() {
443        let block1 = generate_random_block(100, 42);
444        let block2 = generate_random_block(100, 42);
445        let block3 = generate_random_block(100, 43);
446
447        assert_eq!(block1, block2); // Same seed = same data
448        assert_ne!(block1, block3); // Different seed = different data
449    }
450
451    #[test]
452    fn test_generate_compressible_data() {
453        let data = generate_compressible_data(1000);
454        assert_eq!(data.len(), 1000);
455
456        // Check it has repeating pattern
457        let pattern = b"IPFS is a distributed file system. ";
458        for i in 0..10 {
459            assert_eq!(data[i], pattern[i % pattern.len()]);
460        }
461    }
462
463    #[test]
464    fn test_create_blocks_batch() {
465        let data_vec = vec![b"block1".to_vec(), b"block2".to_vec(), b"block3".to_vec()];
466
467        let blocks = create_blocks_batch(data_vec).unwrap();
468        assert_eq!(blocks.len(), 3);
469    }
470
471    #[test]
472    fn test_generate_random_blocks() {
473        let blocks = generate_random_blocks(10, 1024, 42).unwrap();
474        assert_eq!(blocks.len(), 10);
475
476        // All blocks should have the specified size
477        for block in &blocks {
478            assert_eq!(block.data().len(), 1024);
479        }
480
481        // Blocks should be unique (different seeds)
482        let cid1 = blocks[0].cid();
483        let cid2 = blocks[1].cid();
484        assert_ne!(cid1, cid2);
485    }
486
487    #[test]
488    fn test_generate_compressible_blocks() {
489        let blocks = generate_compressible_blocks(5, 1024).unwrap();
490        assert_eq!(blocks.len(), 5);
491
492        for block in &blocks {
493            assert_eq!(block.data().len(), 1024);
494        }
495    }
496
497    #[test]
498    fn test_generate_mixed_size_blocks() {
499        let blocks = generate_mixed_size_blocks(2, 3, 1).unwrap();
500        assert_eq!(blocks.len(), 6); // 2 + 3 + 1
501
502        // Verify sizes
503        assert_eq!(blocks[0].data().len(), 1024); // small
504        assert_eq!(blocks[2].data().len(), 64 * 1024); // medium
505        assert_eq!(blocks[5].data().len(), 1024 * 1024); // large
506    }
507
508    #[test]
509    fn test_generate_dedup_dataset() {
510        let blocks = generate_dedup_dataset(10, 3).unwrap();
511        assert_eq!(blocks.len(), 30); // 10 unique * 3 duplicates
512
513        // First 10 should match next 10 (duplicates)
514        for i in 0..10 {
515            assert_eq!(blocks[i].cid(), blocks[i + 10].cid());
516        }
517    }
518
519    #[test]
520    fn test_extract_cids() {
521        let blocks = generate_random_blocks(5, 1024, 42).unwrap();
522        let cids = extract_cids(&blocks);
523        assert_eq!(cids.len(), 5);
524        assert_eq!(cids[0], *blocks[0].cid());
525    }
526
527    #[test]
528    fn test_compute_total_size() {
529        let blocks = generate_mixed_size_blocks(2, 2, 1).unwrap();
530        let total = compute_total_size(&blocks);
531
532        // 2 * 1KB + 2 * 64KB + 1 * 1MB
533        let expected = 2 * 1024 + 2 * 64 * 1024 + 1024 * 1024;
534        assert_eq!(total, expected);
535    }
536
537    #[test]
538    fn test_group_blocks_by_size() {
539        let blocks = generate_mixed_size_blocks(3, 2, 1).unwrap();
540        let (small, medium, large) = group_blocks_by_size(&blocks);
541
542        assert_eq!(small.len(), 3);
543        assert_eq!(medium.len(), 2);
544        assert_eq!(large.len(), 1);
545    }
546
547    #[test]
548    fn test_validate_block_integrity() {
549        let data = b"test data".to_vec();
550        let block = create_block(data).unwrap();
551        assert!(validate_block_integrity(&block));
552    }
553
554    #[test]
555    fn test_validate_blocks_batch() {
556        let blocks = generate_random_blocks(5, 1024, 42).unwrap();
557        let results = validate_blocks_batch(&blocks);
558        assert_eq!(results.len(), 5);
559        for (_, is_valid) in results {
560            assert!(is_valid);
561        }
562    }
563
564    #[test]
565    fn test_block_statistics() {
566        let blocks = generate_mixed_size_blocks(2, 3, 1).unwrap();
567        let stats = BlockStatistics::from_blocks(&blocks);
568
569        assert_eq!(stats.count, 6);
570        assert!(stats.avg_size > 0.0);
571        assert!(stats.min_size <= stats.max_size);
572        assert!(stats.total_memory_footprint() > stats.total_size);
573    }
574
575    #[test]
576    fn test_filter_blocks_by_size() {
577        let blocks = generate_mixed_size_blocks(5, 5, 5).unwrap();
578        let filtered = filter_blocks_by_size(&blocks, 2000, 100_000);
579
580        // Should filter out 1KB blocks and keep 64KB blocks and 1MB blocks
581        for block in &filtered {
582            let size = block.data().len();
583            assert!((2000..=100_000).contains(&size));
584        }
585    }
586
587    #[test]
588    fn test_sort_blocks_by_size() {
589        let mut blocks = generate_mixed_size_blocks(2, 2, 2).unwrap();
590
591        sort_blocks_by_size_asc(&mut blocks);
592        for i in 1..blocks.len() {
593            assert!(blocks[i - 1].data().len() <= blocks[i].data().len());
594        }
595
596        sort_blocks_by_size_desc(&mut blocks);
597        for i in 1..blocks.len() {
598            assert!(blocks[i - 1].data().len() >= blocks[i].data().len());
599        }
600    }
601
602    #[test]
603    fn test_find_duplicates() {
604        let unique_blocks = generate_random_blocks(5, 1024, 42).unwrap();
605        let mut all_blocks = unique_blocks.clone();
606        all_blocks.extend(unique_blocks.clone()); // Add duplicates
607
608        let duplicates = find_duplicates(&all_blocks);
609        assert_eq!(duplicates.len(), 5); // All 5 blocks appear twice
610
611        for (_, count) in duplicates {
612            assert_eq!(count, 2);
613        }
614    }
615
616    #[test]
617    fn test_deduplicate_blocks() {
618        let unique_blocks = generate_random_blocks(5, 1024, 42).unwrap();
619        let mut all_blocks = unique_blocks.clone();
620        all_blocks.extend(unique_blocks.clone());
621
622        let deduped = deduplicate_blocks(&all_blocks);
623        assert_eq!(deduped.len(), 5);
624    }
625
626    #[test]
627    fn test_estimate_compression_ratio() {
628        // Compressible data (repeating pattern)
629        let compressible = generate_compressible_data(1000);
630        let compressible_ratio = estimate_compression_ratio(&compressible);
631
632        // Random data (incompressible)
633        let random = generate_random_block(1000, 42);
634        let random_ratio = estimate_compression_ratio(&random);
635
636        // Compressible data should have lower entropy (better compression potential)
637        assert!(compressible_ratio < random_ratio);
638    }
639
640    #[test]
641    fn test_sample_blocks() {
642        let blocks = generate_random_blocks(100, 1024, 42).unwrap();
643
644        let sample = sample_blocks(&blocks, 10);
645        assert_eq!(sample.len(), 10);
646
647        // Test sampling more than available
648        let sample_all = sample_blocks(&blocks, 200);
649        assert_eq!(sample_all.len(), 100);
650    }
651
652    #[test]
653    fn test_generate_pattern_blocks() {
654        let sequential = generate_pattern_blocks(5, 1024, "sequential").unwrap();
655        assert_eq!(sequential.len(), 5);
656
657        let random = generate_pattern_blocks(5, 1024, "random").unwrap();
658        assert_eq!(random.len(), 5);
659
660        let compressible = generate_pattern_blocks(5, 1024, "compressible").unwrap();
661        assert_eq!(compressible.len(), 5);
662
663        let sparse = generate_pattern_blocks(5, 1024, "sparse").unwrap();
664        assert_eq!(sparse.len(), 5);
665
666        // Test sparse pattern has mostly zeros
667        let sparse_data = sparse[0].data();
668        let zero_count = sparse_data.iter().filter(|&&b| b == 0).count();
669        assert!(zero_count > sparse_data.len() * 8 / 10); // At least 80% zeros
670    }
671}