1use std::collections::HashMap;
7
8#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum TransferDirection {
11 Upload,
13 Download,
15}
16
17#[derive(Clone, Debug)]
19pub struct TransferChunk {
20 pub chunk_index: u32,
22 pub data: Vec<u8>,
24 pub checksum: u32,
26}
27
28impl TransferChunk {
29 pub fn compute_checksum(data: &[u8]) -> u32 {
31 data.iter().fold(0u32, |acc, &b| acc ^ u32::from(b))
32 }
33
34 pub fn is_valid(&self) -> bool {
36 self.checksum == Self::compute_checksum(&self.data)
37 }
38}
39
40#[derive(Clone, Debug, PartialEq)]
42pub enum TransferState {
43 Pending,
45 InProgress {
47 chunks_done: u32,
49 total_chunks: u32,
51 },
52 Paused {
54 chunks_done: u32,
56 },
57 Completed,
59 Failed {
61 reason: String,
63 },
64}
65
66#[derive(Clone, Debug)]
68pub struct BlockTransfer {
69 pub transfer_id: u64,
71 pub cid: String,
73 pub peer_id: String,
75 pub direction: TransferDirection,
77 pub total_bytes: u64,
79 pub chunk_size: u64,
81 pub state: TransferState,
83 pub received_chunks: Vec<u32>,
85 pub started_at_tick: u64,
87}
88
89impl BlockTransfer {
90 pub fn total_chunks(&self) -> u32 {
94 if self.chunk_size == 0 {
95 return 0;
96 }
97 self.total_bytes.div_ceil(self.chunk_size) as u32
98 }
99
100 pub fn progress(&self) -> f64 {
104 let total = self.total_chunks();
105 if total == 0 {
106 return 1.0;
107 }
108 let done = match &self.state {
109 TransferState::Pending => 0,
110 TransferState::InProgress { chunks_done, .. } => *chunks_done,
111 TransferState::Paused { chunks_done } => *chunks_done,
112 TransferState::Completed => total,
113 TransferState::Failed { .. } => self.received_chunks.len() as u32,
114 };
115 f64::from(done) / f64::from(total)
116 }
117
118 pub fn is_complete(&self) -> bool {
120 self.state == TransferState::Completed
121 }
122}
123
124#[derive(Clone, Debug, Default)]
126pub struct TransferManagerStats {
127 pub active_transfers: usize,
129 pub completed_transfers: u64,
131 pub failed_transfers: u64,
133 pub total_bytes_transferred: u64,
135}
136
137pub struct StreamingBlockTransfer {
142 pub transfers: HashMap<u64, BlockTransfer>,
144 pub next_id: u64,
146 pub current_tick: u64,
148}
149
150impl StreamingBlockTransfer {
151 pub fn new() -> Self {
153 Self {
154 transfers: HashMap::new(),
155 next_id: 1,
156 current_tick: 0,
157 }
158 }
159
160 pub fn start_transfer(
165 &mut self,
166 cid: String,
167 peer_id: String,
168 direction: TransferDirection,
169 total_bytes: u64,
170 chunk_size: u64,
171 ) -> u64 {
172 let id = self.next_id;
173 self.next_id += 1;
174
175 let effective_chunk_size = if chunk_size == 0 { 65536 } else { chunk_size };
176
177 let transfer = BlockTransfer {
178 transfer_id: id,
179 cid,
180 peer_id,
181 direction,
182 total_bytes,
183 chunk_size: effective_chunk_size,
184 state: TransferState::Pending,
185 received_chunks: Vec::new(),
186 started_at_tick: self.current_tick,
187 };
188
189 self.transfers.insert(id, transfer);
190 id
191 }
192
193 pub fn receive_chunk(&mut self, transfer_id: u64, chunk: TransferChunk) -> Result<(), String> {
203 let transfer = self
204 .transfers
205 .get_mut(&transfer_id)
206 .ok_or_else(|| format!("unknown transfer id: {transfer_id}"))?;
207
208 match &transfer.state {
210 TransferState::Paused { .. } => {
211 return Err(format!("transfer {transfer_id} is paused"));
212 }
213 TransferState::Completed => {
214 return Err(format!("transfer {transfer_id} is already completed"));
215 }
216 TransferState::Failed { reason } => {
217 return Err(format!("transfer {transfer_id} has failed: {reason}"));
218 }
219 TransferState::Pending | TransferState::InProgress { .. } => {}
220 }
221
222 if !chunk.is_valid() {
224 return Err(format!(
225 "checksum mismatch for chunk {} of transfer {transfer_id}: expected {}, got {}",
226 chunk.chunk_index,
227 TransferChunk::compute_checksum(&chunk.data),
228 chunk.checksum,
229 ));
230 }
231
232 if transfer.received_chunks.contains(&chunk.chunk_index) {
234 return Err(format!(
235 "chunk {} already received for transfer {transfer_id}",
236 chunk.chunk_index
237 ));
238 }
239
240 transfer.received_chunks.push(chunk.chunk_index);
241 let chunks_done = transfer.received_chunks.len() as u32;
242 let total = transfer.total_chunks();
243
244 if chunks_done >= total {
245 transfer.state = TransferState::Completed;
246 } else {
247 transfer.state = TransferState::InProgress {
248 chunks_done,
249 total_chunks: total,
250 };
251 }
252
253 Ok(())
254 }
255
256 pub fn pause(&mut self, transfer_id: u64) -> bool {
261 let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
262 return false;
263 };
264
265 let chunks_done = match &transfer.state {
266 TransferState::Pending => 0,
267 TransferState::InProgress { chunks_done, .. } => *chunks_done,
268 _ => return false,
269 };
270
271 transfer.state = TransferState::Paused { chunks_done };
272 true
273 }
274
275 pub fn resume(&mut self, transfer_id: u64) -> bool {
279 let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
280 return false;
281 };
282
283 let chunks_done = match &transfer.state {
284 TransferState::Paused { chunks_done } => *chunks_done,
285 _ => return false,
286 };
287
288 let total_chunks = transfer.total_chunks();
289 transfer.state = TransferState::InProgress {
290 chunks_done,
291 total_chunks,
292 };
293 true
294 }
295
296 pub fn fail(&mut self, transfer_id: u64, reason: String) -> bool {
302 let Some(transfer) = self.transfers.get_mut(&transfer_id) else {
303 return false;
304 };
305
306 match transfer.state {
307 TransferState::Completed | TransferState::Failed { .. } => return false,
308 _ => {}
309 }
310
311 transfer.state = TransferState::Failed { reason };
312 true
313 }
314
315 pub fn stats(&self) -> TransferManagerStats {
317 let mut stats = TransferManagerStats::default();
318
319 for transfer in self.transfers.values() {
320 match &transfer.state {
321 TransferState::Pending
322 | TransferState::InProgress { .. }
323 | TransferState::Paused { .. } => {
324 stats.active_transfers += 1;
325 }
326 TransferState::Completed => {
327 stats.completed_transfers += 1;
328 stats.total_bytes_transferred += transfer.total_bytes;
329 }
330 TransferState::Failed { .. } => {
331 stats.failed_transfers += 1;
332 }
333 }
334 }
335
336 stats
337 }
338
339 pub fn advance_tick(&mut self) {
341 self.current_tick += 1;
342 }
343}
344
345impl Default for StreamingBlockTransfer {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn make_chunk(index: u32, data: Vec<u8>) -> TransferChunk {
360 let checksum = TransferChunk::compute_checksum(&data);
361 TransferChunk {
362 chunk_index: index,
363 data,
364 checksum,
365 }
366 }
367
368 #[test]
370 fn test_start_transfer_unique_ids() {
371 let mut mgr = StreamingBlockTransfer::new();
372 let id1 = mgr.start_transfer(
373 "cid1".into(),
374 "peer1".into(),
375 TransferDirection::Download,
376 1024,
377 512,
378 );
379 let id2 = mgr.start_transfer(
380 "cid2".into(),
381 "peer2".into(),
382 TransferDirection::Upload,
383 2048,
384 512,
385 );
386 assert_ne!(id1, id2);
387 assert!(id2 > id1);
388 }
389
390 #[test]
392 fn test_start_transfer_pending_state() {
393 let mut mgr = StreamingBlockTransfer::new();
394 let id = mgr.start_transfer(
395 "cid".into(),
396 "peer".into(),
397 TransferDirection::Download,
398 100,
399 64,
400 );
401 let t = &mgr.transfers[&id];
402 assert_eq!(t.state, TransferState::Pending);
403 }
404
405 #[test]
407 fn test_total_chunks_exact_multiple() {
408 let mut mgr = StreamingBlockTransfer::new();
409 let id = mgr.start_transfer(
410 "c".into(),
411 "p".into(),
412 TransferDirection::Download,
413 65536,
414 65536,
415 );
416 assert_eq!(mgr.transfers[&id].total_chunks(), 1);
417 }
418
419 #[test]
421 fn test_total_chunks_partial() {
422 let mut mgr = StreamingBlockTransfer::new();
423 let id = mgr.start_transfer(
424 "c".into(),
425 "p".into(),
426 TransferDirection::Download,
427 65537,
428 65536,
429 );
430 assert_eq!(mgr.transfers[&id].total_chunks(), 2);
431 }
432
433 #[test]
435 fn test_total_chunks_zero_bytes() {
436 let mut mgr = StreamingBlockTransfer::new();
437 let id = mgr.start_transfer(
438 "c".into(),
439 "p".into(),
440 TransferDirection::Download,
441 0,
442 65536,
443 );
444 assert_eq!(mgr.transfers[&id].total_chunks(), 0);
445 }
446
447 #[test]
449 fn test_receive_chunk_updates_state() {
450 let mut mgr = StreamingBlockTransfer::new();
451 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
453 let chunk = make_chunk(0, vec![1u8; 10]);
454 mgr.receive_chunk(id, chunk)
455 .expect("receive_chunk should succeed");
456 match &mgr.transfers[&id].state {
457 TransferState::InProgress {
458 chunks_done,
459 total_chunks,
460 } => {
461 assert_eq!(*chunks_done, 1);
462 assert_eq!(*total_chunks, 2);
463 }
464 other => panic!("expected InProgress, got {other:?}"),
465 }
466 }
467
468 #[test]
470 fn test_receive_chunk_checksum_mismatch() {
471 let mut mgr = StreamingBlockTransfer::new();
472 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
473 let chunk = TransferChunk {
474 chunk_index: 0,
475 data: vec![42u8; 10],
476 checksum: 0xDEAD_BEEF, };
478 let result = mgr.receive_chunk(id, chunk);
479 assert!(result.is_err(), "expected error for checksum mismatch");
480 assert!(result.unwrap_err().contains("checksum mismatch"));
481 }
482
483 #[test]
485 fn test_all_chunks_received_completes_transfer() {
486 let mut mgr = StreamingBlockTransfer::new();
487 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
488 for i in 0..3u32 {
489 let chunk = make_chunk(i, vec![i as u8; 10]);
490 mgr.receive_chunk(id, chunk)
491 .expect("chunk should be accepted");
492 }
493 assert_eq!(mgr.transfers[&id].state, TransferState::Completed);
494 assert!(mgr.transfers[&id].is_complete());
495 }
496
497 #[test]
499 fn test_duplicate_chunk_rejected() {
500 let mut mgr = StreamingBlockTransfer::new();
501 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
502 let chunk0 = make_chunk(0, vec![1u8; 10]);
503 mgr.receive_chunk(id, chunk0.clone())
504 .expect("first receive should succeed");
505 let dup = make_chunk(0, vec![1u8; 10]);
506 let result = mgr.receive_chunk(id, dup);
507 assert!(result.is_err());
508 assert!(result.unwrap_err().contains("already received"));
509 }
510
511 #[test]
513 fn test_pause_in_progress() {
514 let mut mgr = StreamingBlockTransfer::new();
515 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
516 let chunk = make_chunk(0, vec![0u8; 10]);
517 mgr.receive_chunk(id, chunk)
518 .expect("test: receive_chunk should accept valid chunk");
519 assert!(mgr.pause(id));
520 match &mgr.transfers[&id].state {
521 TransferState::Paused { chunks_done } => assert_eq!(*chunks_done, 1),
522 other => panic!("expected Paused, got {other:?}"),
523 }
524 }
525
526 #[test]
528 fn test_pause_pending() {
529 let mut mgr = StreamingBlockTransfer::new();
530 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 100, 10);
531 assert!(mgr.pause(id));
532 assert_eq!(
533 mgr.transfers[&id].state,
534 TransferState::Paused { chunks_done: 0 }
535 );
536 }
537
538 #[test]
540 fn test_resume_paused() {
541 let mut mgr = StreamingBlockTransfer::new();
542 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 30, 10);
543 let chunk = make_chunk(0, vec![7u8; 10]);
544 mgr.receive_chunk(id, chunk)
545 .expect("test: receive_chunk should accept valid chunk");
546 mgr.pause(id);
547 assert!(mgr.resume(id));
548 match &mgr.transfers[&id].state {
549 TransferState::InProgress {
550 chunks_done,
551 total_chunks,
552 } => {
553 assert_eq!(*chunks_done, 1);
554 assert_eq!(*total_chunks, 3);
555 }
556 other => panic!("expected InProgress, got {other:?}"),
557 }
558 }
559
560 #[test]
562 fn test_resume_non_paused_returns_false() {
563 let mut mgr = StreamingBlockTransfer::new();
564 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
565 assert!(!mgr.resume(id)); }
567
568 #[test]
570 fn test_fail_sets_failed_state() {
571 let mut mgr = StreamingBlockTransfer::new();
572 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 100, 10);
573 assert!(mgr.fail(id, "network timeout".into()));
574 match &mgr.transfers[&id].state {
575 TransferState::Failed { reason } => assert_eq!(reason, "network timeout"),
576 other => panic!("expected Failed, got {other:?}"),
577 }
578 }
579
580 #[test]
582 fn test_fail_on_completed_returns_false() {
583 let mut mgr = StreamingBlockTransfer::new();
584 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 10, 10);
585 let chunk = make_chunk(0, vec![0u8; 10]);
586 mgr.receive_chunk(id, chunk)
587 .expect("test: receive_chunk should accept valid chunk");
588 assert!(mgr.transfers[&id].is_complete());
589 assert!(!mgr.fail(id, "too late".into()));
590 }
591
592 #[test]
594 fn test_progress_values() {
595 let mut mgr = StreamingBlockTransfer::new();
596 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 40, 10);
598 assert_eq!(mgr.transfers[&id].progress(), 0.0);
599
600 let chunk0 = make_chunk(0, vec![1u8; 10]);
601 mgr.receive_chunk(id, chunk0)
602 .expect("test: receive_chunk should accept chunk 0");
603 let p = mgr.transfers[&id].progress();
604 assert!((p - 0.25).abs() < f64::EPSILON, "expected 0.25 got {p}");
605
606 let chunk1 = make_chunk(1, vec![2u8; 10]);
607 mgr.receive_chunk(id, chunk1)
608 .expect("test: receive_chunk should accept chunk 1");
609 let p = mgr.transfers[&id].progress();
610 assert!((p - 0.5).abs() < f64::EPSILON, "expected 0.5 got {p}");
611 }
612
613 #[test]
615 fn test_stats_counts() {
616 let mut mgr = StreamingBlockTransfer::new();
617
618 mgr.start_transfer(
620 "c1".into(),
621 "p1".into(),
622 TransferDirection::Download,
623 10,
624 10,
625 );
626 mgr.start_transfer("c2".into(), "p2".into(), TransferDirection::Upload, 10, 10);
627
628 let id_done = mgr.start_transfer(
630 "c3".into(),
631 "p3".into(),
632 TransferDirection::Download,
633 10,
634 10,
635 );
636 let chunk = make_chunk(0, vec![5u8; 10]);
637 mgr.receive_chunk(id_done, chunk)
638 .expect("test: receive_chunk should accept valid chunk for completed transfer");
639
640 let id_fail = mgr.start_transfer(
642 "c4".into(),
643 "p4".into(),
644 TransferDirection::Download,
645 10,
646 10,
647 );
648 mgr.fail(id_fail, "oops".into());
649
650 let stats = mgr.stats();
651 assert_eq!(stats.active_transfers, 2);
652 assert_eq!(stats.completed_transfers, 1);
653 assert_eq!(stats.failed_transfers, 1);
654 assert_eq!(stats.total_bytes_transferred, 10);
655 }
656
657 #[test]
659 fn test_advance_tick() {
660 let mut mgr = StreamingBlockTransfer::new();
661 assert_eq!(mgr.current_tick, 0);
662 mgr.advance_tick();
663 mgr.advance_tick();
664 assert_eq!(mgr.current_tick, 2);
665 }
666
667 #[test]
669 fn test_started_at_tick_recorded() {
670 let mut mgr = StreamingBlockTransfer::new();
671 mgr.advance_tick();
672 mgr.advance_tick();
673 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Upload, 10, 10);
674 assert_eq!(mgr.transfers[&id].started_at_tick, 2);
675 }
676
677 #[test]
679 fn test_receive_chunk_rejected_when_paused() {
680 let mut mgr = StreamingBlockTransfer::new();
681 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
682 mgr.pause(id);
683 let chunk = make_chunk(0, vec![3u8; 10]);
684 let result = mgr.receive_chunk(id, chunk);
685 assert!(result.is_err());
686 assert!(result.unwrap_err().contains("paused"));
687 }
688
689 #[test]
691 fn test_is_complete_false_for_in_progress() {
692 let mut mgr = StreamingBlockTransfer::new();
693 let id = mgr.start_transfer("c".into(), "p".into(), TransferDirection::Download, 20, 10);
694 let chunk = make_chunk(0, vec![9u8; 10]);
695 mgr.receive_chunk(id, chunk)
696 .expect("test: receive_chunk should accept valid chunk");
697 assert!(!mgr.transfers[&id].is_complete());
698 }
699
700 #[test]
702 fn test_receive_chunk_unknown_id() {
703 let mut mgr = StreamingBlockTransfer::new();
704 let chunk = make_chunk(0, vec![1u8]);
705 let result = mgr.receive_chunk(9999, chunk);
706 assert!(result.is_err());
707 assert!(result.unwrap_err().contains("unknown transfer id"));
708 }
709}