1use std::collections::HashMap;
6
7pub fn fnv1a(bytes: &[u8]) -> u64 {
13 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
14 const PRIME: u64 = 1_099_511_628_211;
15
16 let mut hash = OFFSET_BASIS;
17 for &b in bytes {
18 hash ^= u64::from(b);
19 hash = hash.wrapping_mul(PRIME);
20 }
21 hash
22}
23
24#[derive(Debug, Clone, PartialEq)]
30pub struct PackEntry {
31 pub cid: String,
33 pub offset: u64,
35 pub size_bytes: u64,
37 pub checksum: u64,
39}
40
41#[derive(Debug, Clone)]
47pub struct Pack {
48 pub pack_id: u64,
50 pub entries: Vec<PackEntry>,
52 pub total_size_bytes: u64,
54 pub created_at_secs: u64,
56}
57
58impl Pack {
59 pub fn entry_count(&self) -> usize {
61 self.entries.len()
62 }
63
64 pub fn contains(&self, cid: &str) -> bool {
66 self.entries.iter().any(|e| e.cid == cid)
67 }
68
69 pub fn find(&self, cid: &str) -> Option<&PackEntry> {
71 self.entries.iter().find(|e| e.cid == cid)
72 }
73}
74
75#[derive(Debug, Clone)]
81pub struct PackerConfig {
82 pub max_pack_size_bytes: u64,
84 pub min_block_size_bytes: u64,
87 pub max_entries_per_pack: usize,
89}
90
91impl Default for PackerConfig {
92 fn default() -> Self {
93 Self {
94 max_pack_size_bytes: 67_108_864, min_block_size_bytes: 65_536, max_entries_per_pack: 1024,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
107pub struct PackerStats {
108 pub total_packs: usize,
110 pub total_entries: usize,
112 pub total_packed_bytes: u64,
114 pub avg_pack_utilization: f64,
117}
118
119pub struct StorageBlockPacker {
125 pub packs: HashMap<u64, Pack>,
127 pub next_pack_id: u64,
129 pub config: PackerConfig,
131}
132
133impl StorageBlockPacker {
134 pub fn new(config: PackerConfig) -> Self {
136 Self {
137 packs: HashMap::new(),
138 next_pack_id: 1,
139 config,
140 }
141 }
142
143 pub fn pack(&mut self, blocks: Vec<(String, u64)>, now_secs: u64) -> Vec<u64> {
151 let eligible: Vec<(String, u64)> = blocks
153 .into_iter()
154 .filter(|(_, size)| *size < self.config.min_block_size_bytes)
155 .collect();
156
157 if eligible.is_empty() {
158 return Vec::new();
159 }
160
161 let mut created_ids: Vec<u64> = Vec::new();
162
163 let mut current_entries: Vec<PackEntry> = Vec::new();
165 let mut current_size: u64 = 0;
166 let mut current_offset: u64 = 0;
167
168 for (cid, size_bytes) in eligible {
169 let would_exceed_size = current_size + size_bytes > self.config.max_pack_size_bytes;
171 let would_exceed_entries = current_entries.len() >= self.config.max_entries_per_pack;
172
173 if !current_entries.is_empty() && (would_exceed_size || would_exceed_entries) {
174 let pack_id = self.next_pack_id;
176 self.next_pack_id += 1;
177
178 let pack = Pack {
179 pack_id,
180 total_size_bytes: current_size,
181 entries: current_entries,
182 created_at_secs: now_secs,
183 };
184 self.packs.insert(pack_id, pack);
185 created_ids.push(pack_id);
186
187 current_entries = Vec::new();
189 current_size = 0;
190 current_offset = 0;
191 }
192
193 let checksum = fnv1a(cid.as_bytes());
194 let entry = PackEntry {
195 cid,
196 offset: current_offset,
197 size_bytes,
198 checksum,
199 };
200 current_offset += size_bytes;
201 current_size += size_bytes;
202 current_entries.push(entry);
203 }
204
205 if !current_entries.is_empty() {
207 let pack_id = self.next_pack_id;
208 self.next_pack_id += 1;
209
210 let pack = Pack {
211 pack_id,
212 total_size_bytes: current_size,
213 entries: current_entries,
214 created_at_secs: now_secs,
215 };
216 self.packs.insert(pack_id, pack);
217 created_ids.push(pack_id);
218 }
219
220 created_ids
221 }
222
223 pub fn find_pack(&self, cid: &str) -> Option<(&Pack, &PackEntry)> {
227 for pack in self.packs.values() {
228 if let Some(entry) = pack.find(cid) {
229 return Some((pack, entry));
230 }
231 }
232 None
233 }
234
235 pub fn get_pack(&self, pack_id: u64) -> Option<&Pack> {
237 self.packs.get(&pack_id)
238 }
239
240 pub fn delete_pack(&mut self, pack_id: u64) -> bool {
244 self.packs.remove(&pack_id).is_some()
245 }
246
247 pub fn stats(&self) -> PackerStats {
249 let total_packs = self.packs.len();
250 let total_entries = self.packs.values().map(|p| p.entry_count()).sum();
251 let total_packed_bytes = self.packs.values().map(|p| p.total_size_bytes).sum();
252
253 let avg_pack_utilization = if total_packs == 0 {
254 0.0
255 } else {
256 let sum: f64 = self
257 .packs
258 .values()
259 .map(|p| p.total_size_bytes as f64 / self.config.max_pack_size_bytes as f64)
260 .sum();
261 sum / total_packs as f64
262 };
263
264 PackerStats {
265 total_packs,
266 total_entries,
267 total_packed_bytes,
268 avg_pack_utilization,
269 }
270 }
271}
272
273#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn default_packer() -> StorageBlockPacker {
282 StorageBlockPacker::new(PackerConfig::default())
283 }
284
285 fn small_block(cid: &str, kb: u64) -> (String, u64) {
286 (cid.to_string(), kb * 1024)
287 }
288
289 fn large_block(cid: &str) -> (String, u64) {
290 (cid.to_string(), 65_536)
292 }
293
294 #[test]
296 fn test_new_starts_empty() {
297 let packer = default_packer();
298 assert!(packer.packs.is_empty());
299 assert_eq!(packer.next_pack_id, 1);
300 }
301
302 #[test]
304 fn test_pack_empty_input_returns_empty() {
305 let mut packer = default_packer();
306 let ids = packer.pack(vec![], 100);
307 assert!(ids.is_empty());
308 }
309
310 #[test]
312 fn test_pack_filters_large_blocks() {
313 let mut packer = default_packer();
314 let blocks = vec![large_block("cid-large")];
315 let ids = packer.pack(blocks, 100);
316 assert!(ids.is_empty(), "large block should be filtered out");
317 assert!(packer.packs.is_empty());
318 }
319
320 #[test]
322 fn test_pack_creates_single_pack() {
323 let mut packer = default_packer();
324 let blocks = vec![small_block("cid-a", 1), small_block("cid-b", 2)];
325 let ids = packer.pack(blocks, 100);
326 assert_eq!(ids.len(), 1);
327 assert_eq!(packer.packs.len(), 1);
328 }
329
330 #[test]
332 fn test_pack_creates_multiple_packs_on_size_overflow() {
333 let config = PackerConfig {
334 max_pack_size_bytes: 10_000,
335 min_block_size_bytes: 65_536,
336 max_entries_per_pack: 1024,
337 };
338 let mut packer = StorageBlockPacker::new(config);
339 let blocks = vec![
341 ("cid-a".to_string(), 6000u64),
342 ("cid-b".to_string(), 6000u64),
343 ("cid-c".to_string(), 6000u64),
344 ];
345 let ids = packer.pack(blocks, 200);
346 assert_eq!(ids.len(), 3);
350 assert_eq!(packer.packs.len(), 3);
351 }
352
353 #[test]
355 fn test_pack_creates_new_pack_on_max_entries() {
356 let config = PackerConfig {
357 max_pack_size_bytes: 67_108_864,
358 min_block_size_bytes: 65_536,
359 max_entries_per_pack: 2,
360 };
361 let mut packer = StorageBlockPacker::new(config);
362 let blocks = vec![
363 ("cid-1".to_string(), 100u64),
364 ("cid-2".to_string(), 100u64),
365 ("cid-3".to_string(), 100u64),
366 ];
367 let ids = packer.pack(blocks, 300);
368 assert_eq!(ids.len(), 2);
371 let pack1 = packer.get_pack(ids[0]).expect("pack 1 should exist");
372 assert_eq!(pack1.entry_count(), 2);
373 let pack2 = packer.get_pack(ids[1]).expect("pack 2 should exist");
374 assert_eq!(pack2.entry_count(), 1);
375 }
376
377 #[test]
379 fn test_pack_entry_offsets() {
380 let mut packer = default_packer();
381 let blocks = vec![
382 ("cid-x".to_string(), 100u64),
383 ("cid-y".to_string(), 250u64),
384 ("cid-z".to_string(), 50u64),
385 ];
386 let ids = packer.pack(blocks, 100);
387 assert_eq!(ids.len(), 1);
388 let pack = packer.get_pack(ids[0]).expect("pack should exist");
389
390 let x = pack.find("cid-x").expect("cid-x not found");
391 assert_eq!(x.offset, 0);
392
393 let y = pack.find("cid-y").expect("cid-y not found");
394 assert_eq!(y.offset, 100);
395
396 let z = pack.find("cid-z").expect("cid-z not found");
397 assert_eq!(z.offset, 350);
398 }
399
400 #[test]
402 fn test_pack_entry_checksum() {
403 let mut packer = default_packer();
404 let cid = "QmTestChecksum";
405 let blocks = vec![(cid.to_string(), 500u64)];
406 let ids = packer.pack(blocks, 100);
407 let pack = packer.get_pack(ids[0]).expect("pack should exist");
408 let entry = pack.find(cid).expect("entry not found");
409 assert_eq!(entry.checksum, fnv1a(cid.as_bytes()));
410 }
411
412 #[test]
414 fn test_pack_contains() {
415 let mut packer = default_packer();
416 let blocks = vec![small_block("cid-p", 1), small_block("cid-q", 2)];
417 let ids = packer.pack(blocks, 100);
418 let pack = packer.get_pack(ids[0]).expect("pack should exist");
419 assert!(pack.contains("cid-p"));
420 assert!(pack.contains("cid-q"));
421 assert!(!pack.contains("cid-missing"));
422 }
423
424 #[test]
426 fn test_pack_find_returns_correct_entry() {
427 let mut packer = default_packer();
428 let blocks = vec![("cid-find".to_string(), 1234u64)];
429 let ids = packer.pack(blocks, 100);
430 let pack = packer.get_pack(ids[0]).expect("pack should exist");
431 let entry = pack.find("cid-find").expect("entry not found");
432 assert_eq!(entry.cid, "cid-find");
433 assert_eq!(entry.size_bytes, 1234);
434 assert!(pack.find("nonexistent").is_none());
435 }
436
437 #[test]
439 fn test_pack_entry_count() {
440 let mut packer = default_packer();
441 let blocks = vec![
442 small_block("c1", 1),
443 small_block("c2", 2),
444 small_block("c3", 3),
445 ];
446 let ids = packer.pack(blocks, 100);
447 let pack = packer.get_pack(ids[0]).expect("pack should exist");
448 assert_eq!(pack.entry_count(), 3);
449 }
450
451 #[test]
453 fn test_find_pack_searches_across_packs() {
454 let config = PackerConfig {
455 max_pack_size_bytes: 10_000,
456 min_block_size_bytes: 65_536,
457 max_entries_per_pack: 1024,
458 };
459 let mut packer = StorageBlockPacker::new(config);
460 let blocks = vec![
462 ("cid-first".to_string(), 6000u64),
463 ("cid-second".to_string(), 6000u64),
464 ];
465 let ids = packer.pack(blocks, 100);
466 assert_eq!(ids.len(), 2);
467
468 let result = packer.find_pack("cid-first");
469 assert!(result.is_some());
470 let (_, entry) = result.expect("should find cid-first");
471 assert_eq!(entry.cid, "cid-first");
472
473 let result2 = packer.find_pack("cid-second");
474 assert!(result2.is_some());
475 let (_, entry2) = result2.expect("should find cid-second");
476 assert_eq!(entry2.cid, "cid-second");
477 }
478
479 #[test]
481 fn test_find_pack_returns_none_for_unknown() {
482 let mut packer = default_packer();
483 packer.pack(vec![small_block("known", 1)], 100);
484 assert!(packer.find_pack("unknown-cid").is_none());
485 }
486
487 #[test]
489 fn test_get_pack_some_and_none() {
490 let mut packer = default_packer();
491 let ids = packer.pack(vec![small_block("cid-gp", 1)], 100);
492 assert!(packer.get_pack(ids[0]).is_some());
493 assert!(packer.get_pack(9999).is_none());
494 }
495
496 #[test]
498 fn test_delete_pack_true_false() {
499 let mut packer = default_packer();
500 let ids = packer.pack(vec![small_block("cid-del", 1)], 100);
501 let pack_id = ids[0];
502 assert!(
503 packer.delete_pack(pack_id),
504 "should return true when pack exists"
505 );
506 assert!(
507 !packer.delete_pack(pack_id),
508 "should return false when already deleted"
509 );
510 assert!(packer.get_pack(pack_id).is_none());
511 }
512
513 #[test]
515 fn test_stats_total_packs() {
516 let config = PackerConfig {
517 max_pack_size_bytes: 10_000,
518 min_block_size_bytes: 65_536,
519 max_entries_per_pack: 1024,
520 };
521 let mut packer = StorageBlockPacker::new(config);
522 packer.pack(
523 vec![("c1".to_string(), 6000u64), ("c2".to_string(), 6000u64)],
524 100,
525 );
526 let stats = packer.stats();
527 assert_eq!(stats.total_packs, 2);
528 }
529
530 #[test]
532 fn test_stats_total_entries() {
533 let mut packer = default_packer();
534 packer.pack(
535 vec![
536 small_block("e1", 1),
537 small_block("e2", 2),
538 small_block("e3", 3),
539 ],
540 100,
541 );
542 let stats = packer.stats();
543 assert_eq!(stats.total_entries, 3);
544 }
545
546 #[test]
548 fn test_stats_total_packed_bytes() {
549 let mut packer = default_packer();
550 packer.pack(
551 vec![
552 ("b1".to_string(), 1000u64),
553 ("b2".to_string(), 2000u64),
554 ("b3".to_string(), 3000u64),
555 ],
556 100,
557 );
558 let stats = packer.stats();
559 assert_eq!(stats.total_packed_bytes, 6000);
560 }
561
562 #[test]
564 fn test_stats_avg_pack_utilization() {
565 let config = PackerConfig {
566 max_pack_size_bytes: 10_000,
567 min_block_size_bytes: 65_536,
568 max_entries_per_pack: 1024,
569 };
570 let mut packer = StorageBlockPacker::new(config);
571 packer.pack(vec![("u1".to_string(), 5000u64)], 100);
573 let stats = packer.stats();
574 let expected = 5000.0_f64 / 10_000.0_f64;
575 assert!(
576 (stats.avg_pack_utilization - expected).abs() < 1e-10,
577 "expected {expected}, got {}",
578 stats.avg_pack_utilization
579 );
580 }
581
582 #[test]
584 fn test_stats_avg_utilization_empty() {
585 let packer = default_packer();
586 let stats = packer.stats();
587 assert_eq!(stats.avg_pack_utilization, 0.0);
588 }
589
590 #[test]
592 fn test_pack_id_monotonically_increasing() {
593 let config = PackerConfig {
594 max_pack_size_bytes: 10_000,
595 min_block_size_bytes: 65_536,
596 max_entries_per_pack: 1024,
597 };
598 let mut packer = StorageBlockPacker::new(config);
599 let ids = packer.pack(
600 vec![
601 ("m1".to_string(), 6000u64),
602 ("m2".to_string(), 6000u64),
603 ("m3".to_string(), 6000u64),
604 ],
605 100,
606 );
607 assert_eq!(ids.len(), 3);
608 assert!(ids[0] < ids[1], "pack IDs must be monotonically increasing");
609 assert!(ids[1] < ids[2], "pack IDs must be monotonically increasing");
610 }
611
612 #[test]
614 fn test_fnv1a_deterministic() {
615 let a = fnv1a(b"hello");
616 let b = fnv1a(b"hello");
617 assert_eq!(a, b);
618 let c = fnv1a(b"world");
619 assert_ne!(a, c);
620 }
621
622 #[test]
624 fn test_pack_mixed_blocks() {
625 let mut packer = default_packer();
626 let blocks = vec![
627 ("small".to_string(), 1000u64), ("large".to_string(), 65_536u64), ("tiny".to_string(), 512u64), ];
631 let ids = packer.pack(blocks, 100);
632 assert_eq!(ids.len(), 1);
633 let pack = packer.get_pack(ids[0]).expect("pack should exist");
634 assert_eq!(pack.entry_count(), 2);
635 assert!(pack.contains("small"));
636 assert!(pack.contains("tiny"));
637 assert!(!pack.contains("large"));
638 }
639
640 #[test]
642 fn test_pack_created_at_secs() {
643 let mut packer = default_packer();
644 let ts = 999_999_u64;
645 let ids = packer.pack(vec![small_block("time-cid", 1)], ts);
646 let pack = packer.get_pack(ids[0]).expect("pack should exist");
647 assert_eq!(pack.created_at_secs, ts);
648 }
649
650 #[test]
652 fn test_pack_entries_sorted_by_offset() {
653 let mut packer = default_packer();
654 let blocks = vec![
655 ("first".to_string(), 100u64),
656 ("second".to_string(), 200u64),
657 ("third".to_string(), 300u64),
658 ];
659 let ids = packer.pack(blocks, 100);
660 let pack = packer.get_pack(ids[0]).expect("pack should exist");
661 let offsets: Vec<u64> = pack.entries.iter().map(|e| e.offset).collect();
662 let mut sorted = offsets.clone();
663 sorted.sort_unstable();
664 assert_eq!(offsets, sorted, "entries should be sorted by offset");
665 }
666}